Designing the Write Path in CQRS
Where the System Actually Begins

In a typical monolith, the read and write sides of the application are entangled — reads feed off the same models the writes mutate. But in CQRS, the write side stands alone. It’s the source of truth. The heartbeat. The origin from which all downstream models flow.
If the write path fails — nothing else matters.
If the write path is designed poorly — everything downstream inherits its flaws.
So before we talk about syncing events or denormalized reads, we need to zoom in on this foundational piece.
This section kicks off with:
What the write path really is in CQRS
The expectations placed on it (consistency, idempotency, isolation)
How commands differ from CRUD, and why this subtlety matters
Why the write path isn't just “the old system without the queries”
Let’s get into it.
The Traits of a Good Write Path
In CQRS, the write path is not just the original system with its SELECTs removed. It’s a precision-built component whose job is to accept commands, validate intent, persist durable change, and emit events for everything else to catch up.
A strong write path is shaped by five core traits:
✅ 1. Intent-first, not Data-first
You don’t say “insert a row in the orders table.”
You say: “PlaceOrder.”
The system — not the user — decides how that maps to persistence.
This keeps the model safe from leakage, and your invariants protected.
✅ 2. Idempotency is Non-Negotiable
Whether it’s retries from clients or message duplication from queues, every write operation must do the same thing every time for the same command.
Idempotency ≠ “ignore duplicates.”
It means: process exactly once, even if delivered multiple times.
✅ 3. Transactional Boundaries Are Clear
A good write path knows its unit of work. You never half-update a customer and leave their invoice dangling.
Either the entire change goes through — or none of it does.
This makes rollback reasoning (and debugging) straightforward.
✅ 4. Event Emission Is a Core Concern
The write model doesn’t just write to the DB — it produces events that fuel the read model and other subsystems.
But these events aren’t side effects — they’re first-class citizens.
Their contracts must be stable, well-versioned, and auditable.
✅ 5. Backpressure-Aware and Operationally Lean
Your write path should fail fast, validate early, and shed load when overwhelmed.
It’s better to reject bad or excessive writes than silently clog queues and downstream processors.
Designing Commands and Write Models
The write path in CQRS doesn’t deal with rows or columns — it deals with commands and aggregates. This is where design discipline kicks in: the separation of what is being requested vs how it should be applied safely and consistently.
Commands: Requests With Intent, Not Instructions
A command is a request to perform an action that mutates state. It’s not a DTO with raw data. It’s not a "please insert" instruction.
Example:
public record PlaceOrderCommand(
UUID customerId,
List<OrderItem> items,
PaymentMethod paymentMethod
) {}
A good command object:
Is explicit: No overloaded flags or boolean toggles.
Is immutable: Prevents mid-flight tampering.
Models business language, not technical mechanics.
Aggregates: Gatekeepers of Invariants
The write model is built around aggregates — transactional consistency boundaries that enforce rules.
A single aggregate:
Owns its own lifecycle (
create,update,delete)Rejects illegal state transitions
Produces events to communicate state change
Example (simplified):
public class Order {
private UUID id;
private OrderStatus status;
private List<OrderItem> items;
public Order(PlaceOrderCommand cmd) {
validate(cmd);
this.id = UUID.randomUUID();
this.items = cmd.items();
this.status = OrderStatus.CREATED;
// Emit: OrderPlacedEvent
}
public void cancel() {
if (this.status != OrderStatus.CREATED)
throw new IllegalStateException("Cannot cancel after fulfillment");
this.status = OrderStatus.CANCELLED;
// Emit: OrderCancelledEvent
}
}
🎯 Key Design Principles
Aggregates enforce invariants locally — you don’t rely on DB constraints alone.
All commands go through aggregates — no bypasses or direct repository hacks.
Each command results in at most one state transition — no batch mutations inside one command.
Emit events before persisting — to enable unit testing and auditability.
Patterns of Persistence in the Write Path
You’ve validated the command. The aggregate accepted the state transition. Now comes the part that kills systems when done wrong: persistence.
The write path must persist:
The new application state (e.g., in a write-optimized DB)
The corresponding domain event(s)
And it must do this with consistency guarantees that avoid the classic “write succeeded, but event was lost” pitfall.
Let’s explore the dominant patterns.
1. Dual Writes (Anti-pattern)
What it is: Save state to DB → separately publish event to Kafka/message broker.
Why it breaks:
No transactional boundary between DB and broker.
If the second step fails, state is updated but the event is lost → read models go out of sync.
Still common? Unfortunately yes — especially in rushed microservices or legacy splits.
2. Transactional Outbox Pattern
What it is:
Instead of publishing directly to Kafka, you write the event to a dedicated outbox table in the same transaction as your domain update.
A separate relay process reads from the outbox and pushes to Kafka.
Why it works:
Strong consistency with app state.
Resilient to crashes and retries — you control reprocessing.
Downsides:
More infra (outbox relayer, deduplication keys).
Eventual dispatch still needs to be monitored.
3. Event Sourcing (Special Case)
What it is:
- The event is the state. You don’t store the final model — you persist the sequence of events that led to it.
Why it works:
Perfect alignment between state and events.
Historical replay, versioning, auditing become native features.
But:
- Not always suitable — rebuild costs, event schema drift, and tooling limitations are real.
4. Append-only Logs with Materialization
Used in high-throughput systems (e.g., order books, IoT ingestion):
Log all writes to a fast, immutable store (e.g., Kafka, EventStoreDB).
Materialize the current state asynchronously using event processors.
Advantage: Write speed, decoupling.
Tradeoff: Read-after-write consistency is sacrificed unless the reader is log-aware.
Real-World Decisions
| Use Case | Recommended Pattern |
| E-commerce order system | Transactional Outbox |
| Payment events | Event Sourcing or Dual with audit |
| IoT ingestion | Append-only logs + materializer |
| Legacy monolith split | Dual Write (but beware) |
Choosing the Right Database for the Write Path
CQRS doesn’t dictate what database you must use. It only says: pick the one that best fits the shape of your writes. And that’s where engineering rigor is either shown — or skipped.
Let’s unpack what this actually means.
Start With the Shape of the Workload
Not all writes are equal. Ask:
How frequent are the writes?
10/s or 10,000/s?
Are writes spiky (flash sales) or steady (IoT sensors)?
What consistency guarantees do you need?
Is exactly-once required?
Is at-least-once tolerable?
Is data mutable or append-only?
Orders mutate (status change).
Logs just grow.
Do you need ACID?
- Single-row vs multi-row vs distributed transactions.
What is the write amplification cost?
- Some systems update indexes, materialized views, constraints — all on write.
Write-Optimized DB Categories (and Their Strengths)
| DB Type | Strengths | Weaknesses |
| Relational (Postgres, MySQL) | Transactions, constraints, familiar tooling | Vertical scaling, joins hurt on scale |
| Document DBs (Mongo, Couchbase) | Flexible schemas, denormalized writes | ACID limited to single doc, schema drift risks |
| Wide-Column (Cassandra, Scylla) | High write throughput, predictable patterns | Poor ad-hoc querying, modeling must be done upfront |
| Key-Value Stores (DynamoDB, Redis) | Extremely fast, simple access paths | No multi-key transactions, limited querying |
| Event Stores (EventStoreDB, Kafka) | Append-only writes, replayability | Complex read-side modeling, harder to evolve schemas |
Thinking Like an Architect
Instead of asking "which DB is fastest?" ask:
Can the DB maintain integrity under concurrency?
How does it behave when a partition occurs?
Is it observable under production pressure (e.g., write lag, tombstones)?
What’s the cost per 1000 writes under sustained load?
Can it support idempotent upserts, retries, and backpressure?
What to Avoid
Defaulting to your favorite DB
Just because you know Mongo doesn't mean it’s the right tool for payment mutations.Choosing based on read-side needs
The write DB must be chosen for command handling, not analytics.Assuming eventual consistency means "it doesn’t matter"
Eventual ≠ sloppy. You need consistency models you can reason about.
How to Choose a Write DB — 6 Real Systems, 6 Tradeoffs
🧾 Note:
These aren’t prescriptions — they’re mindset blueprints.
The right database depends on your app’s real constraints: latency, scale, consistency, and team skillsets. The examples here are to show how architects think, not what everyone should use.
Example 1: Online Retail — Orders, Payments, and Inventory
Workload Shape:
Moderate, bursty write traffic (flash sales, promos)
Strong need for consistency (order placement, stock availability)
Multi-entity transactions: orders, inventory, payments
Data is mutable: status updates, delivery tracking, cancellations
Database Chosen: PostgreSQL
Why:
Full ACID support for multi-table transactions
Strong integrity guarantees (foreign keys, constraints)
Can be scaled with read replicas + partitioning on large order volumes
Works well with event-based CDC tools for CQRS sync
What Was Rejected (and Why):
MongoDB: Easy to start, but handling multi-entity atomicity needs awkward denormalization or two-phase logic
DynamoDB: Great for high throughput, but lacks native ACID across multiple items unless you model very carefully
Cassandra: Too much modeling effort for something that needs relational joins internally
Notes for CQRS:
Write side uses Postgres to handle atomic commands (e.g., createOrder + reserveInventory + initiatePayment)
Read side can project to Redis or Elasticsearch for speed
Example 2: Ride-Sharing Platform — Real-Time Trip, Driver, and Location Events
Workload Shape:
Extremely high write throughput: location pings, trip state changes, fare estimates
Writes are small but frequent (e.g., every 3–5 seconds per driver)
Low write latency is critical — riders must see updates in near real-time
Reads and writes often target the same object (e.g., trip status), but read models can be async
Database Chosen: Apache Cassandra or DynamoDB
Why:
Optimized for high write ingestion at low latency
Scales horizontally — critical when tracking millions of concurrent trips
Tunable consistency levels — can relax reads for speed while guaranteeing writes
Write availability is prioritized over strict read accuracy
What Was Rejected (and Why):
Postgres/MySQL: Can’t handle write velocity without aggressive partitioning and connection pooling gymnastics
MongoDB: Better suited for semi-structured doc updates than time-series writes at this granularity
Elasticsearch: Good for analytics, but not built for fast primary writes
Notes for CQRS:
Write path uses Cassandra with partition keys tuned to trip IDs or driver IDs
Sync layer streams trip events to read DBs (e.g., Redis for live map updates, Elasticsearch for search)
Example 3: Gaming Server — Multiplayer Sessions, State Sync, and Leaderboards
Workload Shape:
Very high concurrency (thousands of players interacting live)
Rapid state mutations: health, ammo, position, cooldown timers
Requires fast reads and writes for in-game logic
Some data is ephemeral (e.g., session state), some needs durability (e.g., match history, leaderboards)
Database Chosen: Redis (for live state) + Postgres (for durable writes)
Why:
Redis offers in-memory speed for per-frame updates with predictable latency
Postgres handles transactional storage of completed matches, player stats, XP progression
This dual system splits the fast game loop from persistent storage — a practical mini-CQRS
What Was Rejected (and Why):
MongoDB: Good for semi-structured data, but RAM-bound working set limits live session scalability
Cassandra: Too eventual; game state needs tighter consistency during interactions
DynamoDB: Viable but can get expensive and requires careful tuning for sub-10ms latencies
Notes for CQRS:
Live state updates flow into Redis directly
Post-game events (kills, score, achievements) are synced to Postgres via event stream
Read model (e.g., leaderboard) is asynchronously projected into Redis or Elasticsearch
Example 4: IoT Fleet Management — Sensors, Telemetry, and Alerts
Workload Shape:
Devices push time-series sensor data every few seconds or minutes
High write frequency, low payload per write (e.g., location, battery, temp)
Read patterns include recent-device summaries, anomaly detection, and aggregates
Writes far outnumber reads, but alerts and dashboards must remain responsive
Database Chosen: TimeScaleDB or InfluxDB
Why:
Purpose-built for time-series ingestion with efficient storage formats and rollups
Native support for downsampling, compression, and time-based retention policies
Can index on device ID and time, enabling fast recent-history lookups
Integrates well with Grafana and alerting pipelines
What Was Rejected (and Why):
Postgres/MySQL vanilla: Requires manual partitioning, indexing, and pruning
MongoDB: Flexible, but falls short for high-ingestion, time-series optimizations
Cassandra: Can ingest fast, but hard to query recent time slices efficiently
Redis: Too memory-bound; not sustainable for multi-TB time-series
Notes for CQRS:
Write path dumps device readings into TimeScaleDB
Read model pulls from materialized aggregates (e.g., last 1h avg per region)
Alerting services consume from a Kafka stream for real-time reactions
Example 5: Social Media Platform — Posts, Likes, Follows, and Fanout Triggers
Write Workload Characteristics:
High velocity writes: user posts, comments, likes, follow/unfollow events
Some writes trigger large-scale fanout (e.g., one post → thousands of followers)
Append-mostly behavior but occasionally involves mutability (like unlikes, deletions)
Event order matters (e.g., a follow before a post should show the post in feed)
Database Chosen for Write Path:
Postgres (if you want strong consistency + relational integrity)
Cassandra (if you're optimizing for scale-first, especially write throughput)
Why These Work:
Postgres: Ideal for enforcing constraints (e.g., no double-likes) and ensuring follow graphs are correct. ACID guarantees help maintain consistency across related entities (e.g., post visibility + user status)
Cassandra: Handles massive write throughput with tunable consistency. Suitable for denormalized, write-once models like append-only activity logs
Why Others Were Rejected:
MongoDB: Subdocuments lead to bloated documents or unbounded growth (e.g., comments array)
DynamoDB: Requires overly careful schema planning with GSIs, LSIs, and time-based writes
Redis: Not sustainable as a primary store — volatile memory, eventual consistency, lacks durability
CQRS Hint:
Write events here often fan out to read models asynchronously, making decoupling essential.
The write DB’s role is to ensure integrity and durability — not to serve feeds.
Example 6: Financial Systems — Transactions, Balances, and Audit Trails
Write Workload Characteristics:
Every write mutates core state: balances, ledger entries, transaction logs
Precision is non-negotiable — no replays, no duplicates, no mismatched balances
Often governed by legal, compliance, or regulatory constraints
All changes must be traceable, timestamped, and ideally immutable (append-only)
Database Chosen for Write Path:
Postgres with audit extensions or double-entry schema
Optionally, CockroachDB or Yugabyte for distributed ACID workloads
Why These Work:
Postgres: Strong ACID guarantees, transactional DDL, support for foreign keys and triggers. Native support for complex constraints, isolation levels, and stored procedures
CockroachDB / Yugabyte: Scale-out Postgres-compatible engines that retain serializability
Why Others Were Rejected:
MongoDB: Multi-document transaction support is recent and fragile at scale
Cassandra: No true ACID — would require complex compensating logic
DynamoDB: Difficult to enforce transactional flows, and audit trails are external
Redis: Zero durability by default, not even in the conversation
CQRS Hint:
The write system is your source of financial truth.
Reads for dashboards or reporting are derived later, through rigorously controlled pipelines — not via direct reads on the write DB.
Closing Thoughts
Picking a write-side database isn't about flavor-of-the-month tech. It’s about mapping your system’s write shape — the volume, structure, consistency demands, and lifecycle of each incoming event — to a storage engine that won’t choke when traffic spikes or edge cases hit.
In CQRS, the write path is the source of truth. If it leaks, lags, or locks under pressure, no read model can save you.
Every example above started with one question:
What is this system trying to persist, and what promises must it keep while doing that?
That question shapes everything else.
Up Next:
We’ve now chosen the right database to capture the truth.
But what happens when that truth needs to be read a million different ways — sliced, aggregated, ranked, or searched in milliseconds?
In the next post, we’ll walk through how teams choose the right read-side database — and why trying to “just reuse the write DB” often backfires.
Stay tuned…






