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:
@Data@AllArgsConstructorpublic class UserDTO { private Long id; private String username; private String email;}With Java Records:
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(), andtoString()- 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:
public record Configuration( String host, int port, boolean sslEnabled, int timeout, int maxConnections, String username, String password, boolean debug) {}Creating this object was painful:
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:
@Builder@Datapublic 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;}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:
@RecordBuilderpublic 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:
@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 interfaceJPA 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:
@Entity@Table(name = "users")@Getter@Setter@NoArgsConstructor@AllArgsConstructor@Builderpublic 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:
@With@Datapublic 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 instanceWith Records, you write it manually:
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:
@EqualsAndHashCode(onlyExplicitlyIncluded = true)@Datapublic 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
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@Slf4j@RequiredArgsConstructorpublic 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
@Data@AllArgsConstructorpublic class ProductDTO { private Long id; private String name; private BigDecimal price;}public record ProductDTO(Long id, String name, BigDecimal price) {}Phase 3: Keep Lombok for Entities and Complex Classes
- Don’t migrate JPA entities
- Keep
@Builderfor complex construction - Keep
@Slf4jfor logging - Keep
@RequiredArgsConstructorfor DI
What NOT to Migrate:
@Entity@Getter@Setter@NoArgsConstructor@AllArgsConstructor@Builderpublic 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)
@Slf4jlogging@RequiredArgsConstructorfor 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:
- 👨💻 Project Lombok Official Site
- 👨💻 JEP 395: Records
- 👨💻 RecordBuilder GitHub
- 👨💻 Baeldung: Java Records
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments