Stackbook Logo
data-consistency

CQRS Adoption Framework

Intent

Separate write and read models only where their different needs justify the added complexity.

Problem

A single model serving both transactional writes and diverse queries leads to bloated entities, slow joins, and contention between workloads. Conversely, splitting models everywhere introduces eventual consistency and synchronisation code nobody needed. Teams need criteria for when separation is worth the cost.

Forces

    Solution

    CQRS Adoption Framework

    Command Query Responsibility Segregation uses different models to update information and to read it. It is not an architecture for the whole system, and it does not require event sourcing or a message bus. This framework stages adoption so that each step is justified by an observed problem.

    How it works

    Commands express intent, such as ApproveInvoice, and return success or failure, not data. A command handler loads an aggregate, checks invariants, and persists changes. Queries bypass the domain model and read directly from stores shaped for the question. When stores differ, a projector updates the read side from change events, so the read side trails the write side by a small interval.

    Implementation steps

    1. Start with logical separation: distinct command and query handlers over one database.
    2. Introduce read-optimised views or tables when queries become slow or awkward.
    3. Add asynchronous projection, driven by an outbox or change stream, only when load or isolation requires it.
    4. Make projectors idempotent and checkpointed, and keep a command to rebuild any read model.
    5. Handle lag in the interface: return the new resource version from commands and let the client poll or wait for that version on the read side.
    6. Define ownership: each read model has a named consumer and a documented freshness target.

    Common pitfalls

    • Separating stores before measuring a real bottleneck.
    • Letting queries call command handlers or vice versa, which defeats the separation.
    • Ignoring ordering in projections, so out-of-order updates overwrite newer data.
    • Returning stale data right after a write with no user-facing mitigation.
    • Duplicating business rules in projectors.

    Metrics to watch

    • Projection lag, measured as time between commit and read model visibility.
    • Read-after-write miss rate on first query following a command.
    • Projector error and retry rates.
    • Read model rebuild duration.
    • Query latency percentiles on the read side.

    Example configuration

    readModel: order-summary
    source: orders.events
    projector:
      idempotencyKey: eventId
      ordering: perAggregate
      checkpoint: durable
    freshness:
      targetP95Seconds: 2
      alertAfterSeconds: 30
    rebuild:
      allowed: true
      command: rebuild order-summary
    

    Practical rollout

    Treat the decision as reversible by keeping the split logical for as long as possible. Record the observed problem that justifies each increment, such as a query that cannot be indexed well or a read load that threatens write latency. Revisit the decision after a quarter: if lag complaints exceed the performance gains, merge the models again. Teams that document freshness targets with product owners avoid most disputes.

    Key takeaways

    • Split logically first and physically only when measurements demand it.
    • Design the user experience for read lag, not just the backend.
    • Keep projectors idempotent, ordered, and rebuildable.
    • Apply CQRS to specific components, not as a blanket rule.

    ✓ When to Use

    • •Read and write workloads have very different scale or shape
    • •Complex domain logic on writes contrasts with simple, fast reads
    • •Multiple read representations are needed from the same data
    • •Reads must scale independently of writes

    ✗ When Not to Use

    • •The application is straightforward CRUD
    • •Users require strict read-your-writes everywhere and cannot tolerate lag
    • •The team cannot operate the extra synchronisation machinery

    Pros

    • +Read models fit each query without compromising the write model
    • +Independent scaling of reads and writes
    • +Clearer command validation and domain logic
    • +Enables specialised stores such as search or caches

    Cons

    • −Eventual consistency between sides confuses users and developers
    • −More code, more moving parts, and more failure modes
    • −Projection bugs create subtle data divergence
    • −Debugging spans several stores

    Cost Profile

    Infrastructure

    Operational

    Cognitive

    Failure Modes

      Alternatives

        Related Patterns

          Competency Domains

          data statedistribution communicationeconomics evolution