Chapter 4: The Bridge Between Domain and Data - Repository Pattern
Of course. After defining Aggregates to protect integrity, we need a mechanism to store and retrieve them. This brings us to the next pattern: the Repository.
The Scenario 📝
In DDD, a Repository is not just a data access layer. It's a pattern with a very specific purpose and set of rules.
- System: We have the
OrderAggregate from the previous chapter. - Problem: The
OrderApplicationServiceneeds a way to:- Fetch a complete
Orderobject from the database. - Save that
Orderobject after it has been modified (e.g., after callingorder.Cancel()). How can this be done without leaking database logic (like EF Core'sDbContext) into the Domain Layer?
- Fetch a complete
The Common (but not DDD) Approach: Generic Repository
Many projects use a Generic Repository for all entities.
csharp
// An anti-pattern in DDD
public interface IRepository<T>
{
T GetById(int id);
void Add(T entity);
void Update(T entity);
// ... other generic CRUD methods
}- Problem Analysis:
- Violates Aggregate Boundaries: This pattern allows you to create an
IRepository<OrderItem>. This is extremely dangerous because it allows other developers to fetch and modify anOrderItemindependently, breaking the integrity of theOrderAggregate. - Loses Business Context: The methods
GetByIdandAddare too generic. AnIOrderRepositoryshould have methods that more clearly express the business intent, for example,GetCompletedOrdersForCustomer(customerId).
- Violates Aggregate Boundaries: This pattern allows you to create an
The Solution: One Repository per Aggregate ✅
The Logic:
- Only create a Repository for the Aggregate Root. There will never be an
IOrderItemRepository. - The Repository acts like an in-memory collection of Aggregates. It hides all the details of the database.
- The Repository's interface is defined in the Domain Layer, but its implementation resides in the Infrastructure Layer.
- Only create a Repository for the Aggregate Root. There will never be an
Optimized Code:
Interface in the Domain Layer (
ApplicationCore/Interfaces/IOrderRepository.cs)csharp// This interface knows nothing about the database, it only defines the contracts public interface IOrderRepository { Task<Order?> GetByIdAsync(int orderId); Task AddAsync(Order order); Task SaveChangesAsync(Order order); // Or just SaveAsync(Order order) }Implementation in the Infrastructure Layer (
Infrastructure/Data/OrderRepository.cs)csharp// This class implements the interface using EF Core public class OrderRepository : IOrderRepository { private readonly AppDbContext _context; public OrderRepository(AppDbContext context) { _context = context; } public async Task<Order?> GetByIdAsync(int orderId) { // Must ensure the full Aggregate is loaded, including child entities return await _context.Orders .Include(o => o.Items) // Eagerly load the items .FirstOrDefaultAsync(o => o.Id == orderId); } public async Task AddAsync(Order order) { await _context.Orders.AddAsync(order); } public async Task SaveChangesAsync(Order order) { // EF Core's Change Tracking will automatically handle the update await _context.SaveChangesAsync(); } }
Analysis of the Result ✨
- Aggregate Protection: By only providing an
IOrderRepository, we force all interactions to go through theOrderAggregate Root, ensuring business rules are always followed. - Separation of Concerns:
- The
Domain Layercontains only pure business logic, with no database dependencies. - The
Infrastructure Layeris responsible for persistence, completely separate from the Domain.
- The
- Clear Code: The Repository's interface becomes a clear contract, expressing all the ways the application can query that Aggregate.
Conclusion:
- In DDD, a Repository is not a generic CRUD layer.
- It is a specialized interface that acts like an in-memory collection, responsible for persisting and retrieving an entire Aggregate.
- This pattern is key to maintaining the purity of the Domain Layer and protecting the integrity of your Aggregates.