Lombok and MapStruct
|
This section documents Spring Boot 4.1.x and Spring Framework 7.0.x on the Java 17/21+ baseline, as described by the official Spring Boot reference documentation and each sub-project’s own site (Spring Data, Spring for Apache Kafka, Micrometer, Project Reactor, springdoc-openapi, Spring gRPC, and the others listed in this section’s Bibliography) — which are the references these pages are written and verified against. This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production. Spring Boot and its ecosystem continue to evolve; the examples here target the current 4.1.x / 7.0.x releases. This section’s bibliography lists the reference material consulted while preparing these pages. |
Lombok removes boilerplate (getters, setters, constructors, logging fields) by generating code at compile time, while MapStruct generates compile-time, reflection-free mappers between DTOs and entities. Because both are annotation processors, using them together on the same class requires an extra binding dependency so their processors run in the right order.
Why Lombok
Lombok hooks into javac as an annotation processor and rewrites the in-memory AST during compilation,
inserting methods and fields that are never written by hand and never appear in the .java source. The
trade-off is that the generated code is invisible in the source file — readers, code-review diffs, and tools
that don’t understand Lombok only see the annotation, not the methods it produces — and every IDE needs the
Lombok plugin installed (and annotation processing enabled) to show the generated
members for navigation, autocomplete, and "Find Usages". Delombok (lombok:delombok) can materialize the real
source when that visibility is needed, e.g. for Javadoc generation.
Core annotations
@Getter and @Setter generate accessor methods for fields, at the field or class level:
import lombok.Getter;
import lombok.Setter;
public class Customer {
@Getter
@Setter
private String name;
@Getter
private final String id; // no setter: id is immutable
public Customer(String id) {
this.id = id;
}
}
@RequiredArgsConstructor generates a constructor for every final field (and any field marked
@NonNull), which is the idiomatic way to wire constructor injection without hand-writing it:
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentClient paymentClient;
// Lombok generates:
// OrderService(OrderRepository orderRepository, PaymentClient paymentClient)
}
@Slf4j generates a private static final org.slf4j.Logger log field pre-initialized with the enclosing
class, avoiding the repeated LoggerFactory.getLogger(MyClass.class) line in every class:
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class ShippingService {
public void schedule(String orderId) {
log.info("Scheduling shipment for order {}", orderId);
}
}
Immutable value types: @Value and @Builder
@Value is the immutable counterpart of @Data: it makes the class final, makes every field private
final, and generates getters, equals/hashCode/toString, and an all-args constructor — no setters:
import lombok.Value;
@Value
public class Money {
String currency;
long amountMinorUnits;
}
// usage: new Money("EUR", 1999)
@Builder generates a fluent builder, which is often combined with @Value for immutable DTOs that have many
optional fields:
import lombok.Builder;
import lombok.Value;
@Value
@Builder
public class CustomerDto {
String id;
String name;
String email;
}
// usage:
CustomerDto dto = CustomerDto.builder()
.id("c-1")
.name("Ada Lovelace")
.email("ada@example.com")
.build();
See the full Lombok features reference for the complete annotation list
(@Data, @EqualsAndHashCode, @ToString, @AllArgsConstructor, @NoArgsConstructor, @With, and others)
and their individual configuration options.
Trade-offs to weigh
-
Generated code is invisible — stepping through a debugger, reading a diff, or skimming the file shows only
@Getter/@Builder, not the methods they add, which can surprise reviewers unfamiliar with Lombok. -
IDE plugin requirement — without the Lombok plugin enabled, an IDE reports "cannot find symbol" for generated getters/setters/constructors, even though the project compiles fine on the command line.
-
@Dataon JPA entities is risky — a generatedequals/hashCodethat includes all fields (or a lazy-loaded collection) can trigger unwantedLAZYinitialization or break entity identity semantics; prefer@Getter/@Setterplus an explicit, identifier-basedequals/hashCodeon entities instead of@Dataor@Value. -
Binary compatibility — because members are generated at compile time from the current Lombok version, all modules in a multi-module build should compile against the same Lombok version.
MapStruct: generated, not reflective, mapping
MapStruct is also an annotation processor, but instead of rewriting a class it generates a full
implementation class for an interface you declare, at compile time. The generated mapper is plain Java — direct field assignments and method calls — with no reflection at runtime, which makes it fast and easy to
step through in a debugger. Annotate an interface with @Mapper and declare one method per direction:
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper
public interface CustomerMapper {
CustomerMapper INSTANCE = Mappers.getMapper(CustomerMapper.class);
@Mapping(target = "id", ignore = true)
@Mapping(source = "emailAddress", target = "email")
CustomerEntity toEntity(CustomerDto dto);
@Mapping(source = "email", target = "emailAddress")
CustomerDto toDto(CustomerEntity entity);
}
@Mapping(target = …, source = …) renames or ignores a field when the DTO and entity don’t share exactly
the same property names; every property that matches by name is mapped automatically without any annotation.
Registering the mapper as a Spring bean
Setting componentModel = "spring" makes MapStruct generate the implementation as a @Component, so it can be
constructor-injected like any other Spring bean instead of being looked up via Mappers.getMapper(…):
import org.mapstruct.Mapper;
import org.mapstruct.MappingConstants;
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface OrderMapper {
OrderDto toDto(OrderEntity entity);
OrderEntity toEntity(OrderDto dto);
}
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final OrderMapper orderMapper;
public OrderService(OrderMapper orderMapper) {
this.orderMapper = orderMapper;
}
public OrderDto findOne(OrderEntity entity) {
return orderMapper.toDto(entity);
}
}
Default methods for custom conversions
When a conversion is not a simple property copy (formatting, aggregation, a lookup against another object),
add a default method to the same mapper interface; MapStruct calls it automatically whenever the source and
target types match, without any extra wiring:
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
@Mapper
public interface InvoiceMapper {
@Mapping(target = "issuedOn", expression = "java(formatInstant(entity.getCreatedAt()))")
InvoiceDto toDto(InvoiceEntity entity);
default String formatInstant(Instant instant) {
return instant == null ? null : DateTimeFormatter.ISO_LOCAL_DATE
.withZone(java.time.ZoneOffset.UTC)
.format(instant);
}
}
MapStruct also picks up a default method automatically as a type converter (e.g. Instant to String)
whenever a mapped property needs that exact conversion, with no expression needed. See
the MapStruct reference documentation for mapping
collections, updating an existing target instance (@MappingTarget), and composing mappers with uses = \{
…\}.
Running Lombok and MapStruct together
Both tools are annotation processors that run during the same javac invocation, and MapStruct’s generated
mapper code needs to see the getters/setters that Lombok generates on the DTO/entity classes it maps between.
If the two processors run in the wrong order, MapStruct sees the source before Lombok has added those
accessors and fails to find them. The lombok-mapstruct-binding artifact fixes the ordering and must be
declared as an annotationProcessorPath, alongside Lombok and MapStruct themselves, on the Maven compiler
plugin — listing plain <dependency> entries in the POM is not enough, because that only puts the
processors on the classpath, not in the explicit processor path the compiler plugin uses:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<!-- must come after both lombok and mapstruct-processor -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>${lombok-mapstruct-binding.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
With Gradle, the equivalent is declaring all three as annotationProcessor dependencies, in the same order
(Lombok, MapStruct, then the binding artifact):
dependencies {
compileOnly "org.projectlombok:lombok:${lombokVersion}"
annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
implementation "org.mapstruct:mapstruct:${mapstructVersion}"
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
annotationProcessor "org.projectlombok:lombok-mapstruct-binding:${lombokMapstructBindingVersion}"
}
With the binding artifact in place, a MapStruct @Mapper interface can safely reference DTOs and entities
annotated with @Getter/@Setter/@Builder, and the generated mapper implementation compiles against the
Lombok-generated accessors exactly as if they had been hand-written.
Further reading
-
Lombok features reference — the complete list of annotations (
@Data,@Value,@Builder,@Slf4j, and more) with their configuration options. -
MapStruct reference documentation — mapping strategies, collection mapping, updating existing targets, and mapper composition.