Understanding CQRS architecture - Part 4
In this post, we will briefly outline what CQRS means and explore how to use it to reexamine the preceding simple example.
What is CQRS ?
As mentioned previously, the CQRS architecture assumes the existence of separate models for queries and commands.
It is noteworthy that in the figure above, we do not represent the plumbing machinery like databases. Indeed, CQRS imposes the separation of models (read and write) but does not mandate using a different data store for each one. Nothing then prevents the use of a single relational database, even the same table, for both reading and writing.
This definition is somewhat minimalist, but it encapsulates the underlying philosophy, and there is no need to exaggerate: CQRS is simply the separation of queries and commands.
In CQRS terminology, the write model is denoted as "commands" while the read model is referred to as "queries".
How can we implement this paradigm with our example ?
- Define a User class
1public class User
2{
3 public string Email { get; set; }
4 public string Password { get; set; }
5}
This class remains unchanged from its previous version; however, this time it will exclusively be utilized by the write model.
- Define a UserQuery class
1public class UserQuery
2{
3 public string Email { get; set; }
4}
This class is limited to the essential elements required for display on the screen. Even if an inattentive engineer inadvertently modifies the underlying SQL query to include the password, it will not be serialized and, consequently, not returned. This approach ensures security by design.
- Define the UserRepository class
1public class UserRepository : IUserRepository
2{
3 public Task<List<UserQuery>> FindUsers()
4 {
5 // ...
6 }
7
8 public Task SaveUser(User user)
9 {
10 // ...
11 }
12}
- Below is an example of an Azure Function utilizing this repository.
1[FunctionName(nameof(FindAllUsers))]
2public async Task<IActionResult> FindAllUsers([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, ILogger log)
3{
4 var repository = new UserRepository(); // In a real-world application, use DI.
5 return new OkObjectresult(await repository.FindUsers().ConfigureAwait(false));
6}
7
8[FunctionName(nameof(SaveUser))]
9public async Task<IActionResult> SaveUser([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, ILogger log)
10{
11 var body = await new StreamReader(req.Body).ReadToEndAsync();
12 var data = JsonConvert.DeserializeObject<List<SaveUserRequest>>(body);
13
14 var repository = new UserRepository(); // In a real-world application, use DI.
15 await repository.SaveUser(data.ConvertToUser()).ConfigureAwait(false);
16
17 return new OkResult();
18}
All this for this ! Yes, this implementation may be somewhat underwhelming (though our use case is particularly simple), but once again, we content ourselves with separating different concepts. Who said that architecture must be complicated ?
In real-world scenarios, the User class would typically be more intricate, featuring complex validation rules and additional business requirements to verify during creation or updates. Generally, all these rules are unnecessary when retrieving data. The clear advantage of segregating the two models becomes evident, as queries (read model) remain unburdened by code unrelated to their concerns.
That being said, CQRS is, in practice, implemented with more complex platforms, and we will now briefly explore some intricacies it can introduce.