Software Architectures examples: CQRS
In some applications, we can find many benefits by splitting the model into two parts: the update part, commands, and the reading part, queries. In 2006, Greg Young started talking about this pattern, also known as Command Query Responsibility Segregation (CQRS).
We can see this pattern as a result to apply the Command–query separation principle,CQS at architecture abstraction level.
Benefits
Less complex domain models.
Write model only knows about command related data and validations.
Read model only knows about views data, data do not need to be validated.
Possibility to take decisions about improving performance without affecting the whole application. For example, the read model can be scaled separately using other faster database technology.
Highly potential to integrate with other patterns, for example, patterns based on events.
Potentially to implement resilence strategies like rebuild queries persistence in case of data loss.
Drawnbacks
This pattern is not a silver bullet. In many cases using this pattern only adds more complexity, more code to maintain, and usually more infrastructure parts. An example is to apply this pattern in a simple CRUD application.
The decision to apply this pattern must be followed by model simplification benefits, and maybe by nonfunctional requirements like queries velocity scalability.
Example
Following this aspnetcore example, consider the next code distribution:
This music application example is built on top of the previous Hexagonal Architecture example. It’s recommended to understand previous examples first. In this example, we had decided to separate the code into two parts: reads and writes. In the application layer now we find two different folders:
Application.Write with the application write use cases, the commands, models as services. In this case: CreatePlayList, AddTrackToPlayList, etc.
Application.Read with the application read use cases, models as queries. : GetTracksQuery, GetAllPlayListQuery, etc.
If we look at the project dependency tree, we can see that reads and writes are completely separated:
Models difference
Now we will compare the code with the previus example. As you can see, now models has been separated in two proyects: MyMusic.Domain and MyMusic.Application.Read.Model:
Write model
The write model, is still the domain, who owns validations an behaviour, as public methods, as we can see in the Playlist:
Class: MyMusic.Domain.PlayList
public class PlayList {
public string Id { get; }
public string Name { get; private set; }
public PlayListStatus Status { get; private set; }
public List<Track> TrackList { get; }
public string ImageUrl { get; private set; }
public PlayList(string id, string name, PlayListStatus status, List<Track> trackList, string imageUrl) {
Id = id;
Name = name;
Status = status;
TrackList = trackList;
ImageUrl = imageUrl;
}
public static PlayList Create(string id, string name) {
return new PlayList(id, name, PlayListStatus.Active, new List<Track>(), null);
}
public Option<DomainError> Add(Track track) {
var trackToAddAlreadyInPlayList = TrackList.FirstOrDefault(x => x.Id.Equals(track.Id));
if (trackToAddAlreadyInPlayList != null) return DomainError.CannotAddSameTrackTwice;
TrackList.Add(track);
return Option<DomainError>.None;
}
public Option<DomainError> Remove(string trackId) {
var trackToRemove = TrackList.FirstOrDefault(track => track.Id.Equals(trackId));
if (trackToRemove == null) return DomainError.TrackIsNotInThePlayList;
TrackList.Remove(trackToRemove);
return Option<DomainError>.None;
}
public void Rename(string newPlayListName) {
Name = newPlayListName;
}
public void Archive() {
Status = PlayListStatus.Archived;
}
public void AddImageUrl(string aNewImageUrL) {
ImageUrl = aNewImageUrL;
}
}
On the other hand, PlayList class, have all the properties needed by the domain rules: Id, Name, Status, TrackList, and ImageUrl.
But, If we take a look to Track class:
public class Track {
public string Id { get; }
private Track(string id) {
Id = id;
}
public static Track With(string trackId) {
return new Track(trackId);
}
}
We can see that the domain only needs the Id property to work with the rules. This means, that splitting the models has simplify our write model.
Read model
We can see that in the real model we also have a PlayList class, with the same properties, but without business rules. So we end up with just a plain object to serve data.
public class PlayList {
public string Id { get; }
public string Name { get; private set; }
public PlayListStatus Status { get; private set; }
public List<Track> TrackList { get; }
public string ImageUrl { get; private set; }
public PlayList(string id, string name, PlayListStatus status, List<Track> trackList, string imageUrl) {
Id = id;
Name = name;
Status = status;
TrackList = trackList;
ImageUrl = imageUrl;
}
}
And if we look at Track class, we can see that it have properties like Name and *Artist, who are important for the views, but not necesary for the business rules.
public class Track {
public string Id { get; }
public string Name { get; }
public string Artist { get; }
public int DurationInMs { get; }
public Track(string id, string name, string artist, int durationInMs) {
Id = id;
Name = name;
Artist = artist;
DurationInMs = durationInMs;
}
}
At this point, the model doesn’t care who part of the infrastructure fills the Name data in the database, maybe another system, or data migration.
What about splitting persistence?
A big beneffist to work like this is that now, optionaly, we can change read models persistence by another technology without affecting any business rules.
As simple as implement other adapter, in this case we have TracksPostgreSQLQueriesAdapter
public class TracksPostgreSQLQueriesAdapter : TracksQueryPort {
public Track GetTrack(string trackId) {
//This should be read from PostgreSQL DB
return new Track("2E5804A7-A0CC-46E0-B167-A818A696F3E0", "Mis Colegas", "Ska-P", 246600);
}
}
But it can be easily replaced by TracksMongoQueriesAdapter for example. But take into account, that if you decide to split the persistence, you need a synchronization method between models.