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.
Python code
57 linesfrom 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
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
- Use an event store: write model appends events, read model derives projections from them
- 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
More from Database scaling & optimization
- Approximate Count with HyperLogLog in Python medium
- B-Tree Insert and In-Order Traversal in Python hard
- Broadcast a Small Reference Table in Python easy
- Build a Full Text Search Index in Python medium
- Build a Partial Index Mock in Python for Database Filtering easy
- Composite index leftmost prefix in Python medium
Keep learning
Related tutorials and quizzes for this topic.