1. What is Mapstruct?
"MapStruct is a code generator that greatly simplifies the implementation of mappings between Java bean types based on a convention over configuration approach."
Mapstruct is a prominent key to many Java developers who struggles with mapping for objects like DTO.
It simplifies mapper classes A LOT. Auto-generates boiler-plate mapping codes at the build path, so developers only has to focus on the abstract class or interface, which describes the mapper.
It significantly enhances readabilty, efficiency, and reduces mistakes and bugs in the unnecessary boiler-plate codes.
2. Why I used it?
Well, to be honest, "they" were using.😂 The team was already using the Mastruct for mapping DTOs, entities and etc.
At first, I didn't 100% get why my team was using it. but when collided with the moment when I should have mapped internal data structure to the DTO which heads to the outside world, damn, it was a THING!
All I had to do was connecting variable-to-variable only with the annotation, and Mapstruct auto-generates the boilerplate codes and suprisingly reduces a developer's "chore".
Let's see why is that with the example.
3. Example.
Let's say we have to map from UserEntity to UserDTO.
You can see 4 important points in my example.
For the variable setting...
In case when variable names are different, you can use the @mapping annotation.
If not, you don't even have to write anything about the variables.
When you want to use some Java code when mapping, you have 2 options.
- Option1 : If you want use the function, you can use the qualifiedByName
- Option2 : For simple cases (i.e. LocalDate.now()), you can use the expression.
UserEntity.java
public class UserEntity {
private Long id;
private String user_name; // Will be mapped to username
private String email_address; // Will be mapped to email
private String firstName; // Used for fullName
private String lastName; // Used for fullName
private String birthYear; // Used to calculate age
private LocalDateTime createdAt; // For testing expression: overwrite with now()
// Getters and Setters
}
UserDto.java
public class UserDto {
private Long id;
private String username;
private String email;
private String fullName; // Composed of firstName + lastName
private int age; // Calculated from birthYear
private LocalDateTime syncedAt; // Will be set to current time via expression
// Getters and Setters
}
UserMapper.java
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.factory.Mappers;
@Mapper(componentModel = "spring") // Enable Spring DI
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
@Mapping(source = "user_name", target = "username")
@Mapping(source = "email_address", target = "email")
// Option 1: using qualifiedByName
@Mapping(source = ".", target = "fullName", qualifiedByName = "concatName")
@Mapping(source = "birthYear", target = "age", qualifiedByName = "calculateAge")
// Option 2: using expression - overwrite field with now()
@Mapping(expression = "java(java.time.LocalDateTime.now())", target = "syncedAt")
UserDto toDto(UserEntity entity);
@Mapping(source = "username", target = "user_name")
@Mapping(source = "email", target = "email_address")
UserEntity toEntity(UserDto dto);
@Named("concatName")
static String concatName(UserEntity entity) {
// Concatenate first and last name
return entity.getFirstName() + " " + entity.getLastName();
}
@Named("calculateAge")
static int calculateAge(String birthYear) {
// Simple age calculation (as of 2025)
return 2025 - Integer.parseInt(birthYear);
}
}
UserService.java
- Springboot Example
import org.springframework.stereotype.Service;
@Service
public class UserService {
private final UserMapper userMapper;
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
public void example() {
UserEntity entity = new UserEntity();
entity.setId(1L);
entity.setUser_name("johnd");
entity.setEmail_address("[email protected]");
entity.setFirstName("John");
entity.setLastName("Doe");
entity.setBirthYear("1990");
UserDto dto = userMapper.toDto(entity);
System.out.println(dto.getUsername()); // "johnd"
System.out.println(dto.getEmail()); // "[email protected]"
System.out.println(dto.getFullName()); // "John Doe"
System.out.println(dto.getAge()); // 35
System.out.println(dto.getSyncedAt()); // Current timestamp (now)
}
}
4. My personal thoughts on Mapstruct
✅ Pros – Surprisingly powerful for mapping
MapStruct handles object mapping incredibly well, especially when dealing with DTOs and entities.
Once set up, it removes a huge amount of boilerplate, making your code cleaner and easier to maintain.
It supports custom mappings, type conversions, field renaming, nested objects, lists, enums, and more — all at compile-time, which means zero runtime cost.
The fact that it catches mapping errors during the build phase is a big plus for stability and safety.
In short: If your project involves a lot of object-to-object transformation, MapStruct is almost unbeatable.
⚠️ Cons – A bit too much for simple tasks
Sometimes it feels like using a sledgehammer to crack a nut.
For very simple mappings, especially in small or one-off components, MapStruct may be overkill.
It also requires an annotation processor setup, so your build (especially Gradle) needs to be correctly configured — which may not always be worth it for tiny utilities.
In short: It shines when mapping is complex or frequent, but for quick-and-dirty jobs, manual mapping might actually be faster.
Top comments (0)