MapStruct's @MapperConfig centralizes common mapping rules, global settings, and shared utilities across multiple mappers. Without Spring, mappers generate plain POJOs for manual instantiation.
Why Shared @MapperConfig?
- DRY principle: Define
ignorerules,nullValuePropertyMappingStrategy, etc. once - Consistency: All mappers share identical base behavior
- Inheritance: Prototype methods propagate
@Mappingannotations to concrete mappers - Flexibility: Works as interface (pure config) or abstract class (config + utilities)
Complete Domain Models
public class BaseDto {
private Long id;
private Long createdAt;
private Long version;
// constructors, getters, setters
public BaseDto() {}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Long createdAt) {
this.createdAt = createdAt;
}
public Long getVersion() {
return version;
}
public void setVersion(Long version) {
this.version = version;
}
}
public class BaseEntity {
private Long id;
private Long createdAt;
private Long version;
private Long lastModified;
// constructors, getters, setters
public BaseEntity() {}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Long createdAt) {
this.createdAt = createdAt;
}
public Long getVersion() {
return version;
}
public void setVersion(Long version) {
this.version = version;
}
public Long getLastModified() {
return lastModified;
}
public void setLastModified(Long lastModified) {
this.lastModified = lastModified;
}
}
public class CustomerDto extends BaseDto {
private String firstName;
private String lastName;
private String email;
// constructors, getters, setters
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
public class Customer extends BaseEntity {
private String firstName;
private String lastName;
private String customerEmail;
// constructors, getters, setters
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getCustomerEmail() {
return customerEmail;
}
public void setCustomerEmail(String customerEmail) {
this.customerEmail = customerEmail;
}
}
public class OrderDto extends BaseDto {
private String orderNumber;
private CustomerDto customer;
// constructors, getters, setters
public String getOrderNumber() {
return orderNumber;
}
public void setOrderNumber(String orderNumber) {
this.orderNumber = orderNumber;
}
public CustomerDto getCustomer() {
return customer;
}
public void setCustomer(CustomerDto customer) {
this.customer = customer;
}
}
public class Order extends BaseEntity {
private String orderNumber;
private String customerEmail;
private String status;
// constructors, getters, setters
public String getOrderNumber() {
return orderNumber;
}
public void setOrderNumber(String orderNumber) {
this.orderNumber = orderNumber;
}
public String getCustomerEmail() {
return customerEmail;
}
public void setCustomerEmail(String customerEmail) {
this.customerEmail = customerEmail;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}
@MapperConfig as Interface (Pure Configuration)
GlobalMapperConfig.java:
import org.mapstruct.MapperConfig;
import org.mapstruct.Mapping;
import org.mapstruct.ReportingPolicy;
import org.mapstruct.MappingInheritanceStrategy;
@MapperConfig(
unmappedTargetPolicy = ReportingPolicy.ERROR,
mappingInheritanceStrategy = MappingInheritanceStrategy.AUTO_INHERIT_FROM_CONFIG
)
public interface GlobalMapperConfig {
@Mapping(target = "id", ignore = true)
@Mapping(target = "createdAt", ignore = true)
@Mapping(target = "version", ignore = true)
BaseEntity toBaseEntity(BaseDto dto);
}
CustomerMapper.java:
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
@Mapper(config = GlobalMapperConfig.class)
public interface CustomerMapper {
// AUTO inherits ignore rules for id/createdAt/version
@Mapping(target = "customerEmail", source = "email")
@Mapping(target = "lastModified", ignore = true)
Customer toEntity(CustomerDto dto);
@Mapping(target = "customerEmail", source = "email")
@Mapping(target = "lastModified", ignore = true)
void updateEntity(CustomerDto dto, @MappingTarget Customer entity);
}
Generated CustomerMapperImpl.java:
import javax.annotation.processing.Generated;
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
comments = "version: 1.6.3, compiler: IncrementalProcessingEnvironment from gradle-language-java-9.2.0.jar, environment: Java 25 (Oracle Corporation)"
)
public class CustomerMapperImpl implements CustomerMapper {
@Override
public Customer toEntity(CustomerDto dto) {
if ( dto == null ) {
return null;
}
Customer customer = new Customer();
customer.setCustomerEmail( dto.getEmail() );
customer.setFirstName( dto.getFirstName() );
customer.setLastName( dto.getLastName() );
return customer;
}
@Override
public void updateEntity(CustomerDto dto, Customer entity) {
if ( dto == null ) {
return;
}
entity.setCustomerEmail( dto.getEmail() );
entity.setFirstName( dto.getFirstName() );
entity.setLastName( dto.getLastName() );
}
}
@MapperConfig in Abstract Mapper Class
BaseEntityMapper.java:
import org.mapstruct.BeforeMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingTarget;
import org.mapstruct.Named;
@Mapper(config = GlobalMapperConfig.class)
public abstract class BaseEntityMapper {
// Inherits GlobalMapperConfig ignore rules
@Mapping(target = "lastModified", ignore = true)
public abstract BaseEntity toEntity(BaseDto dto);
// Shared utility method
@Named("normalizeEmail")
protected String normalizeEmail(String email) {
return email != null ? email.trim().toLowerCase() : null;
}
@Mapping(target = "lastModified", ignore = true)
public abstract void updateEntity(BaseDto dto, @MappingTarget BaseEntity entity);
@BeforeMapping
protected void enrichTimestamps(BaseDto dto, @MappingTarget BaseEntity entity) {
entity.setLastModified(System.currentTimeMillis());
}
}
Generated BaseEntityMapperImpl.java:
import javax.annotation.processing.Generated;
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
comments = "version: 1.6.3, compiler: IncrementalProcessingEnvironment from gradle-language-java-9.2.0.jar, environment: Java 25 (Oracle Corporation)"
)
public class BaseEntityMapperImpl extends BaseEntityMapper {
@Override
public BaseEntity toEntity(BaseDto dto) {
if ( dto == null ) {
return null;
}
BaseEntity baseEntity = new BaseEntity();
enrichTimestamps( dto, baseEntity );
return baseEntity;
}
@Override
public void updateEntity(BaseDto dto, BaseEntity entity) {
if ( dto == null ) {
return;
}
enrichTimestamps( dto, entity );
}
}
OrderMapper.java:
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
@Mapper(config = GlobalMapperConfig.class, uses = BaseEntityMapper.class)
public abstract class OrderMapper {
// Inherits GlobalConfig + BaseEntityMapper utilities
@Mapping(target = "customerEmail", source = "customer.email", qualifiedByName = "normalizeEmail")
@Mapping(target = "status", constant = "PENDING")
@Mapping(target = "lastModified", ignore = true)
public abstract Order toEntity(OrderDto dto);
public Order createProcessedOrder(OrderDto dto) {
Order order = toEntity(dto);
order.setStatus("PROCESSED");
return order;
}
}
Generated OrderMapperImpl.java:
import javax.annotation.processing.Generated;
import org.mapstruct.factory.Mappers;
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
comments = "version: 1.6.3, compiler: IncrementalProcessingEnvironment from gradle-language-java-9.2.0.jar, environment: Java 25 (Oracle Corporation)"
)
public class OrderMapperImpl extends OrderMapper {
private final BaseEntityMapper baseEntityMapper = Mappers.getMapper( BaseEntityMapper.class );
@Override
public Order toEntity(OrderDto dto) {
if ( dto == null ) {
return null;
}
Order order = new Order();
baseEntityMapper.enrichTimestamps( dto, order );
order.setCustomerEmail( baseEntityMapper.normalizeEmail( dtoCustomerEmail( dto ) ) );
order.setOrderNumber( dto.getOrderNumber() );
order.setStatus( "PENDING" );
return order;
}
private String dtoCustomerEmail(OrderDto orderDto) {
CustomerDto customer = orderDto.getCustomer();
if ( customer == null ) {
return null;
}
return customer.getEmail();
}
}
Usage in Application Code
public class OrderService {
private final CustomerMapper customerMapper = new CustomerMapperImpl();
private final OrderMapper orderMapper = new OrderMapperImpl();
public Customer createCustomer(CustomerDto dto) {
return customerMapper.toEntity(dto);
}
public Order processOrder(OrderDto dto) {
// Gets 3 layers of configuration
return orderMapper.createProcessedOrder(dto);
}
}
// Fluent factory (alternative)
// import org.mapstruct.factory.Mappers;
// CustomerMapper mapper = Mappers.getMapper(CustomerMapper.class);
// Customer customer = mapper.toEntity(dto);
Inheritance Layers Summary
| Layer | Source | Provides |
|---|---|---|
| GlobalMapperConfig | Interface | id/createdAt/version ignore rules |
| BaseEntityMapper | Abstract class | + normalizeEmail(), @BeforeMapping |
| CustomerMapper | Interface | Specific Customer mappings |
| OrderMapper | Abstract class | Specific Order mappings + createProcessedOrder() |
Key Benefits
- Scalable hierarchy - Global → Base → Specific mappers
- Full type safety - No runtime surprises
This pattern handles dozens of entities while maintaining clean, consistent mapping behavior across your entire application.
Leave a Reply