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. OrderLineRepository is a violation — OrderLine is not an Aggregate Root; accessing it independently bypasses the Order root'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_lines DAO is a natural fit for the DAO pattern.
  • Repository is Aggregate-centric: one Repository per Aggregate Root, loads and saves whole Aggregates. An OrderRepository loads the entire Order (with its OrderLine children), not individual rows. A DAO for order_lines violates Repository semantics because OrderLine is 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 InMemory Repository 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.

PatternRelationship
AggregateOne Repository per Aggregate Root — the two are tightly coupled by definition
Factory-Method-PatternReconstitution 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 WorkCoordinates writes across multiple Repositories in a single transaction — a framework concern (ORM/transaction manager), not shown in skeletal examples
  • 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.
  • 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