Click here to Skip to main content
15,887,083 members
Articles / Hosted Services / Microservices

Understanding CQRS Architecture

Rate me:
Please Sign up or sign in to vote.
5.00/5 (4 votes)
24 Jan 2024CPOL9 min read 7.8K   8  
Comprehending the CQRS architecture and learning how to implement it
Starting today, we embark on a comprehensive series of articles that delve into the CQRS architectures with a specific focus on their application in the realm of microservices architecture. We will incorporate C# code examples as needed; however, the main objective is to thoroughly understand the rationale behind the introduction of this pattern in the early 2010s.

Before delving into the subject, it's worth noting that CQRS stands for Command Query Responsibility Segregation.

Introduction

Microservices are an architectural style that structures a software application as a collection of small, independent, and loosely coupled services. In a microservices architecture, the application is broken down into a set of independently deployable services, each representing a specific business capability. These services can be developed, deployed, and scaled independently, allowing for greater flexibility, maintainability, and scalability.

This approach to presenting microservices is recurrent across various blogs; however, in reality, it merely marks the outset of the narrative. In practice, organizing disparate services cohesively is a highly challenging undertaking that necessitates a profound comprehension of the business. Despite its complexity, this task is arguably the most crucial, and hence, it is referred to as organizing the domain into strategic designs. Once this strategic design is clearly defined and individual services have gained consensus from stakeholders, their implementation is streamlined, and each can adopt a different architectural style. These styles are the tactical patterns. For instance, we can build a CRUD application or implement more sophisticated policies among them.

Throughout this series, we will delve into a widely adopted tactical paradigm known as CQRS, whose objective is to distinctly separate requests for reading data from requests for modifying data. We will explore the implementation of CQRS in the context of Azure Functions for serverless architectures.

The following textbooks on this subject are timeless classic and deserve a place on the shelf of every developer. They extend beyond CQRS architecture and cover a myriad of expansive and general practical use cases.

This article was originally published here.

Disclaimer

The concepts presented in this post are subjective and do not universally apply to all organizations.

Microservices have become a buzzword, and many organizations discuss them and aspire to transition to a service-oriented architecture. While the theoretical concept is compelling, and distributed architectures are envisioned to be transformative, the practical implementation is considerably more challenging. Why? Because organizing an entire enterprise into well-defined silos is a complex undertaking.

What is Strategic Design?

Strategic design, in the context of software architecture and microservices, refers to the high-level planning and organization of services within an application or system. It involves making decisions about how to structure and partition the application to align with the business domain and goals, focuses on creating a coherent and effective arrangement of services that reflects the business's needs and priorities and finally sets the foundation for the overall architecture of a system and directly influences how individual microservices are structured and interact with each other.

It requires a deep understanding of the business domain, collaboration with stakeholders, and a focus on long-term goals and scalability. The decisions made during strategic design significantly impact the success and maintainability of the microservices architecture.

Image 1

Important

In DDD terminology, strategic design involves organizing the enterprise into bounded contexts. The relationships and interactions between different bounded contexts form the context map.

What Is Tactical Design?

Once these patterns are in place, initiating development becomes relatively straightforward, and each service can adopt its own architectural style.

Image 2

Tactical design, in the context of microservices architecture, focuses on the implementation details and specific technical choices made at the level of individual services. It involves decisions related to the internal structure, communication mechanisms, and technology stack used within each microservice. Tactical design is more concerned with the practical aspects of building and maintaining a microservice.

Key aspects of tactical design in microservices include, for example, choosing architectural patterns and styles for individual services, such as RESTful APIs, event-driven architecture, or microkernel architecture, deciding on the data storage approach, including the type of databases used by each microservice and how data is persisted and retrieved, determining how microservices communicate with each other, whether through synchronous RESTful calls, asynchronous messaging, or other communication patterns and so forth.

What is CRUD? What is CQRS?

CRUD and CQRS are both tactical patterns, concentrating on the implementation specifics at the level of individual services. Therefore, asserting that an organization relies entirely on a CQRS architecture may not be entirely accurate. While certain services may adopt this architecture, it is typical for other services to employ simpler paradigms. The entire organization may not adhere to a unified style for all problems.

  • The CRUD architecture assumes the existence of a single model for both read and update operations. CRUD operations are typically linked with traditional relational database systems, and numerous applications adopt a CRUD-based approach for data management.

  • Conversely, the CQRS architecture assumes the presence of distinct models for queries and commands. While this paradigm is more intricate to implement and introduces certain subtleties, it provides the advantage of enabling stricter enforcement of data validation, implementation of robust security measures, and optimization of performance.

