One of the most popular practices in event-driven architectures today is called CQRS, which is short for Command Query Responsibility Segregation. CQRS is a style of architecture that allows you to use different models to update and read domain data.
The basic idea of CQRS is that it’s perfectly natural to need to separate the models you’re using to update and read data. The diagram above shows this basic idea.
CQRS is popular for event-driven architectures because domain events — as inputs — are structurally different than the domain model they are subject to. Take for example the following domain model object representing an account.
Example 1. Account aggregate
1 |
|
When a service wants to query for an account, this is the model it will expect. Now, what if we wanted to update the status to ACCOUNT_SUSPENDED
? Normally this would be a simple update to the domain object for the status field. Now, what if we wanted to use a domain event to update the status instead? Since a domain object is structurally different from an event, we will need an API that accepts a different model as a command.
The following snippet is a domain event that transitions the state of the account from ACCOUNT_ACTIVE
to ACCOUNT_SUSPENDED
.
Example 2. Account event
1 |
|
To process this domain event and apply the update to the query model, we must have an API to accept the command. The command will contain the model of the domain event and use it to process the update to the account’s query model.
This is the simplest explanation of CQRS — to separate the command model from the query model. The complexity we often see today is more to do with the flavor of implementation. This is especially true when applying the pattern to microservices.
CQRS and Microservices
When CQRS combines with microservices, things get a bit complex — to say the least. Let’s take a look at what a “simple” microservice resembles that implements CQRS using Spring Boot.
The diagram above is a rough sketch of an implementation of the CQRS pattern.
Here we’ve split up a single microservice into a command-side, query-side, and event processor—all of which can be deployed independently of one another.
Command-side
The command-side in this example exposes a REST API that accepts requests over HTTP. Requests take the form of commands that drive state changes to domain data owned by the microservice. To put it simply, any writes on domain data will flow from an API request as a command — which handles an action that results in changes to the database.
Commands trigger actions and actions trigger domain events. The domain events persist to an event store—a fancy way to say “a system that combines a database together with a message broker.” |**|An excellent event store to get started with is called Eventuate, which was founded by Chris Richardson as a project to help apply CQRS and Event Sourcing to microservices.|
Domain events store as a series of time-ordered events appended to a log. Since every command generates an event, we’re able to rebuild the total state of the current system from a history of collected events. I cover this topic in more detail in a previous blog post on event sourcing in microservices.
Event Processor
The next component we’ll examine is the Event Processor. This CQRS component takes the form of a worker application that is responsible for ingesting domain events. The event processor is stateless and listens for messages from the event store, applying an action for incoming event messages.
The event processor can respond to a new domain event in many useful ways. One domain event can spawn more events that can be sent to other microservices. This is why most developers of microservices are attracted to CQRS—as a way to publish and subscribe to domain events originating from applications outside of a bounded context.
This approach provides us with a mechanism to ensure referential integrity of domain data. Messages from other microservices can be used to handle domain events that allow maintenance of pesky foreign key relationships relating domain data from other records in the distributed system.
Query-side
The event processor is first and foremost responsible for applying a domain event that changes the state of a domain aggregate.
Each domain event can be used to update a database record that results in an incremental materialized view for describing an aggregate. In turn, the query-side will expose a REST API that allows HTTP clients to read the resulting materialized views that were generated from the processed events.
The constraint in the query-side component is that domain data is read-only. All state changes in this system will flow in from the command-side and result in materialized views that can be read on the query-side.
Distributed Monolith or Microservice?
Now if you’re like me, you might be thinking “Hold up! That’s no moon… it’s a space station.”
When most people think of a single microservice today, they think of an independent service component. In most cases, a microservice is built as an application that focuses on doing one thing well. Most importantly, the service can be upgraded and deployed independently of other services.
Now when it comes to the conventional CQRS implementation, because of the separate components, there seems to be a slight concern for calling it a microservice. So it’s worth asking: is this CQRS application considered a microservice? Or rather, could it be what some developers have started to refer to as a distributed monolith?
The answer is tricky, and it depends on who you’re asking. I find that a microservice is all about empowering small independent teams to continuously deliver features as a part of a larger ecosystem of other microservices.
CQRS deployments are complex when compared to most microservice deployments. For a microservices team, the goal is to be able to continuously deliver features into production. Since the separated components of CQRS can still be independently deployed, then we can say that each unit of deployment still satisfies the minimum requirements for independently delivering features into production. One feature of a microservice should always require—at most—one deployable unit.
A distributed monolith happens when a feature is delivered that requires a coordinated deployment of multiple separate components at the same time.