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.

Medium Python 3.10+ Aug 9, 2026 Streaming & messaging 11 views 0 copies

Python code

75 lines
Python 3.10+
from 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

stdout
[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

  1. Use a single `apply_event` switch statement with match-case syntax for cleaner branching
  2. 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

Run this sample

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

Open editor

More from Streaming & messaging

Related tutorials and quizzes for this topic.