How to Implement CQRS with Separate Read and Write Models in Python

Implements Command Query Responsibility Segregation (CQRS) by splitting data into separate write and read models with dedicated repositories, using dataclasses for structure.

Medium Python 3.9+ Aug 9, 2026 System design patterns 14 views 0 copies

Python code

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


@dataclass
class OrderWriteModel:
    order_id: int
    customer: str
    items: List[str] = field(default_factory=list)

    def add_item(self, item: str) -> None:
        self.items.append(item)


@dataclass
class OrderReadModel:
    order_id: int
    customer: str
    item_count: int
    total_price: float


class OrderWriteRepository:
    def __init__(self) -> None:
        self._orders: Dict[int, OrderWriteModel] = {}

    def save(self, order: OrderWriteModel) -> None:
        self._orders[order.order_id] = order

    def get(self, order_id: int) -> Optional[OrderWriteModel]:
        return self._orders.get(order_id)


class OrderReadRepository:
    def __init__(self) -> None:
        self._orders: Dict[int, OrderReadModel] = {}

    def save(self, order: OrderReadModel) -> None:
        self._orders[order.order_id] = order

    def get_summary(self, order_id: int) -> Optional[OrderReadModel]:
        return self._orders.get(order_id)

    def all_summaries(self) -> List[OrderReadModel]:
        return list(self._orders.values())


class OrderService:
    PRICE_PER_ITEM = 10.0

    def __init__(self, write_repo: OrderWriteRepository, read_repo: OrderReadRepository) -> None:
        self.write_repo = write_repo
        self.read_repo = read_repo

    def place_order(self, order_id: int, customer: str, items: List[str]) -> None:
        order = OrderWriteModel(order_id=order_id, customer=customer, items=items)
        self.write_repo.save(order)

        read_model = OrderReadModel(
            order_id=order_id,
            customer=customer,
            item_count=len(items),
            total_price=len(items) * self.PRICE_PER_ITEM,
        )
        self.read_repo.save(read_model)


def main() -> None:
    write_repo = OrderWriteRepository()
    read_repo = OrderReadRepository()
    service = OrderService(write_repo, read_repo)

    service.place_order(1, "Alice", ["Laptop", "Mouse", "Keyboard"])
    service.place_order(2, "Bob", ["Monitor"])

    for order in read_repo.all_summaries():
        print(f"Order {order.order_id} by {order.customer}: "
              f"{order.item_count} items, total ${order.total_price:.2f}")

    write_order = write_repo.get(1)
    print(f"Write model items for order 1: {write_order.items}")


if __name__ == "__main__":
    main()

Output

stdout
Order 1 by Alice: 3 items, total $30.00
Order 2 by Bob: 1 items, total $10.00
Write model items for order 1: ['Laptop', 'Mouse', 'Keyboard']

How it works

OrderWriteModel and OrderReadModel are dataclasses that represent the two CQRS sides: write models handle commands (adding items) while read models are optimized for queries (counts, totals). The OrderWriteRepository and OrderReadRepository store each model separately, so writes and reads evolve independently. OrderService acts as the command handler, updating both repositories to keep them eventually consistent. Using two repositories supports scaling reads separately from writes in larger applications.

Common mistakes

  • Mixing read and write models into one class, losing the separation benefits.
  • Forgetting to update the read model when the write model changes, causing stale summaries.
  • Using the write repository for query operations, which reduces performance for read-heavy workloads.

Variations

  1. Use a message queue or event bus to asynchronously sync read models after writes.
  2. Store read models in a denormalized table or cache like Redis for faster queries.

Real-world use cases

  • E-commerce checkout services where order placement writes detailed data while dashboards read precomputed summaries.
  • Reporting systems that aggregate millions of events into read-optimized projections.
  • Multi-tenant SaaS platforms needing separate read replicas for scaling queries independently of writes.

Sponsored

Run this sample

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

Open editor

More from System design patterns

Related tutorials and quizzes for this topic.