skip to content

What does 'persistence ignorance' mean for a domain model accessed through repositories, and what specific repository design choices support it or quietly undermine it?

level: middleimportance: must knowfreq 74%

answer

  1. no ORM annotations on domain classes
  2. no setters/no-arg ctors just for hydration
  3. mapping lives in repo impl, not model
  4. anemic model = persistence leaking in
  5. reconstruction path != public API

basics

~20 s

Persistence ignorance means your core business objects (like an Order) don't know or care how they're saved - no database annotations, no SQL, no ORM-specific base classes in the domain code itself. The repository is where that knowledge lives instead, kept out of the model.

solid answer

~50 s

Persistence ignorance is the property that domain model classes have zero dependency on persistence technology: no ORM annotations, no base classes required by a framework, no awareness of transactions, connections, or table structure. Repositories are the mechanism that make this possible - they own the mapping between the domain model's shape and the storage shape, so the domain model itself never has to compromise its design for persistence's sake. It's supported by returning real domain aggregates (not framework proxies) from repository methods, by keeping mapping code entirely in the infrastructure-layer implementation, and by not letting ORM-driven concerns (lazy-loading, dirty-checking, identity maps) dictate how the domain model's classes are shaped. It's undermined the moment domain classes need a no-arg constructor 'for the ORM', public setters that exist purely for hydration, or inheritance from a framework base entity class.

go deeper

for a junior

Should be able to say, in plain terms, that domain objects shouldn't need to know about databases, and that the repository is where that knowledge lives instead.

for a middle

Should identify concrete compromises (setters added for hydration, no-arg constructors, leaked ORM proxies) as violations, and explain the resulting risk (invariant bypass, lazy-loading errors).

for a senior

Should be able to design a reconstruction path that keeps a domain aggregate's public API invariant-safe while still allowing a repository implementation to rehydrate it from storage.

for a principal

Should be able to weigh strict persistence ignorance against hand-written-mapping cost across a codebase, set a pragmatic house standard, and identify where the line must not move (invariant enforcement) even when other compromises are accepted.

