Skip to content

Is Lombok Still Relevant in Modern Java Projects with Records?

The Question That Started It All

I was reviewing a pull request when a junior developer asked: “Should we still use Lombok? Java has Records now.”

I looked at our codebase. We had Lombok annotations everywhere: @Data, @Builder, @AllArgsConstructor, @Slf4j. Then I looked at newer code using Java Records. Both approaches worked, but were we missing something?

This question sent me down a rabbit hole. Here’s what I found.

What Java Records Actually Solve

Java 14 introduced Records (finalized in Java 16) as immutable data carriers. Let me show you the difference.

Before Records, with Lombok:

UserDTO.java (Lombok)
@Data
@AllArgsConstructor
public class UserDTO {
private Long id;
private String username;
private String email;
}

With Java Records:

UserDTO.java (Record)
public record UserDTO(Long id, String username, String email) {
}

That’s it. One line. Records automatically give you:

  • A canonical constructor
  • Getter methods (id(), username(), email())
  • equals(), hashCode(), and toString()
  • Immutability (all fields are final)

For simple DTOs, API responses, and value objects, Records are cleaner. No external dependency. Native language support.

But here’s where it gets interesting: Records only solve about 80% of what Lombok does.

What Lombok Still Does Better

1. The Builder Pattern

This is where I hit a wall with Records. I had a configuration class with 8 fields, most optional:

Configuration.java (Record - No Builder)
public record Configuration(
String host,
int port,
boolean sslEnabled,
int timeout,
int maxConnections,
String username,
String password,
boolean debug
) {
}

Creating this object was painful:

Creating Configuration (Verbose)
Configuration config = new Configuration(
"localhost", // host
8080, // port
true, // sslEnabled
30, // timeout
100, // maxConnections
"admin", // username
"secret", // password
false // debug
);

Can you tell which value is which? Neither could I during code review.

With Lombok’s @Builder:

Configuration.java (Lombok Builder)
@Builder
@Data
public class Configuration {
private String host = "localhost";
private int port = 80;
private boolean sslEnabled = false;
private int timeout = 30;
private int maxConnections = 100;
private String username;
private String password;
private boolean debug = false;
}
Creating Configuration (Builder)
Configuration config = Configuration.builder()
.host("api.example.com")
.port(443)
.sslEnabled(true)
.username("admin")
.password("secret")
.build();

Much clearer. I know exactly what each value means.

There’s a third-party library called RecordBuilder that adds builder support to Records:

Configuration.java (RecordBuilder)
@RecordBuilder
public record Configuration(
String host,
int port,
boolean sslEnabled,
int timeout,
int maxConnections,
String username,
String password,
boolean debug
) implements ConfigurationBuilder.With {
}

But now I’ve traded Lombok for another dependency. The native solution doesn’t have built-in builder support.

2. JPA Entities Don’t Work with Records

Here’s the big one. I tried to use Records for JPA entities:

User.java (Record - Won't Work with JPA)
@Entity
@Table(name = "users")
public record User(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
Long id,
String username,
String email,
LocalDateTime createdAt
) {
}

Hibernate rejected this immediately. Here’s why:

text title="Hibernate Error"
org.hibernate.InstantiationException: Cannot instantiate abstract class or interface

JPA requirements that Records violate:

  • Default constructor: Hibernate needs a no-args constructor to create proxy objects
  • Setters: JPA entities need to be mutable for dirty checking
  • Non-final class: Hibernate creates proxy subclasses

So for entities, Lombok is still necessary:

User.java (Lombok for JPA)
@Entity
@Table(name = "users")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String email;
@Column(name = "created_at")
private LocalDateTime createdAt;
}

3. Immutable Transformations with @With

I often need to create a modified copy of an object. Lombok’s @With handles this elegantly:

Person.java (Lombok @With)
@With
@Data
public class Person {
private String name;
private int age;
private String city;
}
Person original = new Person("Alice", 30, "NYC");
Person updated = original.withAge(31); // Creates new instance

With Records, you write it manually:

Person.java (Record - Manual With)
public record Person(String name, int age, String city) {
public Person withAge(int newAge) {
return new Person(name, newAge, city);
}
}

Not terrible for one field. But if you have 10 fields and want to modify any of them, you’re writing 10 methods.

