Blog

Domain-Driven Design Part 3 – Read Models

13 Jun, 2016
Xebia Background Header Wave

I’m guessing some readers will be like, "Hey, read models are not part of DDD!". They will be only partially correct. Eric Evans does indeed mention the use of queries which return a summary of some calculations.

Part 1. Part 2.

Read Models in the Wild

The problem with the usual approach to read models (sometimes called various other names) is that they usually share the same physical and oftentimes conceptual model as the actual Domain. In a highly partitioned system with many types of Aggregate Roots, possibly living separately in multiple Bounded Contexts, this can cause a serious headache.

In a typical SQL-backed data model, the high road leads to creating many navigation properties on Entities, which are the used to join them together for querying. This is especially easy in C# with Language-Integrated Queries (LINQ). Have you seen similar code?

var ticketsLastMonth = from project in Context.Projects
            where project.IsActive
            from ticket in project.Tickets
            where ticket.StartDate >= startOfMonth
            group ticket by ticket.AssignedPerson into g
            select new TicketViewModel { 
                Title = ticket.Title,
                AssignedPerson = ticket.AssignedPerson.Name + " " + ticket.AssignedPerson.LastName,
                Manager = ticket.AssignedPerson.Manager.Name + " " + ticket.AssignedPerson.Manager.LastName,
                Status = Context.StatusTranslations.FirstOrDefault(t => t.Id == ticket.Status).Translation
            };

This query will produce multiple joins and/or subqueries and it’s not that complex either. Complex screens often require as many as 10 joined tables. It gets even worse when, instead of a specialized type like the TicketViewModel above, actual entities are returned (well, they can be returned inside a view model nonetheless). Combine that with Lazy Loading enabled and you’ve just started walking down a path leading to serious performance issues.

Also, it is trivially simple to go ahead and reuse such queries across multiple read models. This is when this approach has great potential to become, maintenance nightmare –  changing one read model has the potential of breaking multiple others. And even if they don’t break, they may start behaving differently than expected.

Admit it, we’ve all written code like this. But we can do better!

Keep ’Em Separated – CQRS to the Rescue

A first step is to actually separate the domain model from the read model on the conceptual level. By that, I mean that a query should not use the same data model which is used for storing the Entities. The simplest approach is to replace the LINQ query above with a specialized SQL query, possibly encapsulated as a view. Such a query or view would be accessible by a separate database object – Entity Framework’s IDbContext/IObjectContext, NHibernate’s ISession or similar. This way, it is not possible for domain entities to leak from the query.

I personally suggest keeping read models separated in their own libraries as well. This way it will not be possible to reuse queries between read models. It is worth noting that these suggestions are neither exclusive nor inclusive. The middle ground is to keep the database model shared between the domain and read models but not use it directly as domain entities. Do have a look at this post titled Domain models vs Persistence models for a more in-depth discussion.

Unfortunately, the approach of pure-SQL executed over shared relational model has a big downside where it drops the niceties of LINQ. Database changes will only be discoverable at runtime and it may be harder for some developers to write SQL code instead of C#.

Last but not Least: Event Sourcing

Domain Events can be used not only to store the state of Entities but also to create the read models incrementally. They are often called projections, by the way. This use of domain events has great implications. First of all, each query can be a simple select from just one table. Projections are usually highly specialized and denormalized, which helps achieve lightning fast reads. Additionally, because they are in completely separate tables/database, such read models can be selectively duplicated and load-balanced.

How do you build such read models? It is as simple as waiting for Domain Events to happen and updating the read model accordingly. Building a very simple sum of open tickets per project would be implemented by incrementing the project’s ticket counter whenever a hypothetical TicketOpenedEvent has happened and decrementing the counter after TicketOpenedClosedEvent. Simple in theory, though there has to be some infrastructure to make it happen.

Read models built this way can also be safely discarded and rebuilt from scratch whenever they have to change, due to a bug or to include another type of domain event. Finally, as a consequence of all past events being stored forever, a new read model can be created any time and it will retroactively include anything from the past.

Example from the Link Sharing Platform

Part of the PGD.DDD solution is PGS.DDD.ReadModel, which contains basic pieces for creating read models. It contains the necessary interfaces used to build read models from Domain Events.

Building Read Models

In our project, the read models are basically SQL Server database tables, populated incrementally by processing Domain Events. The core of this process are the IReadModelBuilder interfaces.

public interface IReadModelBuilder
{
    void Clear();
    void Save();
}

public interface IReadModelBuilder : IReadModelBuilder where TDomainEvent : DomainEvent
{
    void ApplyEvent(TDomainEvent domainEvent);
}

To create a read model, we implement the builder interface for each relevant event. For us, the implementation of ApplyEvent is simply converting the input to a table row. This could however be more complicated if the read model did some sort of aggregation such as summing and grouping of events.

Recreating the Read Model

I mentioned above that the benefit of using event sourcing to build read models is that they can be discarded at will and recreated from scratch. You may have noticed the Clear method. Its purpose is to delete all contents of a read model so that relevant past events can be processed with the ApplyEvent methods. We have created a simple method which iterates over everything:

private static readonly Lazy<ISet> _supportedDomainEvents;
private readonly IEventStore _eventStore;
private readonly IReadModelBuilderFactory _factory;

private object Recreate()
{
    using (var readModel = _factory.Create())
    {
        readModel.Clear();

        foreach (DomainEvent domainEvent in _eventStore.GetEvents())
        {
            if (_supportedDomainEvents.Value.Contains(domainEvent.GetType()))
            {
                ((dynamic) readModel).ApplyEvent((dynamic) domainEvent);
            }
        }

        readModel.Save();
    }
}

It’s as simple as iterating over every single event from the event store and selecting events supported by the given IReadModelBuilder type. With a little reflection and some dynamic method invocation it takes only a few lines of code.

Summary

The approach described here is very opinionated and depends on the use of event sourcing. Also, instead of a service bus it would be possible to periodically poll an ATOM feed of events and apply those which have happened since the last check.

On the other hand, such an approach gives unmatched possibilities for creating read models retroactively. A new read model can be implemented at any time during the application lifetime and include past data. Traditionally, a new feature cannot do that and only works from the point of deployment. It is also possible to create multiple instances of a critical read model so that it can be load-balanced. This comes at a possible latency cost, where the events get applied. But, from there on, such a read model is simply a static view of your data, which can scale tremendously both horizontally and vertically. It doesn’t even need to be a table. A read model can simply be a static, precomputed HTML file!

Questions?

Get in touch with us to learn more about the subject and related solutions

Explore related posts