1. Configuration in Microservice
MapStruct is configured in the parent pom.xml to standardize object mapping across all backend services.
dependencyManagement— Defines the version centrally. Sub-modules can choose to use MapStruct by adding the dependency (without specifying version).annotationProcessorPathsin build plugins — Runs annotation processors during compilation to generate mapper implementation classes (e.g.,MemberMapperImpl.java). This applies to all sub-modules automatically.
Parent pom.xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<annotationProcessorPaths>
<!-- Lombok MUST come FIRST -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</path>
<!-- MapStruct SECOND -->
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
</path>
<!-- Binding (recommended if using @Builder) -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Sub-Module pom.xml
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
Why Parent vs Sub-Module Split?
| Section | Purpose | Where |
|---|---|---|
<dependencyManagement> |
Defines version only, does NOT add dependency | Parent only |
<dependencies> |
Actually adds to classpath | Sub-module |
<annotationProcessorPaths> |
Runs processors during compilation | Parent only |
Benefits:
- Change version in one place → all modules updated
- Consistent versions across all microservices
- Centralized annotation processor configuration
2. Understanding annotationProcessorPaths
What Each Part Does
| Configuration | What It Provides |
|---|---|
<dependency>mapstruct</dependency> |
@Mapper, @Mapping annotations (compile-time API) |
<annotationProcessorPaths>mapstruct-processor |
Generates MemberMapperImpl.java |
<annotationProcessorPaths>lombok |
Generates getters/setters from @Data |
<annotationProcessorPaths>lombok-mapstruct-binding |
Helps MapStruct understand @Builder |
Without mapstruct-processor in Build Plugin
If you only have mapstruct in dependencies but NOT in annotationProcessorPaths:
target/generated-sources/annotations/
└── (empty) ✗ No MemberMapperImpl.java generated!
Result at runtime:
No bean of type 'MemberMapper' found
Why Lombok is in annotationProcessorPaths
Lombok is not required by MapStruct itself. It’s there because your DTOs use @Data:
@Data // Lombok generates getEmail(), setEmail()
public class MemberDTO {
private String email; // No manual getter/setter
}
Both run at compile time. If Lombok doesn’t run, MapStruct can’t find getEmail().
Why Order Matters
Compile Time Order:
1. Lombok runs FIRST → generates getEmail(), setEmail()
2. MapStruct runs SECOND → sees getEmail(), generates mapper code
Wrong order:
1. MapStruct runs FIRST → "Where is getEmail()?" → FAILS
2. Lombok runs SECOND → Too late!
3. lombok-mapstruct-binding Explained
When You Need It
| Your DTO Uses | Need Binding? |
|---|---|
@Data only |
No |
@Data + @Builder |
Yes |
@Data + @SuperBuilder |
Yes |
@Data + @AllArgsConstructor + @NoArgsConstructor |
No |
What It Does
Without binding, MapStruct doesn’t know how to use Lombok’s @Builder:
@Data
@Builder
public class MemberDTO {
private String email;
}
With binding, MapStruct generates:
// Generated MemberMapperImpl.java
public MemberDTO memberToMemberDTO(Member member) {
return MemberDTO.builder()
.email(member.getEmail())
.build();
}
Why Your Code Works Without It
If your DTO has both @Builder AND @NoArgsConstructor:
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor // ← MapStruct uses this instead
public class MemberDTO { }
MapStruct uses the no-arg constructor + setters, so binding isn’t strictly required. But adding it is safer for future changes.
4. Private Variable Declaration on DTO and Entity
All fields in DTOs and entities should be declared private to enforce encapsulation and maintain JavaBean standards.
Use Lombok to generate getters/setters:
@Data
public class MemberDTO {
private UUID id;
private String email;
// ... other fields
}
@Entity
@Data
public class Member {
@Id
private UUID id;
private String email;
// ... other fields
}
5. MapStruct Potential Bug to Access Private Member Value
Problem Description
MapStruct may silently fail or generate incomplete mapping code if:
- Fields are private
- No getters/setters are found
- Annotation processing is disabled or misconfigured
- Lombok runs AFTER MapStruct (wrong order)
How to Debug Mapping Issues
Step 1: Open the Autogenerated Mapper Implementation
Navigate to:
/target/generated-sources/annotations/...
Visually confirm whether the generated class uses:
member.setEmail(memberDTO.getEmail()); // Correct
Or fails with direct field access like:
member.email = memberDTO.email; // WRONG - will fail for private fields
Step 2: Manually Add Getters/Setters if Needed
If @Data or @Getter/@Setter does not work due to annotation processing issues, define them manually in both MemberDTO and Member.
Step 3: Rebuild Project
./mvnw clean compile
Or IntelliJ’s Build > Rebuild Project with annotation processing enabled.
Step 4: Verify Generated Code Exists
ls target/generated-sources/annotations/com/your/package/mapper/
# Should see: MemberMapperImpl.java
Step 5: Write Proper Test Code
@Test
void testMemberDTOtoMember() {
MemberDTO dto = MemberDTO.builder().email("test@example.com").build();
Member member = memberMapper.memberDTOtoMember(dto);
assertEquals("test@example.com", member.getEmail());
}
This ensures that future source code changes do not silently break mapping logic.
6. Recommended Final Configuration
<annotationProcessorPaths>
<!-- 1. Lombok FIRST - generates getters/setters -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<!-- 2. MapStruct SECOND - generates mapper implementations -->
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<!-- 3. Binding LAST - bridges Lombok @Builder with MapStruct -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>

