Repositories & When to Use DDD
The tactical supporting cast - repositories, domain services, factories - and a clear-eyed verdict on when DDD's investment pays off versus when it's overkill.
On this page
Two things left to make DDD-lite usable: the tactical supporting cast - repositories, domain services, and factories, the objects that surround your aggregates - and the honest question every senior asks before adopting a methodology: is this worth it here? DDD is an investment, and like every pattern in this path, it pays off against real complexity and is dead weight against simple CRUD.
Repositories: collection-like access to aggregates
A repository provides the illusion of an in-memory collection of aggregates, hiding how they're actually
stored. Crucially, there's one repository per aggregate root - you don't get a repository for an internal
entity like Posting; you load the whole Account aggregate through AccountRepository:
interface AccountRepository { // an abstraction owned by the domain (Dependency Inversion!)
Optional<Account> findById(AccountId id);
void save(Account account); // persists the whole aggregate
List<Account> findOverdrawn(); // domain-meaningful queries
}The interface lives in the domain and speaks the ubiquitous language (findOverdrawn, not selectWhereBalanceLt0);
the implementation lives in the infrastructure layer (JPA, JDBC). This is Dependency Inversion at the
architectural scale - and exactly the seam hexagonal architecture formalizes next module. Spring Data's
Repository interfaces are this pattern productized.
Domain services and factories
Not every behavior belongs on an entity or value object. Two supporting types catch the rest:
- Domain service - a stateless operation that doesn't naturally belong to a single aggregate, usually because
it coordinates several. "Transfer money from account A to account B" touches two
Accountaggregates, so it lives in aTransferService, expressed in domain terms - not shoehorned into oneAccount. (Don't confuse a domain service with an application/Spring@Service; a domain service holds real business logic, not just orchestration.) - Factory - encapsulates complex aggregate creation when a constructor isn't enough (enforcing invariants across a cluster of objects at birth). You met factories as a pattern; in DDD they specifically guard the creation of well-formed aggregates.
A tell that logic is homeless: if it doesn't fit on an aggregate (it spans several) and isn't creation, it's
probably a domain service. Resist dumping it into a bloated "manager" - that's how you get back to
LedgerManager.
When DDD is worth it - and when it isn't
Here is the senior verdict. DDD's ceremony - aggregates, value objects, contexts, ubiquitous language - is an investment that pays back when the domain is complex:
Use DDD when:
- The business logic is rich and central - intricate rules, many edge cases, real domain complexity (banking, insurance, logistics, healthcare).
- The domain is your competitive advantage and will be worked on for years.
- You have access to domain experts to build a ubiquitous language with.
Skip (most of) DDD when:
- It's essentially CRUD - forms over data with thin logic. A
ProductController→ProductService→ProductRepositoryover a few fields needs no aggregates or contexts; the ceremony is pure overhead. - The app is small, short-lived, or a prototype.
- The complexity is technical, not domain (a high-throughput pipeline's hard part is performance, not business rules).
Even then, DDD-lite pays off broadly: value objects instead of primitives, a ubiquitous language, and respecting aggregate boundaries improve almost any codebase cheaply. You don't have to adopt the whole methodology to steal its best ideas - which is exactly the spirit of this module.
The biggest failure mode: DDD on a CRUD app
Applying full tactical DDD - aggregate roots, repositories, domain events, an anticorruption layer - to what is really a to-do list creates enormous friction for zero benefit: layers of indirection over logic that's just 'save this form.' Match the tool to the problem. The presence of complex, contested business rules that experts argue about is the signal DDD is warranted; their absence is the signal to keep it simple.
Full DDD is hiring a general contractor with architects, permits, and structural engineers - exactly right for building a house with complex, code-governed, load-bearing requirements, and reckless to skip. But calling that whole apparatus to hang a single shelf is absurd: you'd spend a week on paperwork to drive two screws. Most CRUD apps are shelves. The skill is diagnosing which project you're on - and even for the shelf, you still use a level and stud-finder (value objects, ubiquitous language), just not the full firm.
Evaluate three projects. (1) A core underwriting engine for an insurance company - hundreds of rules, regulatory edge cases, worked on by a large team for years, with actuaries on hand. (2) An internal admin tool to edit a table of feature flags. (3) A high-frequency market-data ingest pipeline where the challenge is processing a million messages/second with low latency. For each, how much DDD (full, lite, or none) and why?
When is a full Domain-Driven Design approach most worth its investment?
Key takeaways
- A repository gives collection-like access to aggregates - one per aggregate root - with a domain-owned interface and an infrastructure implementation (Dependency Inversion).
- Domain services hold stateless logic that spans multiple aggregates (a money transfer); factories guard complex aggregate creation.
- Keep homeless logic out of bloated 'managers' - a cross-aggregate operation is a domain service, not another god class.
- Full DDD is worth it for rich, central, long-lived domain complexity with domain-expert access; it's overkill for CRUD, prototypes, or purely technical challenges.
- DDD-lite - value objects over primitives, a ubiquitous language, respecting aggregate boundaries - improves almost any codebase cheaply, even without the full methodology.