skip to content

What are the real costs of adopting a DTO/Assembler layer across every API endpoint, and under what circumstances would a senior engineer decide NOT to introduce one?

level: seniorimportance: should knowfreq 55%

answer

  1. duplication + sync tax
  2. extra debugging hop
  3. mapper itself can bug out
  4. GraphQL/gRPC already solve part of this
  5. cost scales with external-consumer count, not with 'always do it'

basics

~20 s

Every DTO means extra classes and mapping code to write and keep in sync, which slows down small changes and adds files to maintain. For a tiny internal tool with one trusted client and a short lifespan, that overhead may not be worth it.

solid answer

~50 s

The costs are duplication (a second class per shape, plus mapping code that must be kept in sync as the domain model changes), added indirection (one more hop to trace when debugging a field that's missing or wrong), and a small but real chance of the mapping itself becoming a source of bugs (unmapped fields, stale mappings after a rename). These costs are worth paying when there are external, versioned, or numerous consumers of the API, when internal and external shapes genuinely need to differ, or when security requires an explicit allowlist. They're not worth paying for a short-lived internal admin tool with one client and a tiny, stable schema, or in contexts (like GraphQL, or gRPC contracts already generated from a schema) where field-level selection or contract generation already solves the same problem a different way.

go deeper

for a junior

Should be able to name at least one cost (more classes/files to maintain) without needing to weigh it against alternatives.

for a middle

Should name two or more concrete costs (duplication, debugging indirection, mapper bugs) and give one situation where skipping DTOs is reasonable.

for a senior

Should give a clear decision framework (consumer count, contract volatility, deploy coupling) and compare against at least one alternative technology (GraphQL, gRPC, projections) that addresses similar goals differently.

for a principal

Should set organization-level policy — where DTOs are mandatory (public/external, security-sensitive boundaries) versus optional (internal, same-team, lockstep-deployed) — and justify it in terms of blast radius and coordination cost, not dogma.

## The first cost — raw duplication Adopting DTOs everywhere is often presented as an unambiguous best practice, but it has genuine, quantifiable costs that a senior engineer should be able to name specifically rather than treating the pattern as free. The first cost is **raw duplication**: for every domain concept exposed at a boundary, you now maintain at least two classes (the domain/entity representation and the DTO) plus the mapping code connecting them, and often more than two if there are separate request and response shapes, or multiple API versions. Every time a field is added to the domain model, someone has to remember to decide, deliberately, whether it belongs in the DTO too, and then update the mapper. In a fast-moving codebase with many endpoints, this is a meaningful ongoing tax, not a one-time setup cost. ## The second cost — indirection while debugging The second cost is **indirection during debugging and onboarding**. A field that shows up wrong or missing in an API response might be: - wrong in the domain object; - wrong in the mapping code; - or intentionally omitted by the mapper. Tracing that requires stepping through an extra layer that wouldn't exist if the entity were returned directly. For an engineer new to the codebase, 'where does this JSON field actually come from' now has an extra hop to learn, and if the mapping is spread across many small mapper classes without a consistent naming convention, that hop can be more confusing than illuminating. ## The third cost — bugs the mapping layer alone can produce The third cost is that the mapping layer is itself a source of bugs, distinct from bugs in the domain logic or the API contract: - a field silently left unmapped after a rename; - a mapper that copies a mutable collection by reference instead of defensively copying it (letting a caller mutate internal domain state through the 'read-only' DTO); - a stale mapper that still emits a field the domain model deprecated three months ago. These are all failure modes that exist only because the mapping layer exists. Compile-time-checked generated mappers reduce but don't eliminate this risk; they catch missing target fields but not, for example, a field mapped to the wrong source by mistake if both happen to be the same type. ## When skipping DTOs is legitimate Given those costs, the decision to skip DTOs is legitimate in several recognizable situations. - **A small internal tool** with exactly one client, written and maintained by the same team that owns the backend, and expected to live for a short time or evolve in lockstep with its single consumer, gets little benefit from contract stability that no external party depends on — the entity-as-response approach, while not best practice in the abstract, is a reasonable pragmatic choice there, especially prototypes or admin scripts. - **A second case** is when the technology already solves the same problem differently: GraphQL lets a client request exactly the fields it wants from a single schema, which achieves the 'don't over-expose or over-fetch' goal DTOs exist for, but does it via field-level selection at query time rather than via a fixed set of hand-mapped response shapes; hand-rolling DTOs on top of GraphQL for every possible field combination usually just recreates the problem GraphQL was adopted to avoid. Similarly, gRPC/Protobuf contracts are themselves schema-first and code-generated, and the generated message classes already serve much of the DTO's role (a fixed, versioned, explicit contract) without a hand-written translation layer, though many teams still add a thin mapper between the generated protobuf types and their domain model for the same domain-purity reasons discussed elsewhere. - **A third case** is read-heavy, reporting-style endpoints where a lightweight projection (a database query that selects exactly the needed columns into a flat record, sometimes called a query-side DTO or a CQRS read model) already produces a minimal, purpose-built shape directly from the query, making a separate assembler step redundant because there's no rich domain object being downgraded — the projection is the DTO. ## The judgment call The judgment call a senior engineer should be able to make explicitly is: DTOs pay for themselves in proportion to how much the internal model's stability differs from what the external contract needs, and how many parties depend on that contract not moving underneath them. A team that reflexively DTOs every internal microservice-to-microservice call within a single deployable, single-team monorepo, where both ends change together in the same pull request, is often paying real duplication cost for very little of the decoupling benefit, and it's a defensible senior-level call to skip the ceremony there while still insisting on it at the actual external, versioned, security-sensitive boundary — the public API.

  • If a service-to-service call happens entirely within one team's microservice boundary and both sides deploy together, does that change the DTO calculus?
    Yes — when both producer and consumer are owned by the same team and always deploy in lockstep, the 'external contract must stay stable while I refactor internals' argument for DTOs weakens substantially, since a breaking internal-model change and its consumer fix land in the same coordinated release. Many teams still keep a thin DTO at true service boundaries for testability and clarity, but the urgency is lower than for a public, externally-versioned API.
  • How does GraphQL's approach to this problem differ from hand-rolled DTOs?
    GraphQL lets the client specify exactly which fields it wants at query time against one schema, so the server doesn't need to predefine every possible response shape as a distinct DTO class; the schema itself acts as the allowlist and the resolver layer supplies the requested subset. The trade-off is that the server takes on more responsibility for preventing overly expensive or deeply nested client-chosen queries (e.g., via query complexity limits), a concern DTOs don't have because their shape is fixed by the server in advance.
  • Can you get some DTO benefits without a full hand-written mapper class, for read-only endpoints?
    Yes, for read-heavy endpoints a query-level projection (selecting exactly the needed columns via JPA interface projections, a native query, or a CQRS-style read model) can produce a flat, purpose-built result directly from the database, skipping the 'load full entity, then map it down' step entirely. This only covers the read side, though — write/input DTOs for validating and shaping incoming requests still need their own handling.

Adding a DTO layer everywhere is like requiring a certified translator for every conversation in a company, even between two people on the same team who already speak the same language fluently — valuable at the border with outside parties, wasteful overhead in the hallway between two desks.

saying these in an interview costs you the question

  • Presents DTOs as pure upside with no maintenance or duplication cost
  • Insists on a DTO layer even for a tiny, single-client internal tool without weighing the trade-off
  • Doesn't know that GraphQL or generated gRPC contracts address a similar problem differently
  • Can't name a concrete bug class that the mapping layer itself introduces
  • Treats 'always use DTOs' as an inviolable rule rather than a judgment call based on consumer count and volatility

context