
Domain Driven Design (DDD) is the widely accepted and proven pattern to build applications dealing with complex domains. Not many applications manage to maintain the clear boundaries between bounded contexts as they grow bigger. Even if it is realised, the high costs of refactoring discourage to bring things back on track. The cost of refactoring gets higher with time as unchecked coupling gradually spreads to all the tiers, especially the database.
Often what starts as an ideal view of the DDD world applying OOP principles, soon turns into a tightly coupled monolith. In fact, that is not a bad start. Creating bounded context without understanding the domain runs a risk of getting incorrect boundaries, which would be more expensive to fix. So starting with a monolith and keep pruning / shaping the code as it gets bigger, will be a good strategy to employ.
If you are not familiar with DDD concepts, I would encourage you to read Eric Evan’s book.
ORM is a slippery slope
Entity Framework (EF) or any other ORM has almost become a de facto tool to implement the Repository pattern, mainly because it makes the implementation significantly easier. In a typical ORM based N-Tier application, your data tier is replaced by a superclass called DbContext in Entity Framework and Session in NHibernate. It acts as a conduit between your code and database. For simplicity, I will refer to it as DbContext in this post. To begin with I would like to highlight some of the traps of using an ORM:
Walk in the park: ORM is a facility, whose comfort makes us lazy. The ease at which we can introduce “yet another entity” in our DbContext (and database), tempts us to neglect the fact that this new entity may belong to a different aggregate root or bounded context. While we live in the illusion of being in control, the situation slips out of our hand faster then we realise. What starts as a just few domain entities, soon turns into a tightly coupled web of entities in a fast-growing monolith.
Centre of the universe: Most of the monoliths have a single DbContext containing almost every entity in your application. And then this DbContext gets injected as a dependency to your domain code e.g. Services. Very often it is found to be handy to have all the entities accessible from a single context. With your hands on its instance, you find yourself in the centre of the universe of your domain objects, where you can crawl from one entity to another. This makes it easier to ignore the bounded context boundaries. The luxury of having every entity available in your code further cements the tight coupling within entities as well.
Swiss Knife: One big DbContext is also a culprit of violating SRP (Single Responsibility Principle). It is injected with an initial intention to access a single aggregate root only. But it runs a risk of someone accessing other entities in the class, which otherwise are not in the scope of its responsibility.
Do not blame the tool:
All of the above problems are not the faults of ORM. Just like every other tool, it helps us to do our job efficiently. The only condition is to use it correctly. EF does not dictate the boundaries or size of your bounded context. It is just a medium to persist our domain model.
1) Define the boundaries
Before we take any action, it is important to take a step back and utilise the acquired domain knowledge to identify the subdomains within the system. I will use an example to make it easier to understand. This is a hypothetical example only and the sole purpose is to show how we can separate the model in an iterative way. The example does not reflect the real world application and it may lack some details.
Let’s say that our application deals with the Airline ticket sale and boarding process. The following is how the DbContext of our pretend system looks like.

