# Lombok @Data vs Java 14 Records: Key Differences for Data Classes

> Compare Lombok @Data and Java 14 records. Understand key differences in mutability, accessors, and language features for creating efficient data classes.

- Repository: [JetBrains/kotlin](https://github.com/jetbrains/kotlin)
- Tags: comparison
- Published: 2026-02-16

---

**Lombok `@Data` generates mutable POJOs with JavaBean-style accessors via compile-time annotation processing, while Java 14 records provide immutable data carriers with component accessors as a native language feature.**

When eliminating boilerplate in data-centric Java classes, developers must choose between Project Lombok's `@Data` annotation and the native `record` feature introduced in Java 14. Both approaches automatically generate `equals()`, `hashCode()`, `toString()`, and constructors, yet they differ fundamentally in mutability, inheritance models, and implementation mechanisms. According to the JetBrains/kotlin documentation on Java interoperability, these distinctions become critical when integrating Kotlin with existing Lombok-generated classes or modern Java records.

## Core Architecture: Annotation Processing vs. Language Feature

### Lombok @Data Implementation

In [`lombok/Data.java`](https://github.com/JetBrains/kotlin/blob/main/lombok/Data.java), the `@Data` annotation serves as a meta-annotation that aggregates `@Getter`, `@Setter`, `@RequiredArgsConstructor`, `@ToString`, and `@EqualsAndHashCode`. The actual bytecode generation occurs in [`lombok/processor/HandlerData.java`](https://github.com/JetBrains/kotlin/blob/main/lombok/processor/HandlerData.java), where the annotation processor injects synthetic methods into the compiled class file. This approach produces standard JavaBean patterns including mutable setters for non-final fields.

### Java Record Implementation

As defined in [`java/lang/Record.java`](https://github.com/JetBrains/kotlin/blob/main/java/lang/Record.java) from the OpenJDK source, records are a sealed abstract class in the Java language. When you declare `public record Person(String name, int age)`, the compiler—following the Java Language Specification §8.10—automatically generates a final class extending `java.lang.Record` with final fields, a canonical constructor, and component accessor methods. No external annotation processor is required.

## Generated Methods: Detailed Comparison

### Accessor Patterns

**Lombok `@Data`** generates traditional JavaBean accessors: `getX()` and `setX(...)` methods for each field. For a field named `name`, it produces `public String getName()` and `public void setName(String name)`.

**Java records** generate **component accessors** that match the component name exactly: `public String name()` and `public int age()`. Additionally, the compiler generates `componentN()` methods (e.g., `component1()`, `component2()`) for positional access, though these are primarily intended for framework use rather than direct application code.

### Mutability and Setters

**Critical distinction**: Lombok generates **setters** for all non-final fields by default, making `@Data` classes mutable unless you explicitly mark fields as `final` or use `@Value` instead.

Records enforce **immutability**—all components are implicitly `final`, and no setters are generated. To "modify" a record, you must implement a copy method manually (similar to Lombok's `@With` functionality):

```java
public record Person(String name, int age) {
    public Person withAge(int newAge) {
        return new Person(this.name, newAge);
    }
}

```

### Equality and Hashing

Both approaches generate `equals()` and `hashCode()` implementations that consider all fields/components. However, Lombok's implementation in [`HandlerData.java`](https://github.com/JetBrains/kotlin/blob/main/HandlerData.java) includes a `canEqual` method to handle inheritance hierarchies correctly, while records generate a straightforward comparison based strictly on the component values without inheritance considerations (since records are final).

## Inheritance and Extension Limitations

### Lombok Class Hierarchy

Classes annotated with `@Data` can extend other non-final classes and can be extended themselves (unless declared `final`). This flexibility comes with a caveat: Lombok's generated `equals` and `hashCode` methods use `canEqual` to ensure symmetric equality when inheritance is involved, as implemented in the annotation processor.

### Record Sealing Constraints

Records are **implicitly final** and cannot extend any class other than `java.lang.Record`. They cannot be subclassed, and they cannot declare instance fields beyond those defined in the record header. This design enforces a strict "transparent carrier for immutable data" pattern, as specified in JLS §8.10.

## Practical Implementation Examples

The following examples demonstrate the actual generated code based on the respective source implementations.

### Lombok @Data Class

```java
// Source annotation: lombok/Data.java
// Processing: lombok/processor/HandlerData.java
@Data
public class Person {
    private String name;
    private int age;
}

```

Generated bytecode structure:

```java
public class Person {
    private String name;
    private int age;
    
    // Required-args constructor (implicit no-arg if no other constructors)
    public Person() {}
    
    // JavaBean accessors
    public String getName() { return this.name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return this.age; }
    public void setAge(int age) { this.age = age; }
    
    // Structural methods
    @Override public boolean equals(Object o) { /* uses canEqual */ }
    @Override public int hashCode() { /* based on fields */ }
    @Override public String toString() { /* field values */ }
    protected boolean canEqual(Object other) { /* inheritance support */ }
}

```

### Java 14+ Record

```java
// Base class: java/lang/Record.java (OpenJDK)
// Specification: JLS §8.10
public record Person(String name, int age) { }

```

Generated members:

```java
public final class Person extends java.lang.Record {
    private final String name;
    private final int age;
    
    // Canonical constructor
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // Component accessors
    public String name() { return this.name; }
    public int age() { return this.age; }
    
    // Positional accessors (primarily for frameworks)
    public String component1() { return this.name; }
    public int component2() { return this.age; }
    
    // Structural methods
    @Override public boolean equals(Object o) { /* component comparison */ }
    @Override public int hashCode() { /* component-based */ }
    @Override public String toString() { /* component values */ }
}

```

## Summary

- **Mutability**: Lombok `@Data` generates mutable classes with setters for non-final fields, while Java records enforce immutability with final components and no setters.
- **Accessor style**: Lombok uses JavaBean `getX()`/`setX()` conventions; records use canonical `x()` accessors and `componentN()` methods.
- **Implementation mechanism**: Lombok relies on compile-time annotation processing via [`lombok/processor/HandlerData.java`](https://github.com/JetBrains/kotlin/blob/main/lombok/processor/HandlerData.java); records are native compiler features defined in JLS §8.10.
- **Inheritance**: Lombok classes support class hierarchies with `canEqual` for equality; records are implicitly final and extend only `java.lang.Record`.
- **IDE support**: Records work natively in all Java 14+ IDEs; Lombok requires a specific plugin for navigation and refactoring of generated members.

## Frequently Asked Questions

### Can I use Lombok annotations with Java records?

No, you cannot apply Lombok's `@Data` or other field-level annotations to record components in a meaningful way because records are final immutable classes with fixed component definitions. Records already provide the boilerplate elimination that Lombok targets, and applying Lombok to records often causes compilation conflicts or redundant processing since the compiler already generates the standard methods.

### Do Java records generate setters like Lombok @Data?

No, Java records do not generate setters. All record components are implicitly final, making records immutable by design. To update data in a record, you must manually implement a copy method that returns a new instance with modified values, similar to Lombok's `@With` functionality but requiring explicit implementation in the record body.

### Which approach offers better IDE and tooling support?

Java records offer superior IDE support because they are a native language feature since Java 14. Any standard Java IDE provides full navigation, refactoring, and code completion for records without additional plugins. Lombok requires a specific IDE plugin (such as the Lombok IntelliJ plugin) to recognize generated methods like `getName()` or `equals()` for proper navigation and refactoring operations.

### Can Kotlin interoperate with both Lombok @Data classes and Java records?

Yes, according to the JetBrains/kotlin documentation on Java interoperability, Kotlin can consume both Lombok-generated classes and Java records. However, Kotlin treats them differently: Lombok classes appear as standard Java classes with synthetic methods, while records are recognized as Kotlin data-like classes with component functions mapped to the record's accessor methods. When using Lombok with Kotlin, ensure the Lombok plugin is configured correctly to generate bytecode before Kotlin compilation.