user@elrise.io:~
2026-07-237 min readphpsymfonydoctrineddddtoarchitecture

DTO → Entity in DDD: why “not passed” ≠ “passed as default”

In DDD, a DTO comes from the Application Layer while an Entity lives in the Domain Layer. A mapping seam is needed between them: update only the fields that actually arrived, without leaking transport semantics into the domain. In practice, this is exactly where “not passed” often turns into “reset to default”.

elriseio/dto-entity-updater is one option for closing that seam. This article is not a bundle API reference: it explains the architectural problem and the solution. The full code, contracts, sentinel table, and tests are on the project page and in the repository.

Where this comes from

In DDD model work, I kept seeing the same piece of code: a command handler receives a DTO and has to move it into a Domain Entity. While the DTO describes a complete object, the mapping looks mechanical. As soon as PATCH or partial updates appear, a field value starts carrying two different meanings: a concrete value and no change.

This is not a comparison of every DTO-to-Entity mapping technique. It presents one architectural option: preserve input semantics at the Application–Domain boundary without making the Entity know about the DTO or spreading checks across handlers.

The boundary that must remain explicit

In a layered DDD architecture, the three layers have different responsibilities:

LayerResponsibility
Application Layeruse-case orchestration, command handlers, DTOs, and preparing input for the domain
Domain Layeraggregates, Entities, value objects, domain services, and business invariants
Infrastructure LayerDoctrine, persistence, messages, and integrations with the outside world

A DTO is an Application Layer object. It knows the shape of external input and may reflect HTTP, a message, or another transport. A Domain Entity must not know about DTOs, PATCH, or which field arrived from JSON.

This is not an aesthetic preference. If an Entity starts accepting a DTO or contains a condition such as “if the DTO field is null, do not touch the setter”, the boundary between business rules and transport has already been broken. The Entity starts serving the input format instead of the domain model. Testing it independently becomes harder because Application Layer knowledge has entered the domain.

The consequence is concrete: the Application Layer must pass the domain a normal set of Entity-change intentions rather than a DTO, while preserving which fields were omitted.

Where partial updates break

Consider a PATCH request for a user profile. The request contains name, email, and phone, but the user changes only phone. The expected behaviour is simple: write the new phone and leave the other two fields untouched.

Naive solutions quickly create ambiguity:

The problem is not a particular syntax for the check. It is lost semantics. The DTO says “the value was not passed”, while the mapper sees only a value of type int or string and has to guess what the caller meant. The result is a call that is type-correct but wrong for the use-case.

The solution: separate semantics for “not passed”

A special marker is needed to mean no change rather than a concrete value. Such a marker is commonly called a sentinel: a predefined value that is distinguishable from valid domain values and has a separate meaning at the mapping seam.

The DTO declares the sentinel as the field default. The Application Layer passes values onward through one consistent path instead of adding a branch for every property. A component at the seam compares the input with the sentinel and decides:

The important point is where the decision lives: at the Application–Domain boundary. The Entity receives normal domain values and does not know that a field was omitted outside. The semantics of a partial update therefore do not spread through Entities, setters, and domain services.

This is not the only way to represent absence. Its advantage here is that it works with typed DTOs and separates no change from a valid null, 0, empty string, or another default value.

One seam, two strictness policies

The same mapping logic is useful for different input sources, but they should not share the same error policy.

This matters more than the name of a particular class. The same sentinel pattern receives two policies depending on where the input came from: external transport should not break a handler because of an extra field, while an error in owned typed code should not disappear silently.

dto-entity-updater implements this model with two executors. The bundle does not change the Entity or manage its lifecycle; it formalises the Application–Domain seam. The implementer choice, signatures, and error handling are documented on the project page.

How the solution fits the architecture

The responsibility flow looks like this:

  HTTP / CLI / Messenger
        │
        ▼
  ── Application Layer ────────────────────────
       Command Handler
            │
            ▼
         DTO  (input from transport)
            │
            ▼
       Entity updater  ◀── mapping seam
            │
            ▼
       Domain Entity  (aggregate root, value objects)
  ── Domain Layer ─────────────────────────────
            │
            ▼
  ── Infrastructure Layer ────────────────────
       Repository / Doctrine ORM / DB

The diagram does not describe the bundle’s internal pipeline. It describes architectural responsibility: the DTO ends at the Application–Domain boundary, while the Entity remains part of the domain and accepts only domain values.

When this layer is justified

A separate updater is not needed for every CRUD project. If a form fully replaces an Entity and every input has the same field semantics, a simple explicit mapper may be clearer.

A specialised layer is justified when:

Without these conditions, a sentinel and a separate updater can become ceremony. The problem must be real: the layer is needed not for another abstraction, but to preserve the meaning of a partial update.

Where to see the implementation

The project page contains installation, API, the complete sentinel set, both executors, examples, tests, and licensing. In dummy-market-agent, a reference Symfony service can be started with Docker to inspect how Application Layer, CQRS, API Platform, and persistence are assembled across real layer boundaries.

Conclusion

DTOs and Domain Entities belong to different layers. If the mapping loses the difference between “not passed” and “passed as default”, the system starts changing data that the use-case never asked it to change. The fix is to preserve that semantics at the Application–Domain boundary without bringing DTO knowledge into the domain.

dto-entity-updater is one implementation of that seam: the sentinel separates no change from an ordinary value, while lenient and strict policies distinguish external input from typed input. It is not a universal mapper for every architecture, but a specialised solution for projects where partial updates and a clean Domain Entity actually matter.


Useful links:

Discussion

Comments (0)

No comments yet.