// Domain-Driven Design · 08
Repository
A collection-like interface that loads and saves whole aggregates.
How it fits together
- 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 readExpandCollapse
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.