Clean Spring Boot APIs: Separating Entities, DTOs, and Mappers
Mixing your database domain models (entities) with your API transport models (DTOs) might seem convenient at first, but it often leads to headaches down the road. When a JPA entity is directly used in a controller or returned in a REST response, you risk exposing internal fields or needing lots of annotations to hide data. This tight coupling can also trigger lazy-loading issues (e.g. LazyInitializationException) if the database session is closed. In short, conflating entities and DTOs makes the code harder to maintain, trickier to test, and can even introduce security vulnerabilities. Let’s explore why separating these concerns is so important.
Entities vs. DTOs vs. Mappers: What’s the Difference?
In a layered Spring Boot application, each of these plays a distinct role. Separating them clearly improves the structure of your code:
By keeping these layers separate, you encapsulate your persistence details in entities, your API contract in DTOs, and the conversion logic in mappers. This separation of concerns makes the application easier to reason about.
Data Flow Through Layers
Data flow through a Spring Boot application’s layers: the Controller receives data as a Request DTO, converts it via a Mapper into an Entity for the database. On the flip side, Entities from the DB are converted to Response DTOs before returning to the client. This ensures the external API model is decoupled from the internal database model.
To make this concrete, consider a simple User domain. We define a JPA entity for the database, and separate DTO classes for the API requests and responses. The entity might have extra fields (like a password hash) that we don’t want to expose via the API. The DTOs will include only the necessary fields for their purpose (e.g. the request might include a raw password, while the response omits it).
User Entity (database model): This is a typical JPA entity with annotations:
User DTOs (API models): We create two DTOs – one for incoming data (e.g. user registration) and one for outgoing data. Notice that the request DTO contains a plain password (to be hashed internally) and no ID, while the response DTO contains an ID and omits the password:
These DTO classes are simple POJOs – no JPA annotations, no business methods. They exist solely to ferry data to/from the outside world. By using them in our controller, we ensure that our User entity’s internals (like passwordHash) never leak out, and the client only sends the data we expect (no extra fields that could be misused).
Mapping: Manual vs. MapStruct
Now, we need to convert between User and these DTOs. We have two main approaches:
Recommended by LinkedIn
1. Manual Mapping: Write conversion code by hand. This gives you full control and is straightforward for simple cases, but can become repetitive if you have many fields.
In the manual mapper above, we explicitly copy each field. This is easy to read and you can insert custom logic (for example, hashing a password before setting it). However, you must remember to update this code if fields change or new fields are added, which can be error-prone in large applications.
2. MapStruct (Automatic Mapping): Use the MapStruct library to generate mapping code at compile time. You define an interface with mapping methods, and MapStruct creates the implementation for you:
Here, the @Mapper annotation tells MapStruct to generate an implementation of this interface. We specify one mapping detail: the password from the DTO should map to passwordHash in the User (since the field names differ). MapStruct will automatically map the other fields with matching names (username, email, id). At runtime, you can inject this UserMapper (as a Spring bean) and call mapper.toEntity(dto) or mapper.toDto(user) – the library handles the heavy lifting of copying properties.
Both approaches achieve the same result: converting between DTO and entity. The choice often depends on your project’s needs.
When to Use Manual Mapping vs. MapStruct
Manual mapping and MapStruct each have their place, and you can even mix them in a project. Some considerations when choosing:
In summary, MapStruct is generally recommended for typical use-cases in mid-to-large applications (for its productivity and compile-time safety), whereas manual mapping is perfectly fine for small projects or special situations.
Benefits of Keeping DTOs and Entities Separate
Finally, what do we gain by all this separation? Quite a lot:
By separating concerns in this way, you get a cleaner, more modular application. Your controllers, services, and repositories each deal in the currency that makes sense for them (DTOs for the web layer, entities for the database layer), and mappers bridge the gap. The result is code that’s easier to reason about, less prone to bugs, and safer to expose to the world. In modern Spring Boot development, this pattern of using DTOs with entity mappers is considered a best practice for writing clean and maintainable APIs.
#Java #SpringBoot #DTO #MapStruct #CleanCode #BackendDevelopment
Senior Mobile Developer | Android Software Engineer | Jetpack Compose | GraphQL | Kotlin | Java | React Native | Swift
1wGreat article!
Senior Software Engineer | Java | Spring Boot | React | Angular | AWS | APIs
1wExcellent Post.
Senior Software Engineer specialized in Java, AWS and microservices architecture
1wSorry for the question here, does this applies to JavaScript world such as NestJs framework?
Senior Software Engineer | Full Stack Developer | C# | .NET | .NET Core | React | Amazon Web Service (AWS)
1wVery informative. Thanks for sharing.
Data Engineer | AWS | Azure | Databricks | Data Lake | Spark | SQL | Python | Qlik Sense | Power BI
1wGreat content!