java 122 lines · 4 tabs

Mapping a JPA Entity to a Response DTO with MapStruct in Spring Boot

Shared by codesnips Jul 2026
4 tabs
package com.shop.orders;

import jakarta.persistence.*;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "orders")
public class Order {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private BigDecimal total;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private OrderStatus status;

    @Column(name = "created_at", nullable = false, updatable = false)
    private Instant createdAt = Instant.now();

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<OrderLine> items = new ArrayList<>();

    protected Order() {
    }

    public Long getId() {
        return id;
    }

    public BigDecimal getTotal() {
        return total;
    }

    public OrderStatus getStatus() {
        return status;
    }

    public Instant getCreatedAt() {
        return createdAt;
    }

    public List<OrderLine> getItems() {
        return items;
    }
}
4 files · java Explain with highlit

This snippet shows the classic layered flow in a Spring Boot service: a persistent JPA entity is never exposed directly over HTTP, but is translated into a purpose-built response DTO by a mapper before leaving the application. Decoupling the wire contract from the database schema is the core idea — it keeps lazy-loaded associations, internal columns, and audit fields from leaking into JSON, and lets the API evolve independently of the table structure.

In Order entity, Order is a standard @Entity with a generated identifier, a BigDecimal total, a status enum, and a @OneToMany collection of OrderLine items marked FetchType.LAZY. Returning this object straight from a controller is a common trap: Jackson would touch the lazy items collection outside a transaction and trigger a LazyInitializationException, and internal fields like createdAt would silently become part of the public contract.

OrderResponse DTO is a Java record, which is a natural fit for an immutable, read-only response. It flattens what the client actually needs — the id, total, a string status, and a small LineItem projection — rather than mirroring the entity. Records give value semantics and a compact constructor for free, so no boilerplate getters are required.

OrderMapper is a MapStruct interface annotated with @Mapper(componentModel = "spring"), which makes it an injectable Spring bean. At compile time MapStruct generates the implementation, so the mapping has no reflection cost at runtime. The @Mapping on status uses an expression to call name() on the enum, and toResponse recursively maps the List<OrderLine> because a matching LineItem mapping method exists. This is why explicit hand-written mappers are avoided: they are error-prone and drift out of sync as fields are added.

OrderController wires it together. getOrder loads the aggregate inside a @Transactional(readOnly = true) boundary so the lazy items are initialized while the session is open, then hands the entity to orderMapper.toResponse and returns the DTO. Throwing EntityNotFoundException lets a @ControllerAdvice translate it to a 404. The trade-off is one extra object graph per request, which is negligible next to the safety of a stable, transaction-safe contract. This pattern is the default choice whenever an API must outlive its schema.

Share this code

Here's the card — post it anywhere.

Mapping a JPA Entity to a Response DTO with MapStruct in Spring Boot — share card
Link copied