Skip to content

Lombok vs Java Records: Which Should You Use When?

I was refactoring a Java 17 codebase when I hit a wall: my @Data annotated class was perfect for a DTO, but someone suggested I should use Java Records instead. I tried converting it—and broke my JPA entities. That’s when I realized the choice isn’t “Lombok or Records”—it’s “Lombok AND Records, each in the right place.”

The Core Difference

Lombok generates boilerplate code at compile time. Records are a language-level feature for immutable data carriers. They solve different problems:

Lombok -> Reduces boilerplate on ANY class
Records -> Native immutable data carriers

The 80/20 rule applies here: Records cover about 80% of what Lombok does for data classes. But that remaining 20% (mutable state, JPA entities, builders) still needs Lombok.

When Records Work Perfectly

Records shine for immutable data transfer. If your object is a “bag of data” that never changes after creation, use a Record.

DTOs and API Responses

UserDto.java (Before - Lombok)
@Data
@AllArgsConstructor
public class UserDto {
private Long id;
private String name;
private String email;
}
UserDto.java (After - Record)
public record UserDto(Long id, String name, String email) {
}

The Record version is cleaner and intention-revealing. It says “this is immutable data” by design.

Value Objects with Validation

Records support compact constructors for validation:

Money.java
public record Money(BigDecimal amount, Currency currency) {
public Money {
Objects.requireNonNull(amount, "amount cannot be null");
Objects.requireNonNull(currency, "currency cannot be null");
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
}
public Money add(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(amount.add(other.amount), currency);
}
}

Configuration Objects

DbConfig.java
public record DbConfig(
String host,
int port,
String database,
int maxConnections
) {
public String jdbcUrl() {
return String.format("jdbc:postgresql://%s:%d/%s", host, port, database);
}
}

When Lombok Is Still Necessary

JPA/Hibernate Entities (Critical)

Records do NOT work with JPA entities. I learned this the hard way:

User.java (Wrong - Record with JPA)
@Entity
@Table(name = "users")
public record User( // This will FAIL!
@Id @GeneratedValue
Long id,
String name,
String email
) {}

JPA requires:

  • A no-args constructor
  • Setters for lazy loading proxies
  • Non-final fields for bytecode enhancement
User.java (Correct - Lombok with JPA)
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@Id @GeneratedValue
private Long id;
private String name;
private String email;
}

Builder Pattern

Records don’t have a native builder. Lombok’s @Builder remains essential for complex object construction:

EmailRequest.java
@Data
@Builder
public class EmailRequest {
private String to;
private String subject;
private String body;
private List<String> cc;
private List<String> attachments;
private boolean html;
private Priority priority;
}
// Usage
EmailRequest request = EmailRequest.builder()
.subject("Hello")
.body("<p>Content</p>")
.html(true)
.build();

Mutable State with Custom Logic

When you need setters with validation:

Account.java
@Data
public class Account {
private BigDecimal balance;
public void setBalance(BigDecimal balance) {
if (balance.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Balance cannot be negative");
}
this.balance = balance;
}
}

Logging with @Slf4j

Lombok provides convenient logging:

Service.java
@Slf4j
@Service
public class PaymentService {
public void process(Payment payment) {
log.info("Processing payment: {}", payment.getId());
}
}

Decision Flow

Use this mental model:

+-----------------------------------+
| Do you need mutable state? |
+---------------+-------------------+
|
+-------+-------+
| |
YES NO
| |
v v
+---------------+ +-------------------+
| Use Lombok | | Is it a JPA |
| (@Data, etc.) | | entity? |
+---------------+ +---------+---------+
|
+-------+-------+
| |
YES NO
| |
v v
+---------------+ +---------------+
| Use Lombok | | Use Record |
| (@Entity) | | (immutable) |
+---------------+ +---------------+

Quick test: “Could a Record work here?” If yes, use Record. If no, use Lombok.

Practical Scenarios

Use Records For:

  • DTOs for API responses
  • Method return types (tuples)
  • Configuration objects
  • Value objects (Money, Address, Email)
  • Command/Query parameters

Use Lombok For:

  • JPA/Hibernate entities
  • Objects requiring setters
  • Classes needing @Builder
  • Logging with @Slf4j
  • Legacy codebases on Java 8-13

Migration Strategy

Don’t try to convert everything at once:

  1. New code: Default to Records for data classes
  2. Existing DTOs: Convert when you touch them
  3. Entities: Keep Lombok—Records won’t work
  4. Mixed usage: Both in the same project is fine
MixedUsage.java
// Same project, different tools
public record UserDto(Long id, String name) { } // Record for DTO
@Entity
@Data
public class User { // Lombok for entity
@Id private Long id;
private String name;
}

Common Mistakes

Mistake 1: Trying Records with JPA

Wrong.java
@Entity
public record User(@Id Long id, String name) {}
// Fails: Hibernate can't proxy records

Mistake 2: Using Records for mutable configuration

Wrong.java
public record Config(String apiKey) {
// Can't change apiKey after creation!
}

Mistake 3: Over-complicating Records

Wrong.java
public record User(Long id, String name, String email, Address address,
Phone phone, List<Order> orders, Preferences prefs) {
// Too many fields - consider a class with @Builder
}

Summary

Records and Lombok serve different purposes. Records give you immutable data carriers with minimal syntax. Lombok reduces boilerplate on classes that need features Records can’t provide.

The rule of thumb is simple: start with Records for new data classes. If you hit a wall (JPA, setters, builders), switch to Lombok. They coexist beautifully in modern Java projects.

Key takeaways:

  • Records for immutability (DTOs, value objects, configs)
  • Lombok for mutability (JPA entities, builders, setters)
  • Apply the test: “Could a Record work here?”
  • Both tools can coexist in the same project

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