When reading Spring Boot sample code, you’ll see @Data and @RequiredArgsConstructor come up almost every time. They’re convenient, but many of you have probably been burned by using @Data on JPA Entities. In this article, we’ll organize the Lombok annotations you’ll actually use in practice by purpose, and cover how to use them safely.
What does Lombok actually do for you?
Lombok is an annotation processor. It generates code like getters, setters, and constructors at compile time, so even though the source only has annotations, the bytecode contains regular methods.
It’s included in dependency management in Spring Boot’s official spring-boot-starter-parent, and projects generated by spring init sometimes have it included by default in the template. That’s how routinely it’s used as an assumed library.
Adding dependencies and build configuration
For Maven, the standard practice is to treat it as provided-equivalent and exclude it from the artifact via the Spring Boot Maven Plugin’s exclude.
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
For Gradle, don’t forget to specify it for both compileOnly and annotationProcessor. If you only specify one, the build may pass but code generation won’t run.
dependencies {
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}
Breaking down what’s inside @Data
@Data is actually a shortcut that bundles five annotations together.
@Getter@Setter@ToString@EqualsAndHashCode@RequiredArgsConstructor
In other words, it’s a generous package that “generates getters and setters for all fields, auto-generates equals/hashCode and toString, and also creates a constructor for final fields.” It’s convenient, but it’s also an annotation whose use cases need to be selected carefully.
@Data
public class UserDto {
private Long id;
private String name;
}
// The above is equivalent to the below
@Getter
@Setter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public class UserDto {
private Long id;
private String name;
}
For simple data containers like DTOs, @Data is sufficient, but as we’ll discuss later, there are cases where you’ll want to avoid it on JPA Entities.
The standard for DI is @RequiredArgsConstructor + final
Since Spring 4.3, you can omit @Autowired for single-constructor classes. Combining this with @RequiredArgsConstructor makes service classes much cleaner.
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentClient paymentClient;
public Order place(OrderRequest request) {
// ...
return orderRepository.save(new Order(request));
}
}
Only fields with final become constructor arguments, so dependencies are made explicit and mock injection in tests becomes easier. Avoid field injection (@Autowired private SomeService svc;) since it’s hard to test. We dive deeper into DI concepts in What is DI.
The @Builder pitfall is @Builder.Default
@Builder makes DTO construction more readable, but it has a trap where field initial values are ignored.
@Builder
public class SearchCriteria {
private int page;
@Builder.Default
private int size = 20;
@Builder.Default
private List<String> tags = new ArrayList<>();
}
SearchCriteria c = SearchCriteria.builder()
.page(0)
.build();
// becomes size=20, tags=[]
If you forget to add @Builder.Default, size becomes 0 and tags becomes null. For inheritance, use @SuperBuilder, but be careful that you need to add it to both parent and child.
Why you shouldn’t use @Data on JPA Entities
This is the most important point I want to convey. If you put @Data on an Entity, three problems hit you all at once.
1. @ToString causes infinite recursion in bidirectional associations
In a structure where User has List<Order> and Order has User, if both have @ToString, they keep calling each other and you get a StackOverflow.
2. @EqualsAndHashCode changes dynamically based on ID
The equals/hashCode that Lombok generates is based on all fields. If you put an entity with a null ID into a HashSet and then persist it so the ID gets assigned, the hashCode changes and you can’t retrieve it anymore.
3. Setters break aggregate invariants
@Setter makes all fields freely modifiable, so you can no longer protect domain consistency.
The recommendation is to center around @Getter and limit equals/hashCode to just the ID.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class User {
@Id
@GeneratedValue
@EqualsAndHashCode.Include
private Long id;
private String name;
@OneToMany(mappedBy = "user")
private List<Order> orders = new ArrayList<>();
public User(String name) {
this.name = name;
}
}
For Entity design, you’ll deepen your understanding by also reading JPA Entity Relationship Mapping.
Declare loggers in one line with @Slf4j
It’s tedious to write private static final Logger log = LoggerFactory.getLogger(...) every time. Just by adding @Slf4j, the log variable becomes automatically available.
@Service
@RequiredArgsConstructor
@Slf4j
public class PaymentService {
public void charge(Order order) {
log.info("charging order id={}", order.getId());
// ...
}
}
Spring Boot uses Logback + SLF4J by default, so choosing @Slf4j will work straightforwardly. If you’ve switched to Log4j 2, use @Log4j2 instead.
For immutable objects, @Value or record?
@Value is an immutable version of @Data that generates a final class with all fields as private final. Since Java 16, you can use record, so for new code, preferring record is simpler.
@Value
public class Money {
String currency;
BigDecimal amount;
}
public record Money(String currency, BigDecimal amount) {}
Records work without issue with Jackson deserialization and Bean Validation. However, they can’t be used for JPA Entities (they can’t be proxied), so in that case you’ll go with the traditional class + Lombok combination. For separating DTOs and Entities, also see Organizing Mapping with DTOs and MapStruct.
Avoid @SneakyThrows as a rule
@SneakyThrows is magic that lets you throw checked exceptions without a throws declaration, but it makes the existence of the exception invisible from the caller side.
Furthermore, Spring’s declarative transactions (@Transactional) by default only roll back on unchecked exceptions, so hiding checked exceptions with @SneakyThrows can lead to misreading rollback behavior. Even though it’s convenient, it’s safer to avoid it in production code.
When red underlines appear in your IDE
The symptom where compilation passes but the IDE can’t find getName() is caused by Lombok plugin misconfiguration.
- IntelliJ IDEA: Enable the Lombok plugin (recently bundled), and turn on “Enable annotation processing” under Settings > Build, Execution, Deployment > Compiler > Annotation Processors.
- Eclipse: Double-click the Lombok JAR to run the installer and point it to your Eclipse installation.
- VS Code: The Lombok support included in the Java Extension Pack should work automatically. If it doesn’t, install the
Lombok Annotations Supportextension.
Delombok for when you need it
When distributing libraries, for example, there are situations where you don’t want to leave Lombok as a dependency, and delombok comes in handy. It generates plain Java source with Lombok expanded, letting you remove the Lombok dependency from your distribution. You can call it from Maven or Gradle plugins, so it’s enough to look it up when you need it.
Summary
Here are the points to keep in mind for practical work.
- Don’t use
@Dataon Entities; use@Getter+@EqualsAndHashCodelimited to the ID - The basic form for DI is
@RequiredArgsConstructor+finalfields - For DTOs, use
@Builder+@Builder.Default; record is also an option for new code - Unify logging with
@Slf4j - Avoid
@SneakyThrows
Lombok is convenient, but it’s also a library where you can easily slip up with JPA and DI if you use it without being aware of the generated code. Keep in mind what each annotation generates, and you can confidently reap only the benefits.