These definitions may appear somewhat vague and abstract at the moment, but clarity will emerge as we delve into the details. It's important to note here that CQRS or CRUD should not be regarded as an overarching philosophy to be blindly applied in all circumstances. On the contrary, the choice between them should be made carefully based on the specific characteristics and requirements of the microservices at hand. It is time now to see them in action.

What are the Limitations of the CRUD Architecture?

The CRUD architectural style is extensively utilized and predominantly emphasizes patterns where there is a single model for both queries and commands.

Image 3

We will apply this pattern in a fictional company tasked with developing two straightforward services: one responsible for storing all reference data (countries, currencies, etc.) and another managing identity and the authentication process.

CRUD for the ReferenceData Service

The ReferenceData service is responsible for managing information used throughout the entire organization, including countries, currencies, departments, etc. We will implement it using an Azure Function and code a few CRUD methods.

  • Define a Country class aimed at modeling a country for example:
    C#
    public class Country
    {
        public string Id { get; set; }
        public string Name { get; set; }
    }
  • Define some useful methods in a repository to find or save a country:
    C#
    public class CountryRepository : ICountryRepository
    {
        public async Task<Country> FindById(string id)
        {
            // ...
        }
        
        public Task SaveCountry(Country country)
        {
            // ...
        }
    }
  • Below is an example of an Azure Function utilizing this repository.
    C#
    [FunctionName(nameof(FindCountryById))]
    public async Task<IActionResult> GetAccountById([HttpTrigger
      (AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, ILogger log)
    {
        var id = req.Query["countryId"]; 
    
        var repository = new CountryRepository(); // In a real-world application, use DI.
        return new OkObjectresult(await repository.FindById(id).ConfigureAwait(false));
    }
    
    [FunctionName(nameof(SaveCountry))]
    public async Task<IActionResult> SaveCountry([HttpTrigger
      (AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, ILogger log)
    {
        var body = await new StreamReader(req.Body).ReadToEndAsync();
        var data = JsonConvert.DeserializeObject<List<SaveCountryRequest>>(body);
    
        var country = new Country()
        {
            Id = data.Id,
            Name = data.Name
        };
    
        var repository = new CountryRepository(); // In a real-world application, use DI.
        await repository.SaveCountry(country).ConfigureAwait(false);
    
        return new OkResult();
    }

Information

Certain classes and interfaces (ICountryRepository, etc.) have not been developed here for the sake of conciseness.

This approach is perfectly suitable in this case because the requirements for both read and write operations are the same. When a country is requested, the client application only requires an id and a name. Similarly, for creating or editing a country, an id and a name are the only needed attributes. Separating the two would be unnecessary complexity in this context.

CRUD for the Membership Service

Now, let's contemplate a more intricate scenario wherein an administrator registers a new user (with an email and a password) and subsequently can view all these registered users in their dashboard. Once again, we initiate the process just mentioned for the ReferenceData service.

  • Define a User class aimed at modeling a user:
    C#
    public class User
    {
        public string Email { get; set; }
        public string Password { get; set; }
    }
  • Define a method in a repository to create a user:
    C#
    public class UserRepository : IUserRepository
    {    
        public Task SaveUser(User user)
        {
            // ...
        }
    }
  • Define a method in the same repository to find all users:
    C#
    public class UserRepository : IUserRepository
    {        
        public Task<List<User>> FindUsers()
        {
            // ...
        }
    }
  • Below is an example of an Azure Function utilizing this repository:
    C#
    [FunctionName(nameof(FindAllUsers))]
    public async Task<IActionResult> FindAllUsers([HttpTrigger
        (AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, ILogger log)
    {
        var repository = new UserRepository(); // In a real-world application, use DI.
        return new OkObjectresult(await repository.FindUsers().ConfigureAwait(false));
        
        //
        // Passwords are returned to the client !!!
        //
    }
    
    [FunctionName(nameof(SaveUser))]
    public async Task<IActionResult> SaveUser([HttpTrigger(AuthorizationLevel.Anonymous, 
                            "post", Route = null)] HttpRequest req, ILogger log)
    {
        var body = await new StreamReader(req.Body).ReadToEndAsync();
        var data = JsonConvert.DeserializeObject<List<SaveUserRequest>>(body);    
    
        var repository = new UserRepository(); // In a real-world application, use DI.
        await repository.SaveUser(data.ConvertToUser()).ConfigureAwait(false);
    
        return new OkResult();
    }

However, a problem arises in the FindAllUsers function: the users' passwords are returned to the client, making them susceptible to interception by attackers. More critically, even an administrator should not have access to the credentials of their clients. This constitutes a significant security flaw.

What is the Issue Exactly?

The fundamental issue arises because we need to carry out two distinct operations with different requirements using the same model:

  • The write model must store emails and passwords to enable users to authenticate in subsequent login attempts.
  • The read model involves more than just displaying all the attributes of the user; it encompasses adhering to security requirements and enforcing confidentiality rules.

Why Not Simply Limit the Attributes to Retrieve in the FindUsers Method?

If the data is stored in a relational database, we can, for example, envision coding the retrieval of the user's attributes as follows:

SQL
SELECT t.Email --Password is not returned.
FROM Users 

This way, by not retrieving the password from the database, it is indeed not returned to the client. While this is a solution, in a hurry, an absent-minded developer might one day modify this code as follows:

SQL
SELECT *
FROM Users 

Hence, this demands a stringent discipline that is not always sustainable in the long term.

We could also handle this logic directly within the Azure Function.

C#
[FunctionName(nameof(FindAllUsers))]
public async Task<IActionResult> FindAllUsers([HttpTrigger
  (AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, ILogger log)
{
    var repository = new UserRepository(); // In a real-world application, use DI.
    var users = await repository.FindUsers().ConfigureAwait(false);
    
    users.ToList().ForEach(x => 
    {
        x.Password = "";
    });    
    
    return new OkObjectresult(users);
}

Once again, this is a solution, but the business logic is entangled with the technical code. For non-critical applications, it would be perfectly acceptable, but for typical line-of-business applications, we need to find a better alternative. CQRS to the rescue!

What is CQRS?

As mentioned previously, the CQRS architecture assumes the existence of separate models for queries and commands.

Image 4

Important

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.

Information

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:
    C#
    public class User
    {
        public string Email { get; set; }
        public string Password { get; set; }
    }

    Information

    This class remains unchanged from its previous version; however, this time, it will exclusively be utilized by the write model.

  • Define a UserQuery class:
    C#
    public class UserQuery
    {
        public string Email { get; set; }
    }

    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:
    C#
    public class UserRepository : IUserRepository
    {    
        public Task<List<UserQuery>> FindUsers()
        {
            // ...
        }
        
        public Task SaveUser(User user)
        {
            // ...
        }
    }
  • Below is an example of an Azure Function utilizing this repository:
    C#
    [FunctionName(nameof(FindAllUsers))]
    public async Task<IActionResult> FindAllUsers([HttpTrigger
       (AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest req, ILogger log)
    {
        var repository = new UserRepository(); // In a real-world application, use DI.
        return new OkObjectresult(await repository.FindUsers().ConfigureAwait(false));
    }
    
    [FunctionName(nameof(SaveUser))]
    public async Task<IActionResult> SaveUser([HttpTrigger
       (AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req, ILogger log)
    {
        var body = await new StreamReader(req.Body).ReadToEndAsync();
        var data = JsonConvert.DeserializeObject<List<SaveUserRequest>>(body);    
    
        var repository = new UserRepository(); // In a real-world application, use DI.
        await repository.SaveUser(data.ConvertToUser()).ConfigureAwait(false);
    
        return new OkResult();
    }

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?

Information

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. But, to avoid overloading this article, readers interested in this deeper discussion can find the continuation here.

History

  • 24th January, 2024: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer
France France
Nicolas is a software engineer who has worked across various types of companies and eventually found success in creating his startup. He is passionate about delving into the intricacies of software development, thoroughly enjoying solving challenging problems.


Comments and Discussions

 
-- There are no messages in this forum --