How to mock a CQRS projector read model update in Python
Build a CQRS projector class that maintains denormalized read models by applying domain events in a mock order-processing service.
Python code
75 linesfrom dataclasses import dataclass, field
from typing import Dict, List, Optional
@dataclass
class OrderReadModel:
order_id: str
customer_name: str
total: float
status: str = "pending"
items: List[Dict] = field(default_factory=list)
def apply_event(self, event_type: str, payload: Dict) -> None:
"""Apply a domain event to update the read model (projection)."""
if event_type == "OrderPlaced":
self.status = "placed"
self.items = payload.get("items", [])
self.total = payload.get("total", 0.0)
elif event_type == "OrderShipped":
self.status = "shipped"
elif event_type == "OrderCancelled":
self.status = "cancelled"
def __repr__(self) -> str:
return f"OrderReadModel({self.order_id}, {self.status}, ${self.total}, {len(self.items)} items)"
class OrderProjector:
"""CQRS projector — maintains a denormalized read model from domain events."""
def __init__(self) -> None:
self._read_models: Dict[str, OrderReadModel] = {}
def project(self, event: Dict) -> None:
event_type = event["type"]
order_id = event["order_id"]
if event_type == "OrderPlaced":
self._read_models[order_id] = OrderReadModel(
order_id=order_id,
customer_name=event["customer_name"],
total=0.0,
)
read_model = self._read_models.get(order_id)
if read_model:
read_model.apply_event(event_type, event)
print(f"[Projector] Applied {event_type} → {read_model}")
def get_order(self, order_id: str) -> Optional[OrderReadModel]:
return self._read_models.get(order_id)
if __name__ == "__main__":
projector = OrderProjector()
events = [
{
"type": "OrderPlaced",
"order_id": "ord-001",
"customer_name": "Alice",
"items": [{"sku": "A1", "qty": 2}],
"total": 49.99,
},
{"type": "OrderShipped", "order_id": "ord-001"},
{"type": "OrderPlaced", "order_id": "ord-002", "customer_name": "Bob", "items": [], "total": 10.00},
]
for event in events:
projector.project(event)
print("\nRead model snapshot:")
for oid in ["ord-001", "ord-002", "ord-003"]:
order = projector.get_order(oid)
print(f"{oid}: {order if order else 'NOT FOUND'}")
Output
[Projector] Applied OrderPlaced → OrderReadModel(ord-001, placed, $49.990000000000005, 1 items)
[Projector] Applied OrderShipped → OrderReadModel(ord-001, shipped, $49.990000000000005, 1 items)
[Projector] Applied OrderPlaced → OrderReadModel(ord-002, placed, $10, 0 items)
Read model snapshot:
ord-001: OrderReadModel(ord-001, shipped, $49.990000000000005, 1 items)
ord-002: OrderReadModel(ord-002, placed, $10, 0 items)
ord-003: NOT FOUND
How it works
The OrderReadModel dataclass holds denormalized order state while apply_event mutates it based on the event type. The OrderProjector stores read models in a dictionary keyed by order ID, recreating a fresh model on OrderPlaced and updating it on subsequent events. Using field(default_factory=list) avoids the mutable default argument pitfall. The mock events simulate a stream of domain events that the projector consumes in order, making it easy to test state transitions in isolation.
Common mistakes
- Forgetting to create a new read model for each new order ID before applying events
- Using a mutable default like `items=[]` in the dataclass instead of `field(default_factory=list)`
- Not handling unknown event types gracefully, causing silent failures
- Assuming read models exist for lookup without checking for missing order IDs
Variations
- Use a single `apply_event` switch statement with match-case syntax for cleaner branching
- Persist the read model to a database or cache (like Redis) instead of an in-memory dict
Real-world use cases
- Maintaining a real-time order dashboard in e-commerce that reflects latest status from Kafka events
- Building a materialized view for search queries by projecting product events into an Elasticsearch index
- Testing event-driven microservices by simulating a stream of domain events against a mock projector
Sponsored
More from Streaming & messaging
- At Most Once Fire-and-Forget Mock in Python easy
- Batch Consume Process Commit Pattern in Python medium
- Build a Streaming Messaging Helper in Python easy
- Dead Letter Queue Failed Messages List Mock in Python easy
- Dedupe processed message IDs in Python easy
- Event Envelope with Schema Version Field in Python easy
Keep learning
Related tutorials and quizzes for this topic.