Home / Spring Boot / Production and Testing / Lombok and MapStruct: Less Boilerplate

Lombok and MapStruct: Less Boilerplate

Lombok generates getters, constructors and builders at compile time. MapStruct generates type-safe mapping code between entities and DTOs. Both save many lines, but need correct setup and some care with JPA.

Lombok annotations

AnnotationGenerates
@Getter / @SetterGetters and setters
@ToString, @EqualsAndHashCodetoString, equals and hashCode
@NoArgsConstructor / @AllArgsConstructor / @RequiredArgsConstructorConstructors (Required = final fields only, perfect for constructor injection)
@DataGetter, setter, toString, equals, hashCode, required-args constructor
@ValueImmutable class (all fields private final)
@BuilderBuilder pattern
@Slf4jprivate static final Logger log
@WithCopy with one field changed
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
@Slf4j
@Service
@RequiredArgsConstructor                     // constructor injection without writing the constructor
public class OrderService {

    private final OrderRepository repository;
    private final PaymentClient payments;

    public void place(Long id) {
        log.info("Placing order {}", id);
        // ...
    }
}

@Value
@Builder
public class CreateUserRequest {
    String name;
    String email;
    @Builder.Default int age = 18;
}
// CreateUserRequest r = CreateUserRequest.builder().name("Ram").email("ram@x.com").build();

Lombok and JPA entities: be careful

@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Customer {
    @Id @GeneratedValue private Long id;
    private String name;

    @OneToMany(mappedBy = "customer")
    @ToString.Exclude
    private List<OrderEntity> orders = new ArrayList<>();

    @Override public boolean equals(Object o) {
        return this == o || (o instanceof Customer c && id != null && id.equals(c.id));
    }
    @Override public int hashCode() { return getClass().hashCode(); }       // stable, works before and after save
}

Java records give you immutability, equals, hashCode and toString without Lombok. Use records for DTOs, and Lombok mainly for entities and services.

MapStruct: generate mappers at compile time

<properties><mapstruct.version>1.6.3</mapstruct.version></properties>

<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${mapstruct.version}</version>
    </dependency>
</dependencies>

<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.projectlombok</groupId><artifactId>lombok-mapstruct-binding</artifactId><version>0.2.0</version></path>
          <path><groupId>org.mapstruct</groupId><artifactId>mapstruct-processor</artifactId><version>${mapstruct.version}</version></path>
        </annotationProcessorPaths>
      </configuration>
    </plugin>
  </plugins>
</build>

The order matters: Lombok first, then the lombok-mapstruct-binding, then MapStruct, so MapStruct sees the getters generated by Lombok.

@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR)   // missing mappings fail the build
public interface ProductMapper {

    @Mapping(source = "category.name", target = "categoryName")
    @Mapping(target = "price", expression = "java(entity.getPrice().setScale(2, java.math.RoundingMode.HALF_UP))")
    ProductResponse toResponse(Product entity);

    List<ProductResponse> toResponses(List<Product> entities);

    @Mapping(target = "id", ignore = true)
    Product toEntity(ProductRequest request);

    @Mapping(target = "id", ignore = true)
    void update(ProductRequest request, @MappingTarget Product entity);             // update an existing entity
}

@Service
@RequiredArgsConstructor
public class ProductService {
    private final ProductRepository repository;
    private final ProductMapper mapper;                    // a Spring bean generated by MapStruct

    public ProductResponse get(Long id) { return mapper.toResponse(repository.findById(id).orElseThrow()); }
}
Mapping approachProsCons
Manual mapping codeClear, no magicBoring, easy to forget a field
MapStructFast (plain generated Java), compile time errorsAnnotation processor setup
ModelMapper / reflection mappersLittle codeRuntime errors, slower, magic

Common mistake: Putting @Data on JPA entities. Generated equals, hashCode and toString touch lazy collections and produce LazyInitializationException, infinite recursion or wrong behavior in HashSets. Use focused annotations on entities.

Interview questions

Does Lombok work at runtime?

No. It is an annotation processor that changes the compiled class at build time. Your IDE needs the Lombok plugin support (built in modern IDEs).

Why does MapStruct not see my Lombok getters?

The annotation processors are in the wrong order or lombok-mapstruct-binding is missing. Lombok must run before MapStruct.

Records or Lombok?

Records for immutable DTOs and value objects. Lombok for entities (with care) and to remove constructor boilerplate in services.