Repository in Domain-Driven Design: Pattern, Principles, and the Domain Model Checklist

Central repository gateway managing access to domain aggregates

The previous post covered the factory — the building block that decides whether a new domain object is allowed to exist in the first place. Once an object exists, the work is not over. Products rarely interact with a domain object only once. We load it, change it, save it, and revisit it across many user actions. The DDD repository pattern answers a different question than the factory: how do we handle an object across time without breaking the domain rules that define it?

This is the ninth and final post in the domain driven design series. We close the series by defining the repository, walking through its design principles with a subscription example, and ending with a practical checklist that pulls together every building block we have covered — language, entities, relationships, services, modules, aggregates, factories, and repositories. Think of the repository as a librarian. You ask, “Do you have anything on DDD?” and the librarian brings the book. Whether it sits on the third floor, in the basement archive, or arrives through an inter-library loan is not your problem. The librarian hides the physical layout of the library and speaks to you in the domain language of “book topics.” That is the role a repository plays inside a domain model.


What is a Repository: Managing Stored Domain Objects

Repository managing domain objects across their lifecycle

A factory answers, “What does it take for this object to exist?” A repository answers a different question:

How do we handle this object over time without breaking the domain rules?

A repository is the domain building block that manages a domain object from the moment it is created until it is no longer needed. It exposes an interface for requesting and working with domain objects using the domain language, while hiding the complexity of the underlying storage mechanism. The domain does not know, and should not care, whether the object lives in a SQL database, a NoSQL store, a cache, or behind an external API.

By hiding persistence details and exposing intent-driven operations, the repository keeps the client focused on domain concepts instead of storage concerns. This is how domain logic stays stable as storage strategy and infrastructure evolve underneath it. Eric Evans, who introduced this pattern in Domain-Driven Design, advises providing repositories only for aggregate roots that actually need direct access, delegating all object storage and retrieval through them.

The librarian comparison is more than decorative. When you ask a librarian for a book, you describe what you want in the language of the domain — topic, author, title. You do not say, “Walk to shelf 4B, scan barcode 9821, and pull the third volume.” A repository works the same way. The client speaks in domain terms; the repository translates those terms into whatever storage operations are needed.


Why Repositories Matter: The Intersection of Queries, Performance, and Rules

Repository balancing product requirements performance and domain rules

A repository sits at the intersection of three concerns that pull in different directions:

  • Product requirements — what needs to be retrieved
  • Performance constraints — what needs to be fast
  • Domain rules — what needs to stay consistent

Without a repository, these concerns blur together. Domain logic starts depending on query details, performance shortcuts seep into product rules, and a change in storage ripples through the entire system. The repository absorbs that complexity and gives the domain a clean, stable surface.

Consider a food delivery platform’s ordering system. The user (product requirement) only wants to “order chicken.” Rider dispatch and route optimization (performance) happen behind the scenes. Rules like minimum order amount (domain) stay enforced no matter what else changes. Mix these three together and the user ends up reasoning about rider routes, or the minimum-order rule gets bypassed for speed. Repositories prevent that mixing in the same way: each concern stays in the layer that owns it.


Factory vs Repository: Birth vs Lifecycle

Comparison between object creation and ongoing lifecycle management

Factories and repositories are often confused because both “handle objects.” They do very different jobs.

FactoryRepository
Decides whether a new object is allowed to existManages objects that already exist
Enforces rules at the moment of creationPreserves correctness over time
Focuses on birthFocuses on retrieval and persistence

The simplest way to remember the split: a factory deals with the question can this exist? A repository deals with how do we find, update, and store this thing as the system runs?


Repository Design Principles

One repository per aggregate root with controlled domain access

Three principles keep a repository doing its job without quietly absorbing responsibilities that belong elsewhere.

One repository per aggregate root. Each repository manages exactly one aggregate root. Internal objects inside that aggregate are never loaded or queried directly through a separate repository. This preserves the aggregate‘s consistency boundary and prevents external code from bypassing its invariants. If a client could load an internal entity through the back door, every rule the root protects could be silently broken.

Domain-centric interface. Repository methods express business intent, not query mechanics. findActiveSubscriptionForAccount(accountId) is a domain question. selectFromSubscriptionWhereStatusEqualsActive is a SQL phrasing leaking into the domain. Keeping the interface domain-centric protects the domain language and stops database concerns from spreading upward into product code.

No transaction control. The repository does not decide when to commit or how to coordinate writes across multiple aggregates. That orchestration belongs to an application service or a use case handler. A repository that controls transactions ends up making flow-control decisions that should sit higher in the stack, and the boundary between “access” and “orchestration” starts to dissolve.


Example: A Subscription Repository

Repository retrieving and saving subscription aggregates

A Subscription Repository exists to handle subscriptions that already exist. It answers three domain questions:

  • “Which subscription is this?” — lookup by ID
  • “Does this account already have a subscription?” — lookup by account
  • “This subscription has changed. Can we save it?” — persist an updated subscription

It does not decide whether a subscription is allowed to exist, or whether a change is allowed. Those decisions belong to the factory and the aggregate.

Subscription Repository
───────────────────────

