Mapper composition in MapStruct creates modular mappings by delegating between mappers using @Mapper(uses = {...}). The examples below are fully self-contained, compilable classes with complete domain models, mappers, and main methods.

Complete Domain Classes

import java.time.LocalDate;

// Source domain classes
public class Address {
    private String street;
    private String city;

    public Address(String street, String city) {
        this.street = street;
        this.city = city;
    }

    // Getters and setters
    public String getStreet() { return street; }
    public void setStreet(String street) { this.street = street; }
    public String getCity() { return city; }
    public void setCity(String city) { this.city = city; }
}

public class User {
    private String name;
    private Address address;

    public User(String name, Address address) {
        this.name = name;
        this.address = address;
    }

    // Getters and setters
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public Address getAddress() { return address; }
    public void setAddress(Address address) { this.address = address; }
}

// Extended User for annotation example
public class UserEnriched {
    private String firstName;
    private String lastName;
    private int age;
    private Address address;

    public UserEnriched(String firstName, String lastName, int age, Address address) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
        this.address = address;
    }

    // Getters
    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
    public Address getAddress() { return address; }
}

// Target DTO classes
public class AddressDto {
    private String street;
    private String city;

    public AddressDto() {}

    // Getters and setters
    public String getStreet() { return street; }
    public void setStreet(String street) { this.street = street; }
    public String getCity() { return city; }
    public void setCity(String city) { this.city = city; }

    @Override
    public String toString() {
        return street + ", " + city;
    }
}

public class UserDto {
    private String name;
    private AddressDto address;

    public UserDto() {}

    // Getters and setters
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public AddressDto getAddress() { return address; }
    public void setAddress(AddressDto address) { this.address = address; }

    @Override
    public String toString() {
        return name + " <" + address + ">";
    }
}

public class UserEnrichedDto {
    private String fullName;
    private String ageGroup;
    private AddressDto address;

    public UserEnrichedDto() {}

    // Getters and setters
    public String getFullName() { return fullName; }
    public void setFullName(String fullName) { this.fullName = fullName; }
    public String getAgeGroup() { return ageGroup; }
    public void setAgeGroup(String ageGroup) { this.ageGroup = ageGroup; }
    public AddressDto getAddress() { return address; }
    public void setAddress(AddressDto address) { this.address = address; }

    @Override
    public String toString() {
        return fullName + " (" + ageGroup + ") <" + address + ">";
    }
}

Basic Composition Example

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

@Mapper
public interface AddressMapper {
    AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);

    AddressDto toDto(Address address);
}

@Mapper(uses = AddressMapper.class)
public interface UserMapper {
    UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);

    UserDto toDto(User user);
}

// Runnable test
public class BasicComposition {
    public static void main(String[] args) {
        User user = new User("John Doe",
            new Address("123 Main St", "Auckland"));

        UserDto dto = UserMapper.INSTANCE.toDto(user);

        System.out.println(dto);
        // Output: John Doe <123 Main St, Auckland>
    }
}

Multiple Mapper and Custom Conversion Composition

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
import java.time.format.DateTimeFormatter;
import java.time.LocalDate;

public class DateUtils {
    public static String toString(LocalDate date) {
        if (date == null) {
            return null;
        }
        return date.format(DateTimeFormatter.ISO_LOCAL_DATE);
    }
}

public class Event {
    private String title;
    private LocalDate date;
    private Address address;

    public Event(String title, LocalDate date, Address address) {
        this.title = title;
        this.date = date;
        this.address = address;
    }

    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public LocalDate getDate() { return date; }
    public void setDate(LocalDate date) { this.date = date; }
    public Address getAddress() { return address; }
    public void setAddress(Address address) { this.address = address; }
}

public class EventDto {
    private String title;
    private String dateString;
    private AddressDto address;

    public EventDto() {}

    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public String getDateString() { return dateString; }
    public void setDateString(String dateString) { this.dateString = dateString; }
    public AddressDto getAddress() { return address; }
    public void setAddress(AddressDto address) { this.address = address; }

    @Override
    public String toString() {
        return title + " on " + dateString + " at " + address;
    }
}

@Mapper(uses = {AddressMapper.class, DateUtils.class})
public interface EventMapper {
    EventMapper INSTANCE = Mappers.getMapper(EventMapper.class);

    @Mapping(source = "date", target = "dateString")
    EventDto toDto(Event event);
}

// Runnable test
public class CustomComposition {
    public static void main(String[] args) {
        Event event = new Event("Tech Conference",
            LocalDate.of(2026, 3, 15),
            new Address("Convention Centre", "Auckland"));

        EventDto dto = EventMapper.INSTANCE.toDto(event);

        System.out.println(dto);
        // Output: Tech Conference on 2026-03-15 at Convention Centre, Auckland
    }
}

Advanced: Reusable Annotation Composition

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.Qualifier;
import org.mapstruct.factory.Mappers;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// Custom utility for age grouping
public class AgeUtils {
    @Named("ageToGroup")
    public static String ageToGroup(int age) {
        if (age < 18) return "Minor";
        if (age < 65) return "Adult";
        return "Senior";
    }
}

// 1. Custom composed annotation (bundles multiple @Mapping rules)
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.CLASS)
@Mapping(target = "fullName", expression = "java(user.getFirstName() + \" \" + user.getLastName())")
@Mapping(target = "ageGroup", source = "age", qualifiedByName = "ageToGroup")
public @interface UserEnrichment {}

// 2. Mapper using the composed annotation
@Mapper(uses = {AddressMapper.class, AgeUtils.class})
public interface UserEnrichedMapper {
    UserEnrichedMapper INSTANCE = Mappers.getMapper(UserEnrichedMapper.class);

    @UserEnrichment  // <- Single annotation replaces 3+ @Mapping entries!
    UserEnrichedDto toEnrichedDto(UserEnriched user);
}

// Runnable test
public class AnnotationComposition {
    public static void main(String[] args) {
        UserEnriched user = new UserEnriched(
            "John", "Doe", 42,
            new Address("123 Main St", "Auckland")
        );

        UserEnrichedDto dto = UserEnrichedMapper.INSTANCE.toEnrichedDto(user);

        System.out.println(dto);
        // Output: John Doe (Adult) <123 Main St, Auckland>
    }
}

Global Configuration

See the Shared MapperConfig in MapStruct: Pure Java

Key Benefits Summary

Feature Pure Java Benefit
uses = {...} Automatic instantiation, no DI container
Mappers.getMapper() Single point of access per mapper
@MapperConfig Consistent behavior without framework
Multiple uses Type-based delegation selection
@UserEnrichment Zero-boilerplate reusable mapping rules
Custom utils Seamless integration with plain classes

Each example compiles and runs independently after MapStruct annotation processing, demonstrating self-contained mapper hierarchies without external dependencies.