## What persistence ignorance means **Persistence ignorance** is the design property that a domain model's classes are written purely in terms of the business problem they model, with no knowledge of — and no compromises made for — how instances of those classes get saved to or loaded from durable storage. A persistence-ignorant `Order` class has fields and methods that reflect what an order is and does in the business (line items, a total, a status, methods like `cancel()` or `applyDiscount()`), and nothing that reflects how it's stored: - no `@Entity` or `@Table` annotations, - no required no-argument constructor added solely so a framework can instantiate it via reflection, - no public setters added solely so a mapper can populate private fields, - no base class inherited from an ORM framework. The **repository is the mechanism that makes this possible**: it is the seam at which the domain model's shape and the storage's shape are reconciled, and that reconciliation work — object-relational mapping, key generation, dirty-checking, lazy-loading strategy — lives entirely in the repository's infrastructure-layer implementation, never in the domain class itself. ## Why the property exists Persistence ignorance exists to solve a specific, recurring problem: persistence-technology concerns are notoriously invasive, and left unchecked they reshape domain models around the technology's needs rather than the business's needs. A domain model designed 'ORM-first' tends to end up with - public setters on every field (because the ORM needs to populate them), - collections exposed as mutable lists (because the ORM's lazy-loading proxies need a concrete collection type to wrap), - and behavior-free getter/setter classes because business logic doesn't fit comfortably alongside framework-managed state. The **anemic domain model** anti-pattern is very often downstream of persistence technology leaking into the model. Persistence ignorance, enforced at the repository boundary, keeps the domain model designed around invariants and behavior first, with the mapping problem solved separately and invisibly to domain code. ## The trade-off The trade-off is **real effort moved, not effort eliminated**. Someone still has to write the mapping between a rich domain object (with private fields, no-setter immutable value objects, and business-rule-enforcing constructors) and a persisted representation (rows, columns, foreign keys, or a JSON document). Without an ORM's convention-based mapping (which itself typically demands framework-friendly classes), that mapping is often hand-written: - constructing an `Order` from a result set by calling its real constructor, - reading private state through a documented reconstruction path, - and translating change events back to SQL. This is more code, and more code to maintain, than letting an ORM manage the domain classes directly — the cost of persistence ignorance is paid in repository-implementation complexity, in exchange for a domain model that stays clean, testable without a database, and free to evolve its internal shape without a migration-adjacent conversation about framework constraints. ## Failure modes Failure modes show up as small compromises that compound. 1. **A no-arg constructor** added 'just so the ORM can instantiate it' looks harmless in isolation but means the class can now be constructed in an invalid state anywhere in the codebase, not just inside the repository — the domain model's own invariant-enforcing constructor is no longer the only way to get an instance. 2. **Public setters** added for hydration mean any code, not just the repository, can mutate state that was supposed to be changed only through domain methods that maintain invariants — `order.setStatus(SHIPPED)` bypasses whatever business rule `order.ship()` was supposed to check. 3. **Returning the ORM's own managed/proxied entity type** directly from a repository method (instead of mapping it to a plain domain object) means callers now depend on that entity staying attached to a live persistence session; calling a lazily-loaded getter after the session closes throws a lazy-initialization error at runtime, in code that has no idea it's touching persistence machinery at all. Each of these compromises is individually small, which is exactly why they accumulate — a team rationalizes 'just this one setter' repeatedly until the domain model is, functionally, an ORM entity with some methods bolted on. ## A concrete example A concrete example: a banking `Account` aggregate has a private `balanceMinor: Long` field, no public setter, and a `withdraw(amount: Money)` method that throws if the withdrawal would violate a minimum-balance invariant. **AccountRepository's** infrastructure implementation reconstructs `Account` instances from database rows by calling a package-private or reflection-based reconstruction path specifically reserved for persistence — never the same constructor application code uses to open a new account — and, on save, computes the diff needed to update the balance column rather than exposing a setter that any caller could invoke. The domain module has zero references to the ORM's annotations or interfaces; if the team later swaps the ORM (or drops it for hand-written SQL), only the `AccountRepository` implementation changes — the `Account` class, and every domain/application-service test written against it, is untouched.

  • If a repository returns a real domain aggregate with private fields and no public setters, how does its implementation actually construct that aggregate from a database row?
    Typically through a reconstruction path deliberately separate from the domain model's normal validating constructor - a package-private constructor, a static factory reserved for rehydration, or reflection/mapping-library access to private fields - so that ordinary application code still can't construct or mutate the aggregate except through its invariant-enforcing API. The repository implementation is trusted infrastructure code allowed to use that path; nothing else is.
  • Does using an ORM automatically mean you've lost persistence ignorance?
    Not automatically - some ORMs and mapping approaches (e.g., explicit mapper classes, or ORM configuration done via external mapping files/fluent config rather than in-class annotations) let a domain class stay annotation-free and setter-free while still being persisted. Persistence ignorance is about whether the domain class itself carries framework-specific compromises, not about whether an ORM is used anywhere in the stack.
  • What's a reasonable amount of pragmatism to allow, if strict persistence ignorance is too costly for a given project?
    Some teams deliberately accept a lighter form - annotations on domain classes purely for mapping metadata, but still no public setters and no logic bypassing invariants - trading some purity for less hand-written mapping code. The line worth holding onto even under pragmatism is that state changes still only happen through domain methods that enforce invariants, since that's the property whose loss actually causes bugs.

It's like a chef who cooks a dish without needing to know which delivery truck brought the ingredients or which warehouse they sat in - that logistics knowledge lives entirely with the supply team, so the chef's recipe never has to change because of a change in trucking company.

saying these in an interview costs you the question

  • Adds a public setter to a domain class 'just for the ORM' that bypasses an invariant-enforcing method
  • Domain aggregate has a no-arg constructor with no validation, callable from anywhere
  • Repository methods return the ORM's managed/proxy entity type directly to application code
  • Can't distinguish between a reconstruction path used only by the repository and the model's normal public API
  • Assumes persistence ignorance requires avoiding ORMs entirely rather than avoiding framework leakage into the model

context