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 ignore rules, nullValuePropertyMappingStrategy, etc. once
  • Consistency: All mappers share identical base behavior
  • Inheritance: Prototype methods propagate @Mapping annotations 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

  1. Scalable hierarchy - Global → Base → Specific mappers
  2. Full type safety - No runtime surprises

This pattern handles dozens of entities while maintaining clean, consistent mapping behavior across your entire application.