Full-Stack Architecture Patterns

CQRS and Event Sourcing: Separate the Decisions

Understand command/query separation, event streams, projection lag and concurrency before choosing a more complex persistence model.

CQRS and Event Sourcing: Separate the Decisions

Two independent choices

CQRS separates the model used to change state from the model used to read it. That separation can exist inside one application and one database. Separate databases, asynchronous projections and independently scaled services are further choices, not automatic consequences.

Event sourcing makes an ordered stream of domain events the authoritative record from which state is derived. It is often combined with CQRS, but neither pattern requires the other. CQRS guidance, event sourcing guidance.

Describe a command's contract

A command expresses an intended change, such as reserving seats. Its handler checks authorization and domain rules before committing the result. It can return an acknowledgement, identifier or operation status; forbidding all return values is not a useful API requirement.

A query returns a representation appropriate to a use case. It should not perform business mutations merely because someone refreshes the page. The read implementation may use ordinary tables, views or a separately maintained projection.

Begin with the simplest split that solves the problem. A class named “QueryService” does not, by itself, improve database performance.

A small event sequence

Consider an illustrative reservation stream:

SequenceEventDerived state
1ReservationOpened with two seatsTwo seats held
2ReservationConfirmedConfirmed
3ReservationCancelledCancelled; release handled by the domain workflow

Events should carry stable identifiers and an explicit schema version. A sequence number provides ordering within the stream. A timestamp alone is not a concurrency-control mechanism.

If two handlers both read sequence 2, the store must prevent both from blindly appending incompatible sequence-3 changes. An expected-version append can reject the stale writer, which then reloads and reevaluates the command.

Projections are recoverable only by design

An order-history projection may process events asynchronously. Its lag is observable state, not a guaranteed few milliseconds. After a command succeeds, the UI can show the accepted operation and wait for a known version, rather than immediately displaying an old read as evidence that the change failed.

A projection consumer needs a durable checkpoint and duplicate handling. Applying an event and advancing the checkpoint must have a consistent recovery strategy. Replaying history should rebuild the view without sending old emails or charging customers again.

Snapshots can shorten reconstruction, but must identify the stream version they represent. Test that loading a snapshot plus later events produces the same result as a complete replay.

History still needs governance

An append-only log can support investigation, but audit completeness also depends on which actions are recorded, who can alter storage, retention and access controls. It does not establish regulatory compliance automatically.

Personal information embedded in long-lived events creates additional retention and erasure design questions. Decide what belongs in the stream and what should be referenced from separately managed records.

Choose event sourcing when historical reconstruction or domain behavior justifies those costs. For a straightforward form-and-list application, conventional transactions with a deliberate audit mechanism may meet the requirement more directly.

Sardar Mukhshaf

Sardar Mukhshaf

Full-Stack & DevOps Engineer

I build scalable web applications, secure backend systems, and production-ready cloud infrastructure. My engineering focus spans interface design, API architecture, deployment pipelines, and observability. Follow for more deep dives into system architecture, performance engineering, and emerging technologies.

More from the practice

Keep the useful ideas close.

Follow new articles with your preferred RSS reader.

Open the RSS feed ↗