In part 2, I talked about my domain modeling thought process so this post is about trying to persist those objects for long term storage. Although the avenue for different data storage provider is always open with the kind of design I will talk about here, my default data store tends to be SQL Server which is a relational data store with strong ACID properties. Also, I prefer to use an ORM while dealing with persistence code so I will be using Entity Framework Core in this post because that’s what I used for the expense tracking application that I built. I would imagine things will be similar for most mature ORMs out there. I only have experience with Entity Framework, so there!
Domain Driven Design recommends designing repository interfaces that are specific to the aggregate that you are designing. Assumption being that each aggregate will be retrieved and saved as a whole in one single atomic transaction and that each aggregate will usually have different persistence and data access requirements. One of the patterns that allows me to build data source agnostic applications is called Repository Pattern. I can either build a generic repository interface with a single generic implementation that I can re-use with each entity in my domain model, or, I can follow the “repository per aggregate” recommendation of DDD. How do I decide?
Here are a few observations that I have made in my own efforts to arrive at a decision and hope this helps others too.
First, I start with this base repository interface:
Since this interface doesn’t have any dependency on EF or database or any external component, I can put this interface in a “Common” assembly in the same folder as my domain model assemblies.
Remember from part 2 that I create a high-level domain object interface that will declare an Id of type Guid when implemented by any entity object. The advantage that this offers is that it helps me restrict the types that my repository can deal with as I will show in a moment. Since EF Core still cannot map value objects, all my domain objects end up being entity objects.
Once that’s done I will go in one of the following 2 ways:
1. If the domain model is fairly simple, meaning, all the entities more or less follow CRUD pattern with more or less same data access requirements, then I will create one generic repo implementation and just re-use it across all domain entities without duplicating repo code. For e.g. GenericRepository, GenericRepository etc. i.e. one repo instance (not implementation) per entity:
Ofcourse, notice that I can’t really load navigational properties using this repository because I have no clean way of telling the repsitory of an entity what navigation properties I want loaded , I can only load dependent entities separately using the repository instance of the entity type unless I create some kind of concocted Unit of Work class (I personally don’t like this over-abstraction). For e.g. If I want to load Posts for a specific blog id, I will need to call GenericRepository::GetListAsync(x=>x.BlogId == blogId). This kinda goes against the DDD principle of loading entities only via the aggregate root which in this case is the Blog object (without the Blog, a Post has no meaning). This may or may not be a big problem if there is no consistency boundary to be worried about i.e. if there is no worries about breaking the business constraints by loading Posts directly. As a matter of fact, this might be desirable if I am loading a specific Post, what matters is whether I am loading Posts for reading or writing.
Most trivial apps like a Todo List app or a Address Book app or any app where all I need to do is execute CRUD, might qualify for this simple design as the object model has no real business logic or consistency requirements per se.
2. If the domain model is anything but simple with aggregate specific data access requirements for e.g. financial accounting application or e-commerce application or project management application or any application where I have business logic associated and strong consistency needs, then I will create one repository implementation per aggregate i.e. object graph. This will also allow me to include all the related entities I need in the original query methods and load the entire object graph for e.g. I can load the Period and related Pots and related Expenses in one query.
I will usually create an interface that inherits from the base interface and then put aggregate specific methods in this new interface, this helps keep interfaces clean, in manageable numbers and honour Interface Segregation Principle.
The implementation of the above interface will implement all of the base interface’s methods as well, as shown below. The advantage with this design is that it allows me to include navigational properties in my queries which would be really difficult if you only have one generic repository implementation because not all query requirements are known at the time of generic repository implementation:
Even though this is a more DDD compliant repository, if the object graph being loaded is going to be too large it may pose performance problems so designing the aggregates accurately and understanding the data access requirements is crucial. In some cases, it may be required to load all the dependent entities in the object graph to ensure that the business transaction leaves the aggregate in a consistent state. For e.g. adding an expense must update the Pot’s remaining allocation, the only way to ensure this properly is to load the entire graph in memory, execute the transaction that adds an expense and then adjust the Pot details accordingly, all within the confines of the aggregate. Please refer to part 2 (domain modeling) for details of this.
This would be problematic to achieve if I am only loading one entity type at a time using the “repository per entity” approach.
In part 4, I will talk through the design of Application Services and how it integrates with the domain entities and the repositories to coordinate the dance of use cases in the application. The reason for creating a generic base repository interface will also become a bit clearer.