DAO and DTO Patterns
A Data Access Object hides how data is stored; a Data Transfer Object carries data across a boundary and nothing else. Know what each one hides, where the mapping between them lives, and why an ORM entity should never be your API.
Overview
Two patterns with similar names solve two different problems. A Data Access Object (DAO) is the one class that knows how to read and write a kind of record in a store: it owns the SQL, the connection and the row-to-object code, so nothing else does. A Data Transfer Object (DTO) is a plain bag of fields that carries data across a boundary, such as an HTTP request or a message on a queue, and has no behavior of its own.
They show up together because a typical request crosses both boundaries. JSON arrives and becomes a request DTO; a mapper turns it into a domain entity; a DAO saves the entity; the saved entity is mapped to a response DTO, which goes back out as JSON. Each step has one job, and each object is shaped for the side it faces.
Interviewers use these patterns to test layering. They listen for three things: that you keep SQL in one place, that you never bind a request straight onto a database entity (the mass-assignment bug), and that you can say where the mapping code lives and why.
The Data Access Object
The pattern comes from Sun's Core J2EE Patterns catalog, now hosted by Oracle. Its problem statement is that storage APIs differ by product and by kind of store (relational database, LDAP directory, flat files, a partner system), and code that talks to a store directly becomes tied to it. The fix is to put all access to a data source behind a DAO, so the business code calls a simple interface and never sees the access mechanism. The catalog describes the DAO as acting like an adapter between the component and the data source.
- BusinessObject — OrderService — Needs data; never writes SQL
- DataAccessObject — CustomerDao — Loads and stores; hides the access code
- DataSource — Database — The store: an RDBMS, a file, another system
- TransferObject — Customer — Carries the values between the two
A DAO is shaped like its table: one per table or record type, with create, read, update and delete methods and finders named after columns. The catalog lists the consequences plainly: callers no longer depend on the store, a migration touches only the DAO layer, and the cost is one more layer of classes to write.
DAO or Repository
The Repository Pattern topic compares the two side by side: vocabulary, unit of work, who owns the interface. Both hide SQL; a DAO is shaped like a table and a repository like a piece of the domain. Two consequences that comparison leaves out:
- A column is renamed — The DAO's finder names and callers may change with it — The interface stays; only the implementation changes
- Fits best — CRUD services, reporting, thin domains — Rich domain models with rules
The Data Transfer Object
Martin Fowler's definition in *Patterns of Enterprise Application Architecture* (2003) is an object that carries data between processes in order to reduce the number of method calls. The reason is cost: a remote call is expensive, so instead of asking for a name, then an email, then an address, the client asks once and gets one object with all three. A DTO must be serializable so it can cross the wire, and Fowler puts the copying between DTOs and domain objects in a separate object he calls an assembler.
Fowler also warns against the opposite mistake. In his Local DTO article he calls DTOs inside a single process harmful: a coarse-grained API is harder to use, and the mapping is work that buys nothing when no wire is crossed. He accepts one local case, a screen whose shape differs a lot from the domain model. An HTTP API is a process boundary, so DTOs belong there; passing DTOs between two classes in the same service usually does not pay.
DTO, entity and API schema
Three objects often hold the same fields, which is why people merge them. They change for different reasons, which is why they should stay apart.
- What it is — A plain carrier of fields — An object with identity and rules — The published contract (OpenAPI, JSON Schema, protobuf)
- Has behavior — No, at most validation of its own shape — Yes: withdraw, approve — No, it is a document
- Changes when — The API or message changes — The business rules change — You release a new API version
- Who sees it — The two sides of one boundary — Code inside the service — Every client
- Fields — Only what the call needs — Everything the rules need, including secrets — Exactly what the DTO serializes
The DTO is the code form of the API schema. In many stacks one is generated from the other, so the schema and the DTO change together while the entity is free to change on its own.
Where the mapping lives
Mapping is the code that copies fields between a DTO and an entity. It belongs at the edge of the layer that owns the DTO, in one place per pair, and never inside the entity or the DAO.
- A mapper or assembler class — Next to the controller or service — The default: one place, easy to test
- from_entity on the DTO — The DTO module imports the entity — Fine for small services; the DTO now depends on the domain
- to_dto on the entity — The entity imports API types — Avoid: the domain now depends on the web layer
- A mapping library — Configuration plus generated code — Saves typing; still review that secrets are not copied by name
The direction rule matches the rest of layered design: outer layers know about inner ones, never the reverse. The entity should not know that JSON exists.
Why not send the ORM entity over the API
Returning or accepting the ORM entity directly looks like less code. It creates four problems.
- Mass assignment (overposting) — The framework binds every request field onto the entity, so an extra is_admin=true is saved — The request DTO has only the fields a caller may set
- Leaked fields — The serializer writes every column, including password_hash — The response DTO lists the fields on purpose
- Lazy loading — Serializing touches a lazily loaded relation, which runs extra queries per row or fails after the session closes — The mapper reads exactly the fields it needs inside the transaction
- Coupling — A column rename becomes an API break for every client — The DTO keeps the old name; only the mapper changes
OWASP's Mass Assignment Cheat Sheet names the bug (also called autobinding or object injection, depending on the framework) and lists binding to a DTO as the first defense, with allow-listing fields as the second. It also warns against relying on a block-list alone, because a sensitive field added later stays bindable. Fowler's Lazy Load is the pattern behind the third row: an object that does not hold all its data but knows how to fetch it, which is exactly what a serializer should not trigger.
Immutable DTOs
A DTO is a snapshot. If one layer can change it after another layer has read it, bugs appear far from their cause. Python and Java both make an immutable carrier a one-line declaration.
- Python — @dataclass(frozen=True) — Assigning a field raises FrozenInstanceError (a subclass of AttributeError); with eq, a generated hash
- Java 16 and later — record UserResponse(long id, String email) {} — Private final fields, a canonical constructor, accessors, equals, hashCode, toString
Both are shallow: a frozen dataclass or a record holding a list still lets someone change the list's contents. Use tuples or immutable collections for fields that hold many values. The Python docs also note that frozen instances only emulate immutability, which is enough for a carrier object.
A runnable example
A DAO over an in-memory SQLite database, two frozen DTOs and a mapper. The request DTO checks its keys against its own fields, so a request carrying is_admin is refused before it reaches the entity.
The DAO is the only code with SQL in it, and it uses ? placeholders, never string formatting, which is what the sqlite3 docs require to avoid SQL injection. The service never sees a row; the client never sees the entity.
Versioning DTOs
Clients you do not control keep sending and reading the old shape. The DTO is where you absorb that, because the entity can move on while each version's DTO stays fixed.
- Add an optional response field — Yes — Clients that ignore unknown fields keep working; check that yours do
- Add an optional request field with a default — Yes — Old requests still parse
- Add a required request field — No — Old requests are now rejected
- Rename or remove a field, or change its type — No — Old clients break; add a new version
A strong answer adds the retirement plan: measure who still calls v1, announce a date, and delete the old DTO and its mapper together.
What you will be able to do
- Define a DAO from the Core J2EE catalog and name its four participants.
- Say when a DAO is the better fit than a Repository, and how to put a repository on top of DAOs.
- Give Fowler's definition of a DTO and say why it is wrong inside one process.
- Draw the path from JSON to request DTO, entity, DAO and back, and say where the mapper lives.
- Explain mass assignment, leaked fields and lazy loading as reasons to keep ORM entities off the API, and write an immutable DTO in Python or Java.
Interview questions
- When would you choose a DAO over a Repository?
- Why should an API not accept or return the ORM entity directly?
- Where does the mapping between DTOs and entities live, and who calls it?
- Should DTOs be immutable? Show why.
- How do you change a response DTO without breaking existing clients?
Sign up free to read the full topic
More Low-Level Design topics
Start free on Cariara · Coding problems · System design studio