Advantages of Separating Command & Query Responsibilities for a data model
Let’s take a fictitious e-commerce site, implemented using microservices based architecture
There is a UI layer or web app which communicates with the Product service to get information on products. When a user places an order it sends the information to the Order service. The order service then calls the Payment service to charge the credit card of the user.
Let’s consider only the Product service:
The product service has its own database. Product service does many number of reads when compared to the number of writes. One of the drawbacks of relational databases is that while updating a resource, relational databases use 2 phase lock mechanism to ensure serializability. The two locks on a resource are
- Write lock: This lock doesn’t allow any other transaction to have another write lock or read lock on the same resource
- Read lock: This lock doesn’t allow other transactions to have a write lock on the resource but can have a read lock on the same resource.
So when a resource is being updated all the other read requests are queued. This is not efficient, as most of the requests are read. Generally, the write logic is more complicated as it has a lot of data and business validations, whereas the read operations are very simple.
Using CQRS ( Command Query Responsibility Segregation )
We could separate the read and write responsibilities to separate databases. The writes are called Commands and the reads are called Queries. CQRS ( Command Query Responsibility Segregation ) pattern is separating these to different databases. There are different ways of implementing this, let’s continue to take the example of Product service in our fictitious e-commerce web app:
- We could create different ORM models, one for handling queries and another for handling writes. Both these ORM models could be communicating with the same database. The advantage of this approach is that we can handle security more easily. But the performance remains the same.
- Another way is to split the Product service into 2 services each with their own database, one for performing read and another for performing writes. When an update is written to the write database, the changes are pushed to the read database.
- We can scale the read and write databases independently of each other
- The write database can be normalized to 3rd Normal Form to make writes efficient
- The read databases can be denormalized the data to suit specific queries and need not perform complex operations like JOIN tables to return the required data.
- Managing security and permissions is easy
- It makes queries simple
- It forces us to change the way we think, UI sends a command to the write model and not a data model object
- We can use a relational database for the write side and use NoSQL database for the read side. Scaling a NoSQL database is relatively easy.
- It makes the whole system complex
- Data can be stale
- Handling eventually consistent data is a monster of its own.
Points to consider before implementation
- This is not a system-wide or high-level pattern. It should be applied only in a bounded context where it makes sense.
- The data will be eventually consistent
- It works well with event sourcing pattern
- It is useful in systems where multiple systems/actors perform parallel actions on the same data.
- It shouldn’t be used for applications which are simple CRUD based.
- The write model is always the source of truth. So we could query the write model and return data as the result of command execution.
- It is not simple to handle eventual-consistent data.