Repositories provide a collection-like interface for accessing aggregate roots, abstracting away persistence details from the domain model.

Core Characteristics

Collection Abstraction

  • Act like in-memory collections
  • Hide database implementation
  • Query using domain concepts, not SQL

Aggregate Root Access

  • One repository per aggregate root
  • No repositories for child entities
  • Work with complete aggregates

Persistence Ignorance

  • Domain model doesn’t know about database
  • Repositories handle mapping
  • Swap implementations without changing domain

Purpose

Decouple Domain from Infrastructure

// Application layer - orchestrates use case
public class PlaceOrderHandler
{
    private readonly IOrderRepository _repository;
 
    public void Handle(PlaceOrderCommand command)
    {
        var order = _repository.GetById(command.OrderId);
        order.Submit();  // Entity method - domain logic
        _repository.Save(order);  // Repository abstracts persistence
    }
}
 
// Infrastructure layer - knows about database
public class OrderRepository : IOrderRepository
{
    private readonly DbContext _context;
 
    public Order GetById(OrderId id)
    {
        return _context.Orders
            .Include(o => o.Lines)
            .FirstOrDefault(o => o.Id == id);
    }
 
    public void Save(Order order)
    {
        if (_context.Entry(order).State == EntityState.Detached)
            _context.Orders.Add(order);
 
        _context.SaveChanges();
    }
}

Repository Interface

public interface IOrderRepository
{
    Order GetById(OrderId id);
    IEnumerable<Order> GetByCustomer(CustomerId customerId);
    void Save(Order order);
    void Remove(Order order);
}
 
public interface ICustomerRepository
{
    Customer GetById(CustomerId id);
    Customer GetByEmail(Email email);
    IEnumerable<Customer> GetActive();
    void Save(Customer customer);
    void Remove(Customer customer);
}

Implementation Patterns

Basic Repository

public class OrderRepository : IOrderRepository
{
    private readonly ApplicationDbContext _context;
 
    public OrderRepository(ApplicationDbContext context)
    {
        _context = context;
    }
 
    public Order GetById(OrderId id)
    {
        return _context.Orders
            .Include(o => o.Lines)  // Load aggregate completely
            .FirstOrDefault(o => o.Id == id);
    }
 
    public IEnumerable<Order> GetByCustomer(CustomerId customerId)
    {
        return _context.Orders
            .Include(o => o.Lines)
            .Where(o => o.CustomerId == customerId)
            .ToList();
    }
 
    public void Save(Order order)
    {
        if (order.Id == null)
            _context.Orders.Add(order);
        else
            _context.Orders.Update(order);
 
        _context.SaveChanges();
    }
 
    public void Remove(Order order)
    {
        _context.Orders.Remove(order);
        _context.SaveChanges();
    }
}

Specification Pattern

public interface ISpecification<T>
{
    Expression<Func<T, bool>> Criteria { get; }
    List<Expression<Func<T, object>>> Includes { get; }
}
 
public class OrdersByCustomerSpec : ISpecification<Order>
{
    public Expression<Func<Order, bool>> Criteria { get; }
    public List<Expression<Func<Order, object>>> Includes { get; }
 
    public OrdersByCustomerSpec(CustomerId customerId)
    {
        Criteria = order => order.CustomerId == customerId;
        Includes = new List<Expression<Func<Order, object>>>
        {
            order => order.Lines
        };
    }
}
 
public interface IRepository<T>
{
    T GetById(object id);
    IEnumerable<T> Find(ISpecification<T> spec);
    void Save(T entity);
}

Unit of Work Pattern

public interface IUnitOfWork
{
    IOrderRepository Orders { get; }
    ICustomerRepository Customers { get; }
    IProductRepository Products { get; }
 
    void Commit();
    void Rollback();
}
 
public class UnitOfWork : IUnitOfWork
{
    private readonly ApplicationDbContext _context;
 
    public IOrderRepository Orders { get; }
    public ICustomerRepository Customers { get; }
    public IProductRepository Products { get; }
 
    public UnitOfWork(ApplicationDbContext context)
    {
        _context = context;
        Orders = new OrderRepository(context);
        Customers = new CustomerRepository(context);
        Products = new ProductRepository(context);
    }
 
    public void Commit()
    {
        _context.SaveChanges();
    }
 
    public void Rollback()
    {
        // Discard changes
        _context.ChangeTracker.Clear();
    }
}

Repository vs. DAO (Data Access Object)

AspectRepositoryDAO
AbstractionCollectionDatabase
FocusDomain objectsData tables
InterfaceDomain languageCRUD operations
ScopeAggregate rootsAny entity
QueriesDomain conceptsSQL/database oriented
// Repository: Domain-focused
var activeOrders = _orderRepository.GetActive();
var vipOrders = _orderRepository.GetByCustomerType(CustomerType.VIP);
 
// DAO: Database-focused
var orders = _orderDAO.ExecuteQuery("SELECT * FROM Orders WHERE Status = 1");

Query Methods

By Identity

Order GetById(OrderId id);
Customer GetByEmail(Email email);
Product GetBySku(Sku sku);

By Criteria

