Java Record vs Lombok Data: Choosing the Right Approach
java record vs lombok data: Compare Java records and Lombok @Data for creating data classes. Learn syntax, immutability, customization, compatibility, and when to use...
When you need a simple class that carries data, java record vs lombok data is a decision that affects code structure, immutability, and build dependencies. Java records, introduced as a final feature in Java 16, provide a compact syntax for immutable data carriers. Lombok's @Data annotation generates getters, setters, equals, hashCode, toString, and a required-args constructor for mutable or partially mutable classes. The choice is not always obvious because both reduce boilerplate, but they behave differently in several important ways.
What Java Records Provide
A record is a class that is designed to be a transparent carrier of immutable data. You declare the components in the header, and the compiler generates the private final fields, public accessor methods, equals, hashCode, and toString based on those components. The canonical constructor is also generated, and you can add compact constructors for validation.
public record Point(int x, int y) { public Point { if (x < 0 || y < 0) { throw new IllegalArgumentException("Coordinates must be non-negative"); } } }
This record is immutable. The fields are private final, and there are no setters. The accessor methods are named x() and y(), not getX() and getY(). Records also implicitly extend java.lang.Record, so they cannot extend another class. They can implement interfaces, however, and you can add static methods, instance methods, and additional constructors.
Records are a language feature, so they require Java 16 or later (or Java 14/15 with preview flags). The compiler generates the implementation, which means there is no runtime dependency on a library like Lombok.
What Lombok @Data Provides
Lombok's @Data is an annotation that processes source code at compile time. It generates getters for all fields, setters for non-final fields, equals, hashCode, toString, and a constructor that takes all final fields and all non-final fields that are not initialized. The generated code is written into your class during compilation, but you do not see it in the source.
import lombok.Data; @Data public class User { private final Long id; private String name; private String email; }
With this class, you get getId(), getName(), setName(), setEmail(), equals, hashCode, toString, and a constructor accepting id, name, and email. The id field is final, so it has no setter. The other fields are mutable because they are not final.
Lombok works with any Java version that the annotation processor supports, but it requires adding the Lombok dependency and configuring your build tool and IDE to use the annotation processor. The generated code is not visible in the source, which can make debugging and tooling slightly more complex.
Key Differences in Behavior
Records and @Data differ fundamentally in immutability and in the generated methods.
| Behavior | Java Record | Lombok @Data |
|---|---|---|
| Immutability | Always immutable | Mutable unless all fields final |
| Accessor naming | x(), y() | getX(), setX() |
| Setters | Not generated | Generated for non-final fields |
| equals/hashCode | Based on all components | Based on all fields |
| toString | Includes all components | Includes all fields |
| Constructor | Canonical constructor | Required-args constructor |
Records enforce immutability by design. You cannot declare a record with mutable fields, and there is no mechanism to add a setter. Lombok @Data is typically used with mutable fields, although you can make a class effectively immutable by declaring all fields final. In that case, Lombok still generates getters but no setters, and the generated constructor takes all fields.
Another difference is the accessor naming convention. Records use the component name directly as the method name. Lombok follows the JavaBeans convention with get and set prefixes. This matters if your code or a framework relies on property naming conventions. For example, Jackson can serialize records and Lombok-generated getters, but the default configuration differs. Records have built-in support in Jackson 2.12+ for deserialization using the canonical constructor, while Lombok requires either the getters or a custom deserializer.
Customization and Extension
Records allow you to add methods, static fields, and additional constructors, but the component list is fixed. You cannot add instance fields beyond the components. The canonical constructor can be customized with a compact constructor, as shown earlier. You can also override equals, hashCode, and toString, but that is rarely necessary because the generated versions are usually correct.
Lombok @Data gives you more flexibility because you can add any fields and methods to the class. You can also use other Lombok annotations like @Builder, @NoArgsConstructor, or @AllArgsConstructor alongside @Data to generate additional constructors or builders. However, this flexibility comes at the cost of potentially breaking the immutability contract if you are not careful.
A record cannot be extended by another class, which can be a limitation if you need inheritance. Lombok classes are regular classes, so they can be extended. But inheritance with mutable state often introduces complexity in equals and hashCode, and records avoid that entirely by being final.
Performance and Runtime Considerations
Records do not add a runtime dependency. The generated methods are plain Java bytecode, and the JVM can optimize them like any other method. The equals and hashCode implementations are based on the component types and use Objects.equals and Objects.hashCode internally, which is similar to what Lombok generates. There is no reflection overhead in normal use.
Lombok generates code at compile time, so the runtime behavior is the same as if you had written the methods manually. The main performance consideration is the size of the generated class. Lombok can generate a large amount of code for a class with many fields, which increases the class file size slightly. This is rarely a practical concern.
One area where records have an advantage is serialization. Records have a well-defined serialization mechanism: the canonical constructor is used during deserialization, and the fields are written in a specific order. This makes records more predictable for frameworks that rely on serialization, such as Java's built-in serialization or JSON libraries. Lombok classes rely on the default serialization behavior, which can be more fragile if the class structure changes.
Compatibility and Build Setup
Records require Java 16 or later. If your project targets an older Java version, records are not an option. Lombok works with older Java versions, but you must add the Lombok dependency to your build file and configure the annotation processor. For example, in Maven you add Lombok as a provided dependency and ensure the compiler plugin picks it up. In Gradle, you add the Lombok annotation processor to the annotationProcessor configuration.
IDE support is also a factor. Modern IDEs like IntelliJ IDEA and Eclipse have full support for records, including refactoring and code navigation. Lombok also has IDE plugins, but they require installation and can occasionally lag behind new Java versions. If your team uses an IDE without Lombok plugin support, the generated methods will not be visible, which can confuse developers.
Another compatibility issue is the interaction with other annotation processors. Records are a language feature, so they do not interfere with annotation processing. Lombok uses an annotation processor itself, and it can conflict with other processors that modify the AST, such as MapStruct or QueryDSL. These conflicts are rare but can be difficult to debug.
Choosing Based on Your Project Constraints
The decision between a record and Lombok @Data depends on your Java version, the need for immutability, and the flexibility you require.
Use a record when:
- You are on Java 16 or later.
- You want an immutable data carrier by default.
- You do not need to extend the class.
- You want a clean, language-native syntax without external dependencies.
- You are building a DTO, a value object, or a response model that should not change after creation.
Use Lombok @Data when:
- You are on an older Java version (before 16).
- You need mutable fields with setters.
- You need to add extra instance fields that are not part of the constructor.
- You want to use Lombok's other features like
@Builderor@NoArgsConstructorin the same class. - You have an existing codebase that already relies on Lombok and you want consistency.
A common pattern is to use records for new code on Java 16+ and keep Lombok for legacy classes that require mutability. You can also mix both in the same project, as long as the build is configured correctly. For example, you might use a record for a request body and a Lombok @Data class for a JPA entity that needs a no-arg constructor and setters for lazy loading.
One important limitation of records is that they cannot have a no-arg constructor. If a framework requires a default constructor, such as some JPA providers or older serialization libraries, a record will not work. Lombok can generate a @NoArgsConstructor with @Data if you add it explicitly, but then you lose immutability because the final fields would be uninitialized. In that scenario, you need to weigh the framework requirement against the benefits of immutability.
Another consideration is the evolution of the class. Records are designed to be transparent and immutable, so adding a new component changes the canonical constructor and the equals/hashCode behavior. This is fine for value objects that are not expected to change often. Lombok classes are more flexible because you can add fields without changing the constructor if you use a builder or a no-arg constructor. However, that flexibility can lead to partially initialized objects, which is a common source of bugs.
In practice, the choice often comes down to whether you value immutability and language-native support over the flexibility and backward compatibility that Lombok provides. If you are starting a new project on a recent Java version, records are the more straightforward choice. If you are maintaining a large codebase that already uses Lombok, introducing records for new data classes can reduce boilerplate without forcing a migration of existing classes.
Handling Common Pitfalls
One pitfall with records is the accessor naming. If you are using a framework that expects JavaBeans-style getters, you may need to configure it to recognize record accessors. For example, Spring's BeanWrapper can handle records, but some libraries might not. Jackson 2.12+ handles records natively, but older versions require a module or custom serializers.
Another pitfall is using records with JPA. JPA entities require a no-arg constructor and often rely on field mutation for lazy loading. Records are not suitable for JPA entities. You should use records for DTOs and value objects, not for persistent entities. Lombok @Data is a better fit for JPA entities, but you need to be careful with equals and hashCode when using lazy loading. The generated equals and hashCode can trigger lazy loading if they access uninitialized fields, so many developers override these methods manually in JPA entities.
Lombok's @Data also generates a constructor that includes all final and non-initialized non-final fields. If you have many fields, the constructor parameter list can become unwieldy. You can use @Builder to provide a more readable construction pattern, but that adds another dependency on Lombok's generated builder class.
Records have a compact constructor that can validate parameters, but you cannot add logic that modifies the fields because they are final. If you need to normalize data, you can do it in the compact constructor and assign to the fields. For example, you might trim a string or convert a value to a canonical form.
public record Email(String address) { public Email { address = address.trim().toLowerCase(); } }
This is a powerful feature because it ensures the invariant holds for every instance. With Lombok, you would need to write a custom constructor or use a factory method to achieve the same result, and you would have to remember to use that factory method instead of the generated constructor.
Final Technical Consideration: Serialization and Reflection
Records have a special relationship with Java's serialization. The serialized form of a record is based on the component names and types, and deserialization uses the canonical constructor. This makes records more robust to changes in the class structure, as long as the component names and types remain compatible. Lombok classes use the default serialization mechanism, which relies on the class's serialVersionUID and field names. If you change a field name, the serialized form breaks unless you explicitly manage the serialVersionUID.
Reflection on records is also different. The Record class provides methods like getRecordComponents() to inspect the components at runtime. This is useful for frameworks that need to dynamically read or write record fields. Lombok classes are just regular classes, so reflection works as usual, but there is no standard way to discover which fields are part of the data contract.
For most applications, these differences are not critical, but they become important when you are building libraries or frameworks that need to handle arbitrary data classes. If you are designing an API that accepts a data object and needs to serialize it or compare it generically, records offer a more standardized contract.
Ultimately, the choice between a Java record and Lombok @Data is not about which is better in the abstract. It is about the constraints of your Java version, the mutability requirements of your data, and the ecosystem you are working in. Records are a language feature that fits naturally into modern Java. Lombok is a mature library that provides flexibility and compatibility with older codebases. By understanding the behavioral differences, you can make an informed decision for each class you create.