4. Fine-Grained equals/hashCode Control

Sometimes I need custom equality logic:

Employee.java (Lombok - Custom Equality)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@Data
public class Employee {
@EqualsAndHashCode.Include
private Long employeeId;
private String name;
private String department;
@EqualsAndHashCode.Exclude
private transient String temporaryData;
}

Records include ALL fields in equals() and hashCode(). No way to exclude fields. You can override the methods, but then you’re writing boilerplate code—the exact thing Records were supposed to eliminate.

Performance Considerations

I found an interesting discussion about compile times:

“One project: 5 minutes with Lombok, 1:30 without.”

Lombok uses annotation processing to modify the AST during compilation. This adds overhead. For large codebases, the impact is noticeable.

But I also found counter-arguments:

“Modern IDEs handle generated code well. The DX improvement outweighs compile time cost.”

My take: If your build is taking 5 minutes, Lombok isn’t your only problem. Focus on modular builds and incremental compilation first.

The Hybrid Approach: When to Use Each

After experimenting, I settled on a hybrid strategy:

text title="Decision Matrix"
+------------------+----------+---------+
| Use Case | Records | Lombok |
+------------------+----------+---------+
| DTOs | X | |
| API Responses | X | |
| Value Objects | X | |
| Configuration | | X |
| JPA Entities | | X |
| Complex Builders | | X |
| Custom Equality | | X |
| Logging (@Slf4j) | | X |
+------------------+----------+---------+

When to Use Records

API Response (Record)
public record UserResponse(
Long id,
String username,
String email,
LocalDateTime createdAt
) {
public static UserResponse from(User user) {
return new UserResponse(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getCreatedAt()
);
}
}

Perfect for:

  • Data transfer between layers
  • API request/response objects
  • Immutable configuration values
  • Value objects (money, ranges, coordinates)

When to Use Lombok

Service Class (Lombok)
@Service
@Slf4j
@RequiredArgsConstructor
public class UserService {
private final UserRepository repository;
private final PasswordEncoder encoder;
public User create(CreateUserRequest request) {
log.info("Creating user: {}", request.username());
// ...
}
}

Perfect for:

  • JPA entities (mutable objects)
  • Service classes with dependency injection
  • Objects needing builder pattern
  • Classes with custom equals/hashCode

Migration Path: What I Recommend

If you’re on Java 14+ and have Lombok throughout your codebase:

Phase 1: Use Records for New Code

  • Start with new DTOs and API responses
  • No migration needed for existing code
  • Learn the Records patterns

Phase 2: Migrate Simple DTOs

Before (Lombok)
@Data
@AllArgsConstructor
public class ProductDTO {
private Long id;
private String name;
private BigDecimal price;
}
After (Record)
public record ProductDTO(Long id, String name, BigDecimal price) {
}

Phase 3: Keep Lombok for Entities and Complex Classes

  • Don’t migrate JPA entities
  • Keep @Builder for complex construction
  • Keep @Slf4j for logging
  • Keep @RequiredArgsConstructor for DI

What NOT to Migrate:

Keep This as Lombok
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Order {
@Id
private Long id;
@ManyToOne
private User user;
@OneToMany(mappedBy = "order")
private List<OrderItem> items = new ArrayList<>();
private OrderStatus status;
private LocalDateTime createdAt;
}

This entity needs mutability. Records won’t work here.

The Verdict

Lombok is still relevant in 2026, but its role has shifted. Here’s my summary:

Records are better for:

  • Immutable data carriers (80% of use cases)
  • Cleaner syntax for simple objects
  • Native language feature, no dependency

Lombok is still needed for:

  • JPA entities (Hibernate requires mutable objects)
  • Builder pattern (no native equivalent)
  • @Slf4j logging
  • @RequiredArgsConstructor for dependency injection
  • Custom equals/hashCode
  • Mutable objects with @With

My recommendation:

  • New project on Java 14+: Use Records for DTOs, Lombok for entities
  • Existing Lombok project: Migrate simple DTOs to Records, keep Lombok elsewhere
  • Java 8-13 project: Keep Lombok, Records aren’t available

The future of Java will likely reduce Lombok’s necessity further. Pattern matching, deconstruction, and potential record enhancements are on the horizon. But for now, both tools have their place in a well-designed Java codebase.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments