1. Introduction
In Java applications, data can flow between multiple layers such as entities, DTOs, and domain models. MapStruct handles such conversion processes by automatically generating mapping implementations at compile time, ensuring consistent mappings and reducing boilerplate code. However, this can become more complex when dealing with abstract classes, as they cannot be instantiated directly. To address this challenge, MapStruct provides flexible mechanisms that define how to create instances of abstract targets during mapping.
In this tutorial, we explore practical techniques for mapping abstract classes in MapStruct, including the use of external factory classes and inline factory methods. Furthermore, each method offers a distinct way to manage instance creation and field mapping, enabling developers to design clean and maintainable mapping layers.
2. Setup
Before moving on to the mapping strategies, it’s essential to prepare the project using MapStruct and the required model classes. This setup establishes a uniform structure that each mapping approach relies on throughout the text.
2.1. Adding the Maven Dependency
Let’s start by adding the MapStruct dependency to the project’s pom.xml:
<dependency>
   <groupId>org.mapstruct</groupId>
   <artifactId>mapstruct</artifactId>
   <version>1.6.3</version>
</dependency>
Once added, it enables automatic code generation for mapping implementations.
2.2. Creating the Abstract Class
The Person class defines the core attributes shared across all related models. It serves as the base class for any entity that represents a person in the system:
public abstract class Person {
   private String name;
   private int age;
   // Constructors, getters and setters
}
This class cannot be instantiated directly but provides a reusable structure for subclasses like Employee and Customer.
2.3. Creating Concrete Subclasses
The Employee class extends Person and introduces additional attributes:
public class Employee extends Person {
    private String department;
    private double salary;
   // Constructors, getters and setters
}
Similarly, the Customer class extends Person and includes customer-specific details:
public class Customer extends Person {
   private String customerId;
   private String tier;
   // Constructors, getters and setters
}
This concrete implementation enables MapStruct to instantiate and map it easily.
2.4. Creating the Data Transfer Object
Lastly, the PersonDTO class serves as a simple data carrier object for transferring information between layers.
public class PersonDTO {
   private String name;
   private int age;
 // Employee fields
   private String department;
   private double salary;
 // Customer fields Â
   private String customerId;
   private String tier;
 // Discriminator field
  private String type;
// Constructors, getters and settersÂ
}
This DTO functions as the target type in MapStruct mappings, enabling seamless data transformation from abstract and concrete classes.
3. Using an External Factory Class for Mapping
Alternatively, we can shift the responsibility for creating subclass instances to a dedicated factory class, simplifying MapStruct mappings.
The method follows a two-step process:
- create a factory class to define how different subclass instances are constructed
- integrate the factory class within MapStruct during mapping to automatically obtain the correct subclass instance
Thus, the approach centralizes creation logic, enhances reusability, and separates mapping from object construction.
3.1. Create the Factory Class
The factory class is responsible for providing concrete instances of abstract types, such as Person:
public class PersonFactory {
   public Employee createEmployee() {
       return new Employee();
   }
   public Customer createCustomer() {
       return new Customer();
   }
}
Here, PersonFactory defines how Employee or Customer objects are created. MapStruct uses these methods to instantiate the target objects during mapping.
3.2. Configuring the Mapper
Now, the next step is to configure the mapper interface so that MapStruct knows it should use the external factory during the mapping process:
@Mapper(componentModel = "default", uses = PersonFactory.class)Â
public interface PersonMapperFactory {Â
Â
   PersonMapperFactory INSTANCE = Mappers.getMapper(PersonMapperFactory.class);Â
Â
   @Mapping(target = "department", source = "department")Â
   @Mapping(target = "salary", source = "salary")Â
   Employee toEmployee(PersonDTO dto);Â
Â
   @Mapping(target = "customerId", source = "customerId")Â
   @Mapping(target = "tier", source = "tier")Â
   Customer toCustomer(PersonDTO dto);Â
}
Here, the @Mapper(uses = PersonFactory.class) annotation instructs MapStruct to use the external factory to create the appropriate concrete subclass whenever it needs to map an abstract type, ensuring correct instantiation during mapping.
4. Using an Inline @ObjectFactory Method for Mapping
Lastly, we can define the object creation logic directly inside the mapper through an inline @ObjectFactory method, enabling MapStruct to handle both instantiation and mapping in one place.
The method can be implemented fairly simply:
@Mapper
public interface PersonMapperInlineFactory {
   PersonMapperInlineFactory INSTANCE = Mappers.getMapper(PersonMapperInlineFactory.class);
   Person toPerson(PersonDTO dto);
   @ObjectFactory
  default Person createPerson(PersonDTO dto) {
    if ("employee".equalsIgnoreCase(dto.getType())) return new Employee();
    else if ("customer".equalsIgnoreCase(dto.getType())) return new Customer();
    throw new IllegalArgumentException("Unknown type: " + dto.getType());
  }
}
The @ObjectFactory method used here provides a way for MapStruct to instantiate the concrete type. Hence, it’s a great way to implement mapper-specific instantiation rules, keeping both mapping and creation logic centralized within a single mapper.
5. Conclusion
In this article, we looked at ways to map abstract classes in MapStruct. Specifically, we saw that it requires handling instantiation carefully, as abstract types cannot be created directly. In particular, we demonstrated two effective strategies to address this challenge.
The external factory class approach separates object creation from mapping logic, providing a reusable and centralized way to create concrete subclasses. This is useful when multiple mappers share instantiation rules or when object creation involves additional logic. The inline @ObjectFactory method keeps the instantiation logic inside the mapper, offering a concise solution for mapper-specific needs.
Both strategies ensure precise mapping from DTOs to concrete subclasses while keeping mapping layers clean and maintainable.
As always, all the source code is available over on GitHub.