Objects that do not know they are stored
A Socratic walk-through of objects that do not know they are stored — reasoned out one step at a time, not lectured.
The question we started with
THE QUESTION #Why keep the code that saves a thing away from the thing itself?
An Invoice object knows how to total its lines, apply a discount, and refuse to be issued twice. It seems entirely natural to give it one more skill: invoice.save(). The thing knows most about itself, so surely it should know how to write itself down.
Plenty of successful frameworks agree — that is exactly what Active Record does, and Rails built a decade of productivity on it. Yet a substantial tradition insists on the opposite: the domain object must not know that a database exists at all. Both cannot be simply right, so it is worth working out what the disagreement is actually about.
Reasoning it through
REASONING #Ask first what save() would have to know. It needs a connection or session, a table or collection name, a mapping from fields to columns, a policy on when to write, and a way to report a conflict. None of that is an invoicing rule. So the moment we add the method, the class has two subjects: the commercial rules of invoicing, and the mechanics of one particular storage technology.
Is that a problem in itself? Not obviously. Classes hold several concerns all the time. The sharper question is: do the two subjects change for the same reasons? Invoicing rules change when the business changes — a new tax rule, a new discount type. Storage code changes when the schema is migrated, the driver is upgraded, an index is added, or the team moves to a different store. Those are different clocks, driven by different people, and every commit that touches one now risks the other.
Now push on testing, because that is where the argument becomes concrete rather than aesthetic. If the rule "an invoice cannot be issued twice" lives in a class that also carries a database connection, then testing that rule means having a database — or an elaborate substitute for one. If the rule lives in a class that knows nothing about storage, the test is a few lines with no infrastructure and no cleanup. And in practice the number of business rules vastly exceeds the number of storage decisions, so this ratio decides how fast the whole test suite runs.
So where does the saving go? Into a separate collaborator, conventionally a repository: an object that presents itself as a collection of domain objects — add, remove, find_by_number — while hiding the fact that the collection is really a table. Fowler catalogued this in Patterns of Enterprise Application Architecture in 2002, alongside the Data Mapper that does the field-to-column translation underneath it; the domain-driven design community later named the resulting property persistence ignorance.
Notice the direction of the dependency, because that is the whole trick. The invoice does not point at the repository. The repository points at the invoice. Storage knows about the domain; the domain does not know about storage. That asymmetry is what lets the domain be reasoned about, and tested, on its own.
But now the honest objection. Does this actually let you swap databases? The usual claim is yes, and the usual outcome is no — teams rarely change store, and when they do the query semantics leak through the interface anyway. I would not defend the pattern on that ground. The dependable gains are narrower and more mundane: business rules that can be read without wading through SQL, tests that run without infrastructure, and a single place to change when a query needs tuning. The portability is a bonus that often fails to arrive.
There is a real cost, too. You now maintain a mapping between object shape and table shape, and the two want to be different — objects have references and inheritance, tables have foreign keys and joins. That friction has a name, the object-relational impedance mismatch, and mappers spend most of their complexity budget on it. For a small application whose objects are essentially rows, Active Record's coupling is not a flaw but an accurate reflection of the design, and the separation buys you layers to maintain and nothing else.
The analogy
THE ANALOGY #Think of a museum piece and its catalogue. The vase does not carry a shelf number glazed into its side; a curator's register records where the vase is kept, how it is packed, and which crate it travels in. Move the collection to a new building and the register is rewritten while the vase is untouched — and, equally, you can study the vase's shape and glaze without consulting the register at all.
a vase is genuinely inert, whereas a domain object must still be retrievable by its own attributes — the repository has to offer find_by_number, so the invoice's own fields shape the queries, and the separation is one of code and dependency direction rather than a real independence of concerns.
Clarifying the model
THE MODEL #Three refinements.
First, persistence ignorance is about dependency, not about ignorance of identity. A domain object still has an identity, still has an invoice number, and may still carry the version field used for optimistic concurrency. What it lacks is any reference to a session, connection, or query language.
Second, the abstraction is honestly leaky in one important place: lazy loading. A mapper that returns a partly-populated object which fetches the rest on access has smuggled a database call into what looked like a plain attribute read — which is how a loop over ten thousand invoices becomes ten thousand queries. The domain object stayed ignorant; the calling code did not, and paid for it.
Third, a repository per aggregate, not per table. The point of the pattern is to hand back a whole consistent object — an invoice with its lines — so that the rules can be enforced on it. A repository for every table reintroduces the joins into the calling code and gives up most of the benefit.
A picture of it
THE PICTURE #How to readRead the arrowheads, not the boxes. Every arrow that touches Invoice points at it — the mapper and the repository know about the invoice, while the invoice names nobody. The interface sits between the service and the concrete implementation so that the only class in the picture aware of a session is the one at the bottom right, and it is the only one that has to change when the schema does.
What became clearer
WHAT CLEARED #Separating the saving from the thing being saved is not tidiness for its own sake; it is a deliberate choice about which way the dependency arrow points. The domain gets to be the part that nothing else constrains, which makes it cheap to test and cheap to reason about — and the price is a mapping layer that is only worth paying when the rules are genuinely richer than the rows.
Where to go next
ONWARD #- Active Record versus Data Mapper, and which application shapes each actually suits.
- The N+1 query problem as the canonical leak in a persistence abstraction.
- How aggregate boundaries decide what a repository is allowed to return.
Key terms
TERMS #| Term | What it means |
|---|---|
| Repository | an object that presents stored domain objects as if they were an in-memory collection, hiding the query mechanics. |
| Data Mapper | a layer that translates between domain objects and database rows while keeping the two independent of each other. |
| Active Record | the alternative pattern in which a class both carries the domain behaviour and knows how to read and write its own row. |
| Persistence ignorance | the property of a domain class having no reference to, or knowledge of, how it is stored. |
| Impedance mismatch | the structural disagreement between the object model and the relational model that any mapping layer must absorb. |
Every term the collection defines is gathered in the glossary.