forge-docs

0013. Migration from MapStruct to Manual Mappers

Status: Accepted Date: 2026-01-30 Context: MapStruct introduced build cache, CDI, annotation processor, and CI issues; manual mappers are proposed instead.

Context

We implemented MapStruct to handle DTO-to-entity mapping, but it has caused significant build issues:

  1. Build cache problems: Generated code interferes with incremental builds and Maven build cache
  2. CDI injection failures: MapStruct-generated implementations sometimes fail to be recognized as CDI beans, causing UnsatisfiedResolutionException
  3. Annotation processing complexity: Requires careful configuration of annotation processors, which can break with Maven/Gradle version changes
  4. CI/CD unpredictability: Build failures due to stale generated code or annotation processing issues
  5. Debugging difficulty: Generated code is harder to debug and reason about

The build issues have become more costly than the benefits MapStruct provides.

Alternatives Considered

Why Not Other Mapping Libraries?

We evaluated whether other 3rd party mapping libraries would solve our issues, but determined they would likely face similar problems:

Compile-Time Code Generation Libraries (Selma, JMapper)

Conclusion: Other compile-time generators would likely face the same annotation processing and CDI integration issues that plagued MapStruct.

Runtime Reflection-Based Libraries (Dozer, ModelMapper, Orika)

Conclusion: Runtime reflection-based mappers avoid build issues but introduce:

Why Manual Mappers?

Given that:

  1. Compile-time generators (MapStruct, Selma, JMapper) all use annotation processing → same build issues
  2. Runtime reflection mappers (Dozer, ModelMapper, Orika) avoid build issues but introduce performance and type safety problems
  3. Manual mappers provide:
    • Zero build complexity
    • Full type safety
    • Best performance (no reflection, no code generation)
    • Explicit, debuggable code
    • No external dependencies

The decision to use manual mappers was made because:

Decision

We will migrate from MapStruct to manual mapper classes that explicitly handle conversions between DTOs and persistence entities.

Rationale

  1. Build stability: No annotation processing means no generated code, eliminating build cache issues
  2. Explicit and debuggable: Manual code is easier to understand, debug, and maintain
  3. Type safety: Java’s type system provides compile-time safety without code generation
  4. Clean architecture alignment: Explicit mappers fit better with our clean architecture approach
  5. Simple maintenance: No special build configuration or annotation processor setup required
  6. Performance: Manual mappers are as fast as MapStruct (both compile-time, no reflection)

Trade-offs

Pros:

Cons:

Implementation Strategy

Phase 1: Create Reference Implementation

Phase 2: Migrate Document Service Mappers

Phase 3: Cleanup

Mapper Implementation Pattern

All manual mappers will follow this pattern:

@ApplicationScoped
public class CandidateMapper {
    
    public ActorRecord toRecord(RegisterRequestWithAuthIdentity source) {
        // Explicit field mapping
    }
    
    public ActorResponse toActorResponse(ActorRecord source) {
        // Explicit field mapping
    }
}

Principles

  1. One mapper class per aggregate: Keep mappers focused on one domain concept
  2. Explicit null handling: Handle nulls explicitly rather than relying on defaults
  3. Immutable where possible: Prefer creating new objects over mutating
  4. Testable: Each mapper method should be easily unit testable
  5. Documented: Complex mappings should have comments explaining business logic

Migration Checklist

Consequences

Positive

Negative

Neutral

References