To keep the complexity in check, our team concluded that we should divide the system into smaller subsystems. Ideally, we will have a separate database or at least database schema for each bounded context. Similarly, in the code, we can reflect the separation by multiple DbContexts.
Bounded Context or Aggregate Root: Ideally you will have a DbContext per aggregate root within the bounded context, but that can be too much to take on, given we are starting from a highly coupled and complex domain model.
Based on the deep understanding of the domain, our team have a consensus to split it into two bounded contexts i.e. IFlightDbContext and ISalesDbContext. As mentioned above, it is just a hypothetical example, not necessarily reflecting the best model for the real world scenario.
From above, we know where we are and our destination. Now we can work on our journey plan.
2) Ensure MainDbContext has an interface defined.
In refactoring, keeping the scope limited is very important. Small iterative small changes over the big bang ensure the current system is running while providing the opportunity to improve. I hope “I” of SOLID is employed in the system i.e. MainDbContext is referenced via an interface. If not that should be the first refactoring step. Tools like reshaper come quite handy for this type of refactoring.
<br><br>public interface IMainDbContext {<br>IDbSet Customers {get;set;} IDbSet Flights {get;set;}<br>IDbSet BoardingPass {get;set;}<br>IDbSet Invoices {get;set;}<br>IDbSet Itinerary {get;set;}<br>IDbSet Seats {get;set;}<br>IDbSet Tickets {get;set;} IDbSet Passengers {get;set;}<br>}<br><br>
3) Define separate implementations for the BoundedContexts
So, in this case, we can introduce two new interfaces, while keeping the current one intact. These new interfaces will be a subset of the existing interface:
<br><br>public interface IMainDbContext {<br>IDbSet Customers {get;set;}<br>IDbSet Flights {get;set;}<br>IDbSet BoardingPass {get;set;}<br>IDbSet Invoices {get;set;}<br>IDbSet Itinerary {get;set;}<br>IDbSet Seats {get;set;}<br>IDbSet Tickets {get;set;}<br>IDbSet Passengers {get;set;}<br>}<br><br>// new contexts<br>public interface ISalesDbContext{<br>IDbSet Tickets {get;set;}<br>IDbSet Invoices {get;set;}<br>IDbSet Flights {get;set;}<br>IDbSet Passengers {get;set;}<br>IDbSet Customers {get;set;}<br>}<br><br>public interface IFlightDbContext {<br>IDbSet Flights {get;set;}<br>IDbSet Seats {get;set;}<br>IDbSet BoardingPass {get;set;}<br>IDbSet Itinerary {get;set;}<br>}<br>
4) Replace IMainDbContext with new interfaces where possible.
Start with marking IMainDbContext as obsolete so that it would annoy developers where it is referenced in the project. That would be a reminder for the dev team to replace them with a fine-grained dbcontext. This refactoring will highlight many violations of SRP. The initial struggle would make the tightly coupled joins more supple and lay the foundation for better separation in further refactoring. Let’s take an example of FlightService that generates a Board Pass. With a shared DbContext that code would have evolved something along the lines:
<br><br>public class FlightService: IFlightService {<br>public BoardingService(IMainDbContext dbContext){<br>this.dbContext = dbContext;<br>}<br><br>public void FlightService (<br>string passengerId, string flightId, string seatNumber) {<br><br>var ticket = await this.dbContext.Tickets<br>.SingleAsync(t =><br>t.Passengers.Any(p => p.Id == passengerId));<br><br>var seat = await this.dbContext.Seats<br>.SingleAsync(s =><br>s.Flight.Id == flightId && s.number == seatNumber);<br><br>var boardingPass = new BoardingPass(ticket, passenger);<br>dbContext.Add(boardingPass)<br>dbContext.Save();<br>}<br>}<br><br>
In the code above, boarding service knows too much about the other domain entities that do not belong to its scope. As per, the restructured domain model, we can see that the ticket is an aggregate root. Now, let’s strip it out from this DbContext to better reflect our domain model.
<br>public class FlightService: IFlightService {<br>public FlughtService(IFlightDbContext dbContext){<br>this.dbContext = dbContext;<br>}<br><br>public void GenerateBoardingPass(<br>PassengerDetails passenger, string flightId, string seatNumber) {<br><br>var seat = await this.dbContext.Seats<br>.SingleAsync(s => s.Flight.Id == flightId && s.number == seatNumber);<br><br>var boardingPass = new BoardingPass(passenger);<br>seat.BoardingPass.Add(boardingPass)<br>dbContext.Save();<br>}<br>}<br><br>
5) Remove inter-entity links
Apart from the dbcontext, entities themselves have links to other entities which often violate aggregate boundaries. Ideally, we want to remove the physical link from the entity and database. In some cases, that could mean a major refactoring. It will require data migration as well as code compilation fixes. Fortunately, EF offers a way to break away from the links in a more iterative way. DbModelBuilder has an ignore method to exclude mappings of type or a property. From the remodelling above, we can break the relation between BoardingPass and Ticket as below:
<br>public interface FlightDbContext: IFlightDbContext {<br>public IDbSet Flights {get;set;}<br>public IDbSet Seats {get;set;}<br>public IDbSet BoardingPass {get;set;}<br>public IDbSet Itinerary {get;set;}<br><br>protected override void OnModelCreating(DbModelBuilder modelBuilder)<br>{<br>modelBuilder.Ignore();<br>}<br>}<br>
Using this process, gradually we will get rid of all the references to the IMainDbContext. In software development there is no final stage, as we get more understanding of the system, we refactor our code to match it. The above separation will enable us to refactor the code in the contained scope. We can take this separation to the next level by physically separating the code and database.
I hope this post helps you to untangle and simplify your domain model. The example above is a very simplified version but in real-world, things can be very complex. We will have to consider more than just slicing the DbContext and database such as employing techniques to provide communication between the bounded contexts such as events on queues/service bus etc.
Some good stuff (even 5 years later )…. Alas there are times where EF6 still tends to pollute. For example when one logical operation results in so much data that having a single context is too much (and thus having passed in an exiusting context is not viable)…
LikeLike