Command Query Responsibility Segregation (CQRS) in Scala: Mastering Design Patterns

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 Pattern Separates Different Kinds of Truth

The write side owns:

  • validation
  • invariants
  • accepted commands
  • domain state transitions

The read side owns:

  • query speed
  • view shaping
  • denormalized projections
  • consumer-friendly result formats

Those are different jobs. CQRS helps only when treating them as the same job is already causing pain.

A Typical Scala CQRS Shape

 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.

CQRS Often Works Best With Projections

The write model may be normalized and domain-driven, while the read model is shaped for specific questions:

  • “show me all products low on stock”
  • “show the customer dashboard”
  • “show the current order timeline”

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.

CQRS Is Not the Same as Event Sourcing

The two are related but separate:

  • CQRS separates read and write responsibilities
  • Event Sourcing stores domain changes as events

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.

When It Actually Helps

CQRS is a good fit when:

  • write-side invariants are rich
  • read traffic and write traffic have very different shapes
  • projections materially improve latency or query clarity
  • eventual consistency between write and read sides is acceptable

It is especially common in systems with workflows, dashboards, audit requirements, or high read volume over relatively small write throughput.

When It Just Adds Ceremony

CQRS is usually a bad trade when:

  • the system is basically CRUD
  • read and write models are nearly identical
  • eventual consistency would confuse users
  • the team cannot support the operational complexity of projections and sync lag

A small well-designed service can be more maintainable than a needlessly split architecture.

Common Failure Modes

CQRS Everywhere

The pattern gets applied mechanically to simple forms and lookup endpoints that do not need it.

Ignoring Projection Lag

The read model is treated as if it were instantly current, even though the architecture is explicitly asynchronous.

Thin Commands, Fat Queries, No Domain Model

The code gets the ceremony of CQRS without the benefit because the write side is still just CRUD in disguise.

Practical Heuristics

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.

Knowledge Check

Loading quiz…
Revised on Thursday, April 23, 2026