Explore CQRS in Scala as a separation of write-side invariants and read-side projections, with guidance on when it pays off and when it only adds complexity.
CQRS (Command Query Responsibility Segregation): An architectural pattern that separates the write side of a system, where invariants are enforced, from the read side, where data is shaped for retrieval.
CQRS is not about creating a command and a query class for every CRUD endpoint. It is useful only when read and write concerns are meaningfully different. In Scala, it becomes most valuable when the write side is modeled explicitly as commands and domain behavior, while the read side is optimized as a projection or view tailored to the questions the system needs to answer.
The write side owns:
The read side owns:
Those are different jobs. CQRS helps only when treating them as the same job is already causing pain.
1sealed trait InventoryCommand
2final case class AddProduct(id: String, name: String, quantity: Int) extends InventoryCommand
3final case class AdjustQuantity(id: String, delta: Int) extends InventoryCommand
4
5final case class ProductView(id: String, name: String, quantity: Int)
6
7trait CommandHandler[C]:
8 def handle(command: C): Either[String, Unit]
9
10trait QueryHandler[Q, R]:
11 def handle(query: Q): R
The important point is not the traits themselves. It is that commands and queries serve different purposes and can evolve differently.
The write model may be normalized and domain-driven, while the read model is shaped for specific questions:
That means the read side might not mirror the write-side model at all. It may be denormalized, cached, or rebuilt from events or change feeds.
The separation looks like this:
flowchart TB
Client["Client"] --> CommandApi["Command API"]
CommandApi --> WriteModel["Write Model / Command Handler"]
WriteModel --> Events["Events or State Changes"]
Events --> Projection["Projection Updater"]
Projection --> ReadModel["Read Model"]
Client --> QueryApi["Query API"]
QueryApi --> ReadModel
The main thing to notice is that the read side does not need to ask the write side to answer every question in real time. It can consume a projection that was built for retrieval.
The two are related but separate:
They are often paired, but neither requires the other. You can use CQRS with ordinary persistence, and you can use event sourcing without building a separate read stack for every feature.
CQRS is a good fit when:
It is especially common in systems with workflows, dashboards, audit requirements, or high read volume over relatively small write throughput.
CQRS is usually a bad trade when:
A small well-designed service can be more maintainable than a needlessly split architecture.
The pattern gets applied mechanically to simple forms and lookup endpoints that do not need it.
The read model is treated as if it were instantly current, even though the architecture is explicitly asynchronous.
The code gets the ceremony of CQRS without the benefit because the write side is still just CRUD in disguise.
Use CQRS when write-side invariants and read-side access patterns are meaningfully different. Keep the split purposeful, make projection lag explicit, and do not assume event sourcing is mandatory. If the architecture does not buy clearer invariants or better retrieval, it is probably not earning its extra moving parts.