Mock CQRS Read/Write Split in Python

Separate order mutations from queries using a read model and write model to mock CQRS-style separation of concerns.

Medium Python 3.9+ Aug 9, 2026 Database scaling & optimization 12 views 0 copies

Python code

57 lines
Python 3.9+
from dataclasses import dataclass, field
from typing import List, Dict


@dataclass
class Order:
    id: int
    amount: float
    status: str = "pending"


class OrderWriteModel:
    """Handles all mutations (writes) to orders."""

    def __init__(self):
        self._orders: Dict[int, Order] = {}
        self._next_id = 1

    def create_order(self, amount: float) -> Order:
        order = Order(id=self._next_id, amount=amount)
        self._orders[order.id] = order
        self._next_id += 1
        return order

    def update_status(self, order_id: int, new_status: str) -> None:
        order = self._orders[order_id]
        order.status = new_status


class OrderReadModel:
    """Handles all queries (reads) - separate from writes."""

    def __init__(self, write_model: OrderWriteModel):
        self._orders = write_model._orders  # shared source of truth

    def get_order(self, order_id: int) -> Order:
        return self._orders[order_id]

    def list_pending(self) -> List[Order]:
        return [o for o in self._orders.values() if o.status == "pending"]


if __name__ == "__main__":
    write_model = OrderWriteModel()
    read_model = OrderReadModel(write_model)

    # Write operation
    order = write_model.create_order(amount=125.50)
    print("Created order:", order.id)

    # Read operation (separate model)
    fetched = read_model.get_order(order.id)
    print("Fetched order amount:", fetched.amount)

    # Write another update
    write_model.update_status(order.id, "completed")
    print("Pending orders after update:", [o.id for o in read_model.list_pending()])

Output

stdout
Created order: 1
Fetched order amount: 125.5
Pending orders after update: []

How it works

The write model owns the order dictionary as its single source of truth, while the read model gets a reference to that same dictionary. Mutations only go through the write model's methods, keeping writes explicit and auditable. Reads go through the read model, which only exposes query methods like get_order and list_pending — the read side never mutates state. This mirrors how CQRS separates command and query paths even in an in-process mock.

Because both models share the underlying dict, reads always see the latest committed writes — a simple consistency model. The pattern is ideal for prototyping before introducing a real database or event-sourced storage.

Common mistakes

  • Letting the read model mutate the shared dictionary instead of keeping it read-only
  • Splitting data storage into two copies instead of sharing the source of truth
  • Forgetting that `get_order` raises `KeyError` for missing IDs without a fallback
  • Placing business rules in the read model, which should only shape queries

Variations

  1. Use an event store: write model appends events, read model derives projections from them
  2. Add `read_model.get_order_or_none(order_id)` returning `None` to avoid `KeyError` on missing IDs

Real-world use cases

  • Prototyping how an e-commerce backend will separate order mutations from query projections before choosing a database.
  • Teaching newcomers how to split command and query responsibilities in an existing monolith without rewriting infrastructure.
  • Designing a local in-memory layer for tests that mimics a CQRS production system when mocking external services.

Sponsored

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Database scaling & optimization

Related tutorials and quizzes for this topic.