Repository
Repository
"A mechanism for encapsulating storage, retrieval, and search behaviour which emulates a collection of objects." — Eric Evans, Domain-Driven Design (2003)
Intent
Repository provides a collection-like interface for Aggregate Roots. Clients interact with the Repository as if they are working with an in-memory collection — they do not know (or care) whether the Aggregate was retrieved from a relational database, a document store, a cache, or a test fixture held in memory.
The key insight is that Repository is a domain concept. The domain layer defines the Repository interface using domain language (findById, save). The infrastructure layer provides the concrete implementation (ORM adapter, HTTP client, in-memory test double). This inversion keeps domain logic free of persistence concerns.
Repository also acts as a reconstitution point: findById reassembles a fully-valid Aggregate Root from its stored form, ensuring that invariants are intact before the Aggregate is handed to the calling code.
When NOT to Use
- Exposing a Repository for a non-root Entity within an Aggregate.
OrderLineRepositoryis a violation —OrderLineis not an Aggregate Root; accessing it independently bypasses theOrderroot's invariant enforcement. - Using Repository as a generic DAO with arbitrary query methods.
findByStatusAndCreatedDateBetweenAndCustomerIdIn(...)is a table-centric DAO smell. Repository methods should speak the domain's ubiquitous language, not expose relational query plumbing. - When a simple CRUD table has no domain logic and no Aggregate boundary is needed. A lookup table of country codes does not warrant a Repository — a DAO or a simple query is appropriate. Repository earns its abstraction cost only when an Aggregate with real invariants is in play.
Repository vs DAO contrast:
- DAO is table-centric: one DAO per database table, CRUD operations on rows. A
order_linesDAO is a natural fit for the DAO pattern. - Repository is Aggregate-centric: one Repository per Aggregate Root, loads and saves whole Aggregates. An
OrderRepositoryloads the entireOrder(with itsOrderLinechildren), not individual rows. A DAO fororder_linesviolates Repository semantics becauseOrderLineis not an Aggregate Root.
When to Use
- Persisting and retrieving Aggregate Roots without coupling domain logic to a specific storage technology.
- Enabling testability — swap the production ORM implementation for an
InMemoryRepository in unit tests with no test-framework ceremony. - Centralising query complexity: the Repository is the one place where query translation (from domain terms to storage query) lives.
- Decoupling the domain model from infrastructure so the storage mechanism can be replaced (SQL → document store) without touching domain logic.
How It Works
Key rule: One Repository per Aggregate Root — never one per non-root Entity.
Participants:
- Repository Interface (domain layer) — defines collection-like methods in domain language:
findById,save,delete. May include semantically named finders (findPendingOrders) but never generic CRUD queries that return non-root entities. - Repository Implementation (infrastructure layer) — the concrete class that translates repository calls into ORM queries, HTTP calls, or in-memory map operations. The domain layer has no compile-time dependency on this class.
- Aggregate Root — the only type the Repository loads and saves. Children of the Aggregate are persisted as part of the root; they have no Repository of their own.
Reconstitution: findById does the inverse of a factory — it assembles a fully-configured Aggregate Root from persistent data, restoring all invariants. This is analogous to Factory-Method-Pattern creating objects without exposing construction details.
No Unit of Work in skeletal examples: In production systems, a Unit of Work (often the ORM's transaction scope) coordinates writes across multiple Repository operations in one transaction. This is a framework concern; it is not shown in the skeletal examples below.
Class Diagram
classDiagram
class OrderRepository {
<<interface>>
+findById(id : OrderId) Order
+save(order : Order) void
+delete(id : OrderId) void
+findPendingOrders() Order[]
}
class SqlOrderRepository {
-dataSource : DataSource
+findById(id : OrderId) Order
+save(order : Order) void
+delete(id : OrderId) void
+findPendingOrders() Order[]
}
class InMemoryOrderRepository {
-store : Map~OrderId‚ Order~
+findById(id : OrderId) Order
+save(order : Order) void
+delete(id : OrderId) void
+findPendingOrders() Order[]
}
class Order {
<<Aggregate Root>>
-id : OrderId
-items : OrderItem[]
-status : OrderStatus
}
OrderRepository <|.. SqlOrderRepository : implements
OrderRepository <|.. InMemoryOrderRepository : implements
SqlOrderRepository ..> Order : reconstitutes
InMemoryOrderRepository ..> Order : reconstitutes
note for OrderRepository "Domain layer interface:\ncollection-like semantics\none per Aggregate Root"
note for SqlOrderRepository "Infrastructure layer:\nORM queries, DB access"
TypeScript Example
// Repository — TypeScript (interface + in-memory implementation)
// Source: Evans, Domain-Driven Design, p.147
interface OrderRepository {
findById(orderId: string): Promise<Order | null>;
save(order: Order): Promise<void>;
// No findOrderLineById — OrderLine is NOT an aggregate root
}
// Infrastructure layer — the domain layer never imports this class directly
class InMemoryOrderRepository implements OrderRepository {
private store = new Map<string, Order>();
async findById(id: string): Promise<Order | null> {
return this.store.get(id) ?? null;
}
async save(order: Order): Promise<void> {
this.store.set(order.orderId, order);
}
}Java Example
// Repository — Java (plain interface + in-memory implementation)
// Source: Evans, Domain-Driven Design, p.147
interface OrderRepository {
Optional<Order> findById(String orderId);
void save(Order order);
// No findOrderLineById — OrderLine is NOT an aggregate root
}
class InMemoryOrderRepository implements OrderRepository {
private final Map<String, Order> store = new HashMap<>();
@Override
public Optional<Order> findById(String orderId) {
return Optional.ofNullable(store.get(orderId));
}
@Override
public void save(Order order) {
store.put(order.getOrderId(), order);
}
}
// Real-world: Spring Data JPA interface extending JpaRepository<Order, String>
// — repository pattern implemented by the framework.Lineage Backward
Factory-Method-Pattern — Repository's findById reconstitutes Aggregates from persistent form without exposing construction details, analogous to Factory Method creating objects without revealing the concrete class. Both patterns abstract object creation/reconstruction behind an interface.
Lineage Forward
Aggregate (bidirectional) — Repository is the persistence boundary for each Aggregate Root; the two patterns are defined in terms of each other. The Repository loads and saves Aggregates; Aggregates emit the events and enforce the invariants that make a Repository necessary.
Related Concepts
| Pattern | Relationship |
|---|---|
| Aggregate | One Repository per Aggregate Root — the two are tightly coupled by definition |
| Factory-Method-Pattern | Reconstitution analogy — findById assembles an Aggregate as Factory Method assembles an object |
| DAO (Data Access Object) | Table-centric alternative — Repository is Aggregate-centric; DAO operates on rows, not invariant boundaries |
| Unit of Work | Coordinates writes across multiple Repositories in a single transaction — a framework concern (ORM/transaction manager), not shown in skeletal examples |
Related Architecture Patterns
- Clean-Architecture — The Dependency Rule's canonical example: Repository interface defined in the Use Cases ring (application layer), implementation in the Frameworks and Drivers ring (infrastructure).
- Hexagonal-Architecture — Repository is the canonical secondary port; the interface is defined inside the application core (hexagon), the ORM implementation lives outside as a driven adapter.
Related System Design
- SQL-vs-NoSQL — the SQL-vs-NoSQL selection decision is enforced at the Repository implementation boundary; domain logic above the Repository is agnostic to whether the backing store is relational or NoSQL
Sources
- Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software, Addison-Wesley 2003 — p.147
- Fowler, Patterns of Enterprise Application Architecture, Addison-Wesley 2002 — Repository pattern