design·lab

Architecture

CQRS

Split your data model in two: a command side that writes and validates, and a query side that reads — each shaped for its own job.

✗ The problem

One model, two jobs that fight each other

A single model must serve heavy writes and complex reads. Read-optimized indexes slow down writes; a write-normalized schema forces reads into expensive joins. You can't scale, tune, or shape them separately.

class OrderModel {
  // heavy write validation…
  save(order) { /* normalize, validate */ }

  // …fights a read-optimized query
  getDashboard() {
    return db.join(orders, items, users) // slow ✗
             .groupBy(...);
  }
}
Writes
One Model
read + write
Reads
Same table, opposite needs: one side wants normalized rows, the other wants pre-joined, denormalized views. Optimizing one degrades the other.
✓ How it works

Separate command side from query side

commands validate and change state on the write model. Writes emit an event that projects the change into a separate, denormalized read model. queries only ever read from it — they never mutate anything.

class PlaceOrder { // command
  execute(cmd) {
    write.save(cmd);
    emit('OrderPlaced', cmd);
  }
}

class Dashboard { // query
  on('OrderPlaced', e =>
    readModel.project(e));  // update view

  get() { return readModel.summary; }
}
Command
Place Order
Write Model
validate + mutate
↓ event
Read Model
denormalized (projection)
Query
Get Dashboard
✓ See it live

Place a command — query the projection

Place order writes to the write store, then an event projects it into the read model. Query dashboard always reads the read model — instantly, but it may lag behind by a beat.

📝 Write store (normalized)

no orders yet
event

📊 Read model (denormalized)

no orders yet

no orders yet

✓ Takeaway

Two models, each shaped for its job

  • Scale independently: add read replicas or write shards without touching the other side.
  • Tailor each model: normalized for safe writes, denormalized for fast reads — no compromise.
  • Clearer code: commands mutate, queries read — no method secretly does both.
  • Caution: more moving parts and eventual consistency between the two sides. Only reach for CQRS in high-scale or complex domains — plain CRUD is fine otherwise (YAGNI).
🎯 Combines / relates: names come from Command (writes) vs queries; driven by events / pub-sub; frequently paired with Event Sourcing.