┌──────────────────────────────────────────┐
│ Subscription Repository                  │
│------------------------------------------│
│ Domain questions it answers:             │
│ - Find subscription by ID                │
│ - Find the subscription for an account   │
│ - Save an updated subscription           │
└───────────────┬──────────────────────────┘
                │ returns / persists
                ▼
┌──────────────────────────────────────────┐
│ Subscription (Aggregate Root)            │
└──────────────────────────────────────────┘

The repository says nothing about how the subscription is stored or retrieved. Storage technology, query language, and performance tuning are real concerns, but they sit outside the domain surface the repository provides. If the team migrates from PostgreSQL to a managed NoSQL store next quarter, the repository’s interface stays the same — only its implementation changes. That stability is the whole point.


Domain Model Practical Checklist: All Building Blocks in One Place

Integrated map of DDD building blocks and domain boundaries

The checklist below pulls together every building block this DDD series has covered and helps locate where a model is bending. Run through it when a product starts feeling more complex than it should.

1) Language and Concepts

  • Every core term can be explained in a single sentence.
  • Core terms are used consistently across PRDs, tickets, and engineering discussions.
  • When disagreement appears, it is clear whether the team disagrees about behavior or about definition.

2) Entities vs Value Objects

  • For each concept: even when it changes, is it still “the same thing over time” (entity)?
  • Concepts that do not need continuity are modeled as replaceable descriptors (value object).
  • No value object is quietly being treated like an entity — tracked, edited, audited.

3) Relationships

  • Most relationships are unidirectional by default.
  • Large one-to-many relationships include qualifiers (date, role, status) where useful.
  • Direct links that do not affect behavior have been removed.
  • Bidirectional relationships exist only when justified by a real domain rule.

4) Domain Services

  • Business decisions that span multiple concepts live in a domain service, not inside a single entity.
  • Service names express business intent, not technical steps.
  • Domain services are stateless and do not own data.

5) Modules

  • Modules are organized around product concepts, not technical layers.
  • Concepts inside a module tend to change together.
  • Dependencies between modules are limited, intentional, and directional.

6) Aggregates

  • Each aggregate boundary holds the smallest set of objects that must change together to stay valid.
  • Invariants are enforced inside the aggregate boundary.
  • External access is restricted to the aggregate root.
  • Most transactions modify only one aggregate.
  • Coordination between aggregates happens through references and events, not shared state.

7) Multiple Aggregates

  • Different product promises (planning, ownership, money, recording) are separated.
  • Aggregates with different failure modes have independent lifecycles.
  • Long-lived or high-risk concepts (payments, reservations) are not mixed with planning concepts.

8) Factory Checklist

  • The correct way for a core object to come into existence is precisely defined.
  • Creation-time rules — defaults, validation, forbidden combinations — are centralized.
  • Creation either fully succeeds or fully fails.
  • Multiple creation paths do not produce inconsistent objects.

9) Repository Checklist

  • Each aggregate root has its own repository.
  • Internal objects are not loaded or queried directly.
  • Repository methods reflect business questions, not database mechanics.
  • The repository focuses on access and persistence, not transactions or flow control.

10) Quick Diagnostic Signals: When Your Domain Model is Breaking

Warning signals spreading across a fragmented domain model

Some short questions surface model trouble fast. If any of these sound familiar, the issue is rarely “messy code” — it is a missing or misplaced domain boundary.

  • “How is this state even possible?” → An invariant or creation boundary is missing.
  • “The same thing behaves differently depending on where it was created.” → A factory problem.
  • “A small change affects many unrelated areas.” → A boundary or relationship problem.
  • “We have to query deep internal information from everywhere.” → Aggregate or repository leakage.
  • “Everything has to be consistent immediately.” → Multiple aggregates are not being acknowledged.

If the team cannot clearly say what must be true, when it must be true, and who enforces it, the domain model is breaking — no matter how clean the code looks.


Conclusion

A domain model breaks down when a team cannot clearly explain three things. First, what must always be true in the product, independent of features or flows. Second, when those rules apply — only at creation, during updates, or across the entire lifecycle. Third, which part of the model is responsible for enforcing each rule — the aggregate, the factory, the service, or the repository.

When those answers are unclear, rules slowly leak into UI logic, services, and storage code. The system still runs, but it becomes fragile and expensive to change. A well-designed domain model keeps these responsibilities explicit so that product decisions remain legible as the system evolves.


Domain-Driven Design Series

(1) Domain-Driven Design as a Product Management Framework

(2) What is a Domain Model? Definition, Quality Criteria, and Examples

(3) DDD Layered Architecture: The Role of Each Layer in Domain-Driven Design

(4) Object in DDD: Building Blocks of the Domain Model

(5) Service in DDD: When Business Logic Doesn’t Belong to a Single Entity

(6) Modules and Bounded Contexts in DDD: Structuring Domains for Scale

(7) Aggregate Root Pattern in DDD: Consistency Boundaries Explained

(8)Factory Pattern in DDD: Controlling How Aggregates Come Into Existence

(9) Repository in DDD: Pattern, Principles, and the Domain Model Checklist