In our DDD journey, we’ve delved into Entities, Value Objects, and how to implement Aggregates from a developer’s viewpoint. Now, we’ll tackle the repository pattern. Simply put, this pattern neatly separates data access from domain logic. While there’s a wealth of online resources, check out Microsoft’s “Design the Infrastructure Persistence Layer” for hands-on implementation in the “eShop on containers” project.
Most intermediate programmers are familiar with the repository pattern and its workings. My focus here will shift from a developer’s lens to an architectural one at this article. I hope to spark that “Aha” moment for you, making you ponder: “Why opt for the repository pattern when we already have an ORM in our project like Entity Framework?” Let’s dive in!
Benefits of the Repository Pattern in Software Design
Repository Pattern in a simple language
Imagine you’re at a huge store with many aisles and countless products. Instead of searching through every aisle yourself, you have a personal shopper who knows exactly where everything is. You just give them a list, and they bring the products to you.
In this scenario:
- The store with its many aisles and products is your database.
- The personal shopper is the repository.
- Giving the list to the shopper is like querying for data.
The Repository Pattern is like having that knowledgeable personal shopper who fetches products (data) for you, so you don’t have to know anything about the store. your personal shopper can even change the store twithought you notice or care (ex. switch from SQL Server to PostgreSQL for some reason)
Repository Pattern in software engineer’s language!
The Repository pattern is a design principle that abstracts data access logic behind a set of interfaces, separating the business logic from direct interactions with data sources, such as databases. It centralizes data access operations, making code more maintainable, testable, and flexible.
The Repository pattern is crucial in software design, especially when working with databases. Here are its key benefits:
- Separation of Concerns: It detaches business logic from data access, ensuring a cleaner design.
- Centralized Access: All data access logic is in one place, simplifying maintenance.
- Testability: Makes unit testing easier by allowing data access to be mocked.
- Flexibility: Changing data sources becomes hassle-free, as only the repository needs modification.
- Consistency: Provides a unified data access approach, enhancing code maintainability.
- Caching: Centralized data access means simpler caching implementation.
- Decoupling: The application isn’t tied to specific data access technologies or libraries.
- Query Abstraction: Avoids database-specific queries, making code cleaner.
Please note while powerful, the Repository pattern isn’t always the best choice. It can be excessive for simple tasks or if misused, cause inefficiencies. Always evaluate its fit for your application.
Navigating Dependencies with the Repository Pattern
DDD focuses on the core domain and domain logic. The primary principle regarding dependencies in DDD is that domain logic should not be dependent on external concerns. Here are the dependencies:
Entities (including Aggregates) & Value Objects: These are the heart of your domain logic. They shouldn’t depend on anything outside the domain.
Domain Services: Operations that don’t naturally fit within an entity or value object.
What if we directly use ORM instead of Wrapping it in the Repository in the Layer
At a glance, the repository pattern might seem like added complexity. One might wonder, “Why not just use the Entity Framework context directly?” Let’s dive into the implications of such a decision.
Consider you’ve modified an aggregate and wish to save these changes. Directly referencing an ORM, such as Entity Framework, may seem more straightforward. For instance, observe the OrderingContext
:
public class OrderingContext : DbContext, IUnitOfWork
{
public OrderingContext(DbContextOptions<OrderingContext> options) : base(options) { }
public DbSet<Order> Orders { get; set; }
}
Now, let’s look at a snippet of the Order
aggregate:
public class Order : Entity, IAggregateRoot
{
private readonly OrderingContext _context;
public Order(OrderingContext context)
{
_context = context;
}
// Some code ...
//Just an example of a simple operation to make you understand why we don't use ORM directly in domain
public async Task changeCustomerName(string customerName)
{
var order = await _context.Orders.FindAsync(_Id);
order.CustomerName = customerName;
// Note: In this application, _context.SaveChanges() is event-based and occurs outside this aggregate.
}
}
However, notice the problem? Our domain is now tightly coupled with DbContext
. As shown above, the Order
doesn’t resemble a typical DDD entity. Instead, it directly fetches and modifies data, bypassing many DDD principles. While it seems simpler, it’s mainly because it resembles a CRUD model without deeper business logic. If such simplicity was the goal, perhaps starting with a CRUD design would’ve been more fitting.
How to solve the database dependency problem with repository pattern
To solve the dependency problem, interfaces are defined in the domain layer, but the implementation, which interacts with the database, resides outside. see IOrderRepository and OrderRepository (where reside in Ordering.Infrastructure instead of Ordering.Domain). Note that there is one repository per aggregate root is defined.
To Illustrate the above see this diagram:
+----------------------+ +----------------------+
| Ordering.Domain | | Ordering.Domain |
| | | |
| IOrderRepository | | IBuyerRepository |
| [Interface] | | [Interface] |
+----------------------+ +----------------------+
| |
| Implements | Implements
v v
+------------------------+ +------------------------+
| Ordering.Infrastructure| | Ordering.Infrastructure|
| | | |
| OrderRepository | | BuyerRepository |
| [Implementation] | | [Implementation] |
+------------------------+ +------------------------+
| |
| Interacts | Interacts
v v
+----------------------------------------------------+
| UNIT OF WORK |
+----------------------------------------------------+
Considering Alternatives to the Repository Pattern
The Repository pattern in DDD helps developers manage data access. However, it’s not the only way. In fact, some experts, like Jimmy Bogard, suggest other approaches. They feel repositories can sometimes hide important details about saving or retrieving data.
For a different approach, let’s look at MediatR with CQRS:
public class UpdateOrderStatus : IRequest<bool>
{
public int OrderId { get; set; }
public string Status { get; set; }
}
public class UpdateOrderHandler : IRequestHandler<UpdateOrderStatus, bool>
{
private readonly DbContext _context;
public UpdateOrderHandler(DbContext context)
{
_context = context;
}
public async Task<bool> Handle(UpdateOrderStatus request, CancellationToken cancellationToken)
{
var order = await _context.Orders.FindAsync(request.OrderId);
order.Status = request.Status;
await _context.SaveChangesAsync();
return true;
}
}
Here, MediatR directly handles database actions, making things clearer. So, while the Repository pattern can be useful, remember there are other choices too. Pick what fits your needs best.
conclusion
In wrapping up, understanding patterns like Repository is more than just about code—it’s about architecting systems that are maintainable, scalable, and clear. As we journeyed through DDD concepts, the central theme remains: choose patterns that align with your project’s needs and goals. Whether you adopt the Repository pattern or another approach, always prioritize a design that best serves the unique requirements of your application. Thanks for joining this exploration, and may your coding journey be ever insightful!
Leave a Reply