IEnumerable<Order> GetByCustomer(CustomerId customerId);
IEnumerable<Order> GetPending();
IEnumerable<Product> GetByCategory(Category category);

Existence Checks

bool Exists(OrderId id);
bool ExistsByEmail(Email email);

Best Practices

One Repository Per Aggregate Root

// Good: Repository for aggregate root
public interface IOrderRepository
{
    Order GetById(OrderId id);
    void Save(Order order);
}
 
// Bad: Repository for child entity
public interface IOrderLineRepository  // DON'T DO THIS
{
    OrderLine GetById(int id);
}
// Access OrderLines through Order aggregate instead

Return Complete Aggregates

// Good: Loads entire aggregate
public Order GetById(OrderId id)
{
    return _context.Orders
        .Include(o => o.Lines)      // Load children
        .Include(o => o.Payments)   // Load all parts
        .FirstOrDefault(o => o.Id == id);
}
 
// Bad: Partial aggregate
public Order GetById(OrderId id)
{
    return _context.Orders.Find(id);  // Lines not loaded!
}

Domain-Oriented Queries

// Good: Domain language
IEnumerable<Order> GetOverdueOrders();
IEnumerable<Customer> GetVipCustomers();
IEnumerable<Product> GetOutOfStock();
 
// Bad: Generic queries
IEnumerable<Order> GetWhere(Expression<Func<Order, bool>> predicate);

Hide Persistence Details

// Good: Repository handles mapping
public class OrderRepository
{
    public Order GetById(OrderId id)
    {
        var dto = _context.Orders.Find(id);
        return MapToOrder(dto);  // Hide mapping
    }
}
 
// Bad: Exposing persistence
public class OrderRepository
{
    public OrderEntity GetById(OrderId id)  // Leaks DB entity!
    {
        return _context.Orders.Find(id);
    }
}

Testing with Repositories

In-Memory Implementation

public class InMemoryOrderRepository : IOrderRepository
{
    private readonly Dictionary<OrderId, Order> _orders = new();
 
    public Order GetById(OrderId id)
    {
        return _orders.TryGetValue(id, out var order) ? order : null;
    }
 
    public void Save(Order order)
    {
        _orders[order.Id] = order;
    }
 
    public void Remove(Order order)
    {
        _orders.Remove(order.Id);
    }
}
 
// Usage in tests
[Test]
public void PlacingOrder_ShouldSaveToRepository()
{
    var repository = new InMemoryOrderRepository();
    var handler = new PlaceOrderHandler(repository);
 
    var order = new Order(...);
    handler.Handle(new PlaceOrderCommand(order.Id));
 
    var saved = repository.GetById(order.Id);
    Assert.NotNull(saved);
}

Common Patterns

Lazy Loading

⚠️ Use with care — it pulls against DDD’s grain. An aggregate is meant to load as a whole (see Return Complete Aggregates above), and having the Order entity hold a repository to fetch its own children both leaks infrastructure into the domain and reaches for child entities directly — the very thing one repository per aggregate root warns against. Prefer loading the aggregate completely; reach for lazy loading only when a genuinely large child collection makes eager loading impractical, and treat it as an ORM-mapping concern rather than something the domain model is aware of.

public class Order
{
    private ICollection<OrderLine> _lines;
 
    public ICollection<OrderLine> Lines
    {
        get
        {
            if (_lines == null)
                _lines = _repository.GetLinesForOrder(Id);
            return _lines;
        }
    }
}

Caching

public class CachedOrderRepository : IOrderRepository
{
    private readonly IOrderRepository _innerRepository;
    private readonly ICache _cache;
 
    public Order GetById(OrderId id)
    {
        var cacheKey = $"Order:{id}";
 
        if (_cache.TryGet(cacheKey, out Order order))
            return order;
 
        order = _innerRepository.GetById(id);
        _cache.Set(cacheKey, order, TimeSpan.FromMinutes(5));
 
        return order;
    }
}

Event Dispatching

public class OrderRepository : IOrderRepository
{
    private readonly IEventDispatcher _eventDispatcher;
 
    public void Save(Order order)
    {
        _context.SaveChanges();
 
        // Dispatch domain events after save
        foreach (var @event in order.DomainEvents)
        {
            _eventDispatcher.Dispatch(@event);
        }
 
        order.ClearDomainEvents();
    }
}

Anti-Patterns

Generic Repository

// Bad: Too generic, no domain meaning
public interface IRepository<T>
{
    T GetById(int id);
    IEnumerable<T> GetAll();
    void Add(T entity);
    void Update(T entity);
    void Delete(T entity);
}
 
// Good: Domain-specific
public interface IOrderRepository
{
    Order GetById(OrderId id);
    IEnumerable<Order> GetPendingOrders();
    void Save(Order order);
}

Repositories for Everything

// Bad: Repository for non-aggregate
public interface IOrderLineRepository { }  // OrderLine is part of Order aggregate
 
// Good: Access through aggregate
var order = _orderRepository.GetById(orderId);
var line = order.Lines.First();
  • Aggregates - What repositories manage
  • Entities - Aggregate roots accessed via repositories
  • Domain Driven Design - Overall pattern context
  • Unit of Work - Transaction management pattern
  • Specification - Query encapsulation pattern

References