Skip to content

// Domain-Driven Design · 08

Repository

Domain codeleads toby root idRepositoryleads toWhole aggregate

A collection-like interface that loads and saves whole aggregates.

How it fits together

Repository structureApplication serviceSubmitOrder use case«interface»OrderRepositoryGet(orderId)Add(order)Save(order)SqlOrderRepositoryinfrastructureloads Order + linesInMemoryOrdersfake for unit testsOrders databaseorders, order_linesversion column1 Get(id)3 Save(order)2 load graph
uses / callsimplements· numbered steps replay in order
  • Application service uses / calls OrderRepository (1 Get(id))
  • Application service uses / calls OrderRepository (3 Save(order))
  • SqlOrderRepository implements OrderRepository
  • InMemoryOrders implements OrderRepository
  • SqlOrderRepository uses / calls Orders database (2 load graph)

Spot it

  • One repository per table, OrderLines included
  • Domain classes import the database library
  • Callers save order lines on their own

It costs

Another abstraction over an ORM that is often already a repository and unit of work.

In code

// Domain: speaks the aggregate's language, knows nothing about storage.public interface IOrderRepository{    Task<Order?> GetAsync(OrderId id, CancellationToken ct);    Task AddAsync(Order order, CancellationToken ct);    Task SaveAsync(Order order, CancellationToken ct);}​// Infrastructurepublic sealed class SqlOrderRepository(OrdersDbContext db) : IOrderRepository{    public Task<Order?> GetAsync(OrderId id, CancellationToken ct) =>        db.Orders            .Include(o => o.Lines)            .SingleOrDefaultAsync(o => o.Id == id, ct);​    public async Task AddAsync(Order order, CancellationToken ct) =>        await db.Orders.AddAsync(order, ct);​    // A concurrency token on Order turns a lost update into an exception.    public Task SaveAsync(Order order, CancellationToken ct) => db.SaveChangesAsync(ct);}
Go deeperThe full explanation and what to ask for · 1 min readExpand

A repository gives the domain the illusion of an in-memory collection of aggregates: get one by id, add a new one, save its changes. It loads and saves whole aggregates through the root, so nobody can bypass an invariant by writing an OrderLine row on its own.

The interface belongs to the domain and speaks its language; the implementation lives in infrastructure and knows the ORM, the SQL and the mapping. That is dependency inversion: the domain does not depend on the database, and unit tests can use an in-memory fake.

Avoid a generic CRUD repository per table: it exposes inner entities, invites partial updates and adds nothing over the ORM. Keep methods to what commands need, and serve screens and reports with separate read queries (the read side of CQRS) instead of growing FindByStatusAndDateAndRegion methods.

Ask for it

  • repository
  • aggregate root
  • persistence ignorance
  • unit of work
  • port and adapter
  • read model

Replace the generic per-table Repository<T> with an OrderRepository for the Order aggregate: Get(orderId), Add(order), Save(order) with a version check. Interface in the domain, implementation in infrastructure, no open-ended query objects leaking out; serve list screens from a separate read query.