Java Records are a language feature that can replace many of Lombok’s boilerplate-reducing annotations.

Let’s see Lombok in action. Imagine a simple User class with id and username fields.

import lombok.Getter;
import lombok.Setter;
import lombok.EqualsAndHashCode;
import lombok.ToString;

@Getter
@Setter
@EqualsAndHashCode
@ToString
public class UserLombok {
    private int id;
    private String username;
}

This single class definition, with Lombok annotations, gives you:

  • getId() and getUsername()
  • setId(int id) and setUsername(String username)
  • equals(Object o) and hashCode() based on id and username
  • toString() representing UserLombok(id=..., username=...)

Now, let’s look at the equivalent using Java Records.

public record UserRecord(int id, String username) {
}

This UserRecord automatically provides:

  • A public accessor method for each component: id() and username().
  • An equals(Object o) method that compares all components.
  • A hashCode() method that is consistent with equals().
  • A toString() method that represents the record’s state, like UserRecord[id=..., username=...].

The primary problem Java Records solve is the tedious, repetitive creation of simple data-holding classes. Before records, you’d write getters, setters, equals, hashCode, and toString manually or rely on libraries like Lombok. Records bake these essential data-handling methods directly into the Java language.

Internally, a record is a special kind of class. The compiler generates a final class with private final fields for each component declared in the header. It then generates public accessor methods (named after the components), a canonical constructor that initializes all components, and implementations for equals(), hashCode(), and toString(). You can add your own methods, including static factory methods, instance methods, and even implement interfaces, but you cannot modify the automatically generated components or their accessors.

When you have a record, you don’t have setters. The data is immutable by design. This is a key differentiator from Lombok’s @Setter or @Data annotations, which generate mutable fields and setters by default. Immutability is a significant benefit for concurrent programming and for creating reliable data structures, as the state of an object cannot change after it’s created. This often leads to simpler, more predictable code.

The most significant difference in practical use comes down to mutability. While Lombok’s @Data annotation (which combines @Getter, @Setter, @ToString, @EqualsAndHashCode, and @RequiredArgsConstructor) gives you a mutable data carrier, Java Records are inherently immutable. You get the getters (accessor methods) and the constructor, but no setters. If you need mutability, you’d typically create a separate mutable class or potentially use a builder pattern in conjunction with the record, but the record itself remains a snapshot of data. This design choice forces a more functional, immutable-first approach, which can be a powerful paradigm shift for many Java developers accustomed to mutable objects.

The next step is often understanding how to add custom behavior or validation to your records, and how they interact with existing Java APIs that expect traditional classes.

Want structured learning?

Take the full Java course →