Skip to content

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 Order Aggregate from the previous chapter.
  • Problem: The OrderApplicationService needs a way to:
    1. Fetch a complete Order object from the database.
    2. Save that Order object after it has been modified (e.g., after calling order.Cancel()). How can this be done without leaking database logic (like EF Core's DbContext) into the Domain Layer?

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:
    1. 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 an OrderItem independently, breaking the integrity of the Order Aggregate.
    2. Loses Business Context: The methods GetById and Add are too generic. An IOrderRepository should have methods that more clearly express the business intent, for example, GetCompletedOrdersForCustomer(customerId).

The Solution: One Repository per Aggregate ✅

  • The Logic:

    1. Only create a Repository for the Aggregate Root. There will never be an IOrderItemRepository.
    2. The Repository acts like an in-memory collection of Aggregates. It hides all the details of the database.
    3. The Repository's interface is defined in the Domain Layer, but its implementation resides in the Infrastructure Layer.
  • 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 ✨

  1. Aggregate Protection: By only providing an IOrderRepository, we force all interactions to go through the Order Aggregate Root, ensuring business rules are always followed.
  2. Separation of Concerns:
    • The Domain Layer contains only pure business logic, with no database dependencies.
    • The Infrastructure Layer is responsible for persistence, completely separate from the Domain.
  3. 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.