ReviseAlgo Logo

Building REST APIs

DTOs and MapStruct — Separating API Contracts from Domain Models

Designing data transfer structures and automating conversion mapping compilation.

Last Updated: June 15, 2026 15 min read

Introduction

In well-architected applications, database entities should never be exposed directly to client consumers. Instead, we use Data Transfer Objects (DTOs) to encapsulate the exact contract required by the API. To automate the conversion between domain models and DTOs without manual boilerplate, Spring developers rely on MapStruct, a high-performance, compile-time mapping framework.

Why It Matters

Exposing database entities creates severe issues: it leaks internal structures (like password hashes or audit fields), causes runtime lazy loading exceptions when serialization triggers database queries, and couples DB schema updates directly to the client contract. MapStruct solves this efficiently by generating plain Java mapping code at compile-time, avoiding the high CPU and reflection costs of runtime mappers like ModelMapper.

Real-World Analogy

Think of an internal payroll spreadsheet. The accountant's master file (Domain Entity) contains columns for name, address, tax status, internal review score, and bank details.

When sending the employee a pay stub (DTO), we do not hand over the entire spreadsheet. An automated compiler machine (MapStruct) reads the sheet and copies only the allowed fields (e.g. name and net pay) to a separate certificate. This maintains security boundaries.

How It Works

MapStruct integrates into the build phase (Maven/Gradle annotation processors). When compilation starts:

  1. The compiler detects interfaces annotated with @Mapper.
  2. It analyzes properties in source and target classes, matching fields with identical names and compatible types automatically.
  3. It generates a concrete implementation class (e.g., UserMapperImpl.class) under target generated sources. At runtime, mapping is simple, standard getter/setter execution, running at native Java speed with zero reflection.

Practical Example

Let's map a User database entity to a clean UserResponseDto and see how to use the mapper:

Now, define the MapStruct interface. Using componentModel = "spring" registers the generated implementation as a Spring Bean:

Inject the mapper into your controller using standard constructor injection:

Quick Quiz

Q1: Why is MapStruct faster than ModelMapper at runtime?

A) It compiles to native assembly code

B) It runs mappings inside database procedures

C) It generates standard Java classes with direct getters/setters during compilation, avoiding runtime Java Reflection

D) It runs mapping asynchronous processes in background threads

Answer: C — MapStruct works during the compilation phase, generating plain Java bytecode, which results in near-zero CPU mapping overhead.

Q2: How do you register a MapStruct mapper as an injectable Spring bean?

A) Annotate the interface with @Component

B) Configure the mapper annotation as @Mapper(componentModel = "spring")

C) Build a manual mapping config factory bean class

D) MapStruct is registered as a Spring bean automatically by default

Answer: B — Specifying componentModel = "spring" instructs MapStruct to append Spring's @Component annotation to the generated implementation class.

Scenario-Based Challenge

Production Scenario:

Your database entity stores a user's name in two properties: firstName and lastName. However, your target DTO requires a single property named fullName. How do you configure MapStruct to handle this mapping?

View Solution

You can specify custom mapping behavior using a default method in the mapper interface or using MapStruct annotations:


Alternatively, you can write a helper method inside the interface or use @AfterMapping to build the full name string manually.

Interview Questions

1. How can you handle unmapped properties warnings during compilation in MapStruct?

By default, MapStruct alerts unmapped properties as warnings. In production, configure unmapped target policies inside the mapper annotation: @Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR). This turns unmapped properties into compilation failures, prompting developers to address them immediately.

2. What is the benefit of using @InheritInverseConfiguration?

It allows MapStruct to reuse the configuration defined on a forward mapping (e.g. source to target) for the reverse mapping (e.g. target to source) without redeclaring every @Mapping rule in reverse.

Production Considerations

Keep mappers stateless. Do not invoke external database queries inside MapStruct mappings, as this will lead to unexpected N+1 query patterns or performance bottlenecks. Perform mapping operations outside database transaction bounds when possible, freeing up connection pools faster.