Stage 13 - Mapping Strategies and Lazy Loading
Backend code becomes hard to maintain when important behavior is hidden in random controller methods. This article focuses on manual mapping, MapStruct, and lazy loading. The topic looks technical, but the real question is practical: what happens when a real client sends a real request and something is incomplete, too large, forbidden, slow, or inconsistent?
Mapping is where you decide what data the API really needs. If a mapper accidentally walks a lazy collection, one response can trigger dozens of SQL queries or fail after the transaction is closed.

The picture to keep in your head
Think about a small online shop. A user opens the frontend, clicks a button, and the browser calls the backend. The backend receives HTTP data, converts it into Java objects, checks rules, touches the database, and returns JSON. If each step has a clear owner, the system is understandable. If every step is mixed in one method, the first production incident becomes painful.
For this topic the sequence is:
- Repository loads planned graph.
- mapper reads selected fields.
- DTO leaves persistence context.
- The API returns a stable response.
| Part | Responsibility |
|---|---|
| Entity | Persistence object managed by Hibernate. |
| Transaction | Boundary where database changes commit or roll back together. |
| Mapper | Converts persistence shape into API shape. |
| Migration | Versioned database change stored with application code. |
Concrete Spring example
@Mapper(componentModel = "spring")
interface OrderMapper {
@Mapping(target = "customerName", source = "customer.name")
OrderDetailsResponse toDetails(Order order);
}
// The query must fetch customer before this mapper reads customer.name.
The code is intentionally small because the important part is the boundary. The controller should not quietly decide business rules that belong in the service. The service should not depend on servlet objects. The repository should not know which JSON field name the frontend expects. When the boundary is clean, the same behavior can be documented, tested, and changed without touching every layer.
Why it matters
In a demo project, many shortcuts look harmless. Returning all records is fine when there are five rows. Returning an entity is fine until it contains a hidden field. Logging without a request id is fine until several users fail at the same time. Running against a manually created database is fine until staging has a different schema. Production work is mostly about removing these hidden assumptions before they become incidents.
The simplest useful rule is this: make the behavior explicit at the edge, enforce it in the right layer, and keep the public contract stable. If the frontend knows the request format, response format, and error behavior, it can work confidently. If tests exercise the same contract, refactoring becomes safer. If logs and configuration reflect the same design, operations become less dependent on guesswork.
Common mistakes
- Putting the whole use case into a controller method.
- Letting persistence details leak into the API contract.
- Handling the happy path but leaving failure behavior undefined.
- Using a local shortcut that cannot work in staging or production.
Understanding checklist
- I can draw the sequence for this topic from request to response.
- I can explain which layer owns the decision.
- I can name the production problem this topic prevents.
Self-check questions
- What happens if this rule is implemented differently in every endpoint?
- Which part of the behavior should be documented for API clients?
- Which test would prove the rule works, not only that the happy path works?