LabHub

Blog

CQRS Pattern Practical Implementation Guide

한국어English日本語

CQRS Pattern Implementation

1. What Is CQRS?

CQRS (Command Query Responsibility Segregation) is an architectural pattern that separates data writes (Commands) and reads (Queries) into distinct models.

Traditional CRUD vs CQRS

In traditional CRUD, a single model handles both reads and writes. CQRS separates them so each can be optimized independently.

Traditional CRUD:
  Client → [Same Model] → Database

CQRS:
  Client (Write) → [Command Model] → Write DB
  Client (Read)  → [Query Model]  → Read DB

When Should You Apply CQRS?

2. Implementing the Command Model

Defining Commands

from dataclasses import dataclass
from datetime import datetime
from uuid import UUID, uuid4

# Command objects
@dataclass(frozen=True)
class CreateOrderCommand:
    customer_id: UUID
    items: list[dict]
    shipping_address: str

@dataclass(frozen=True)
class CancelOrderCommand:
    order_id: UUID
    reason: str

# Domain Event
@dataclass(frozen=True)
class OrderCreatedEvent:
    event_id: UUID
    order_id: UUID
    customer_id: UUID
    items: list[dict]
    total_amount: float
    created_at: datetime

Command Handler

from typing import Protocol

class EventStore(Protocol):
    def append(self, stream_id: str, events: list) -> None: ...
    def load(self, stream_id: str) -> list: ...

class OrderCommandHandler:
    def __init__(self, event_store: EventStore, event_bus):
        self.event_store = event_store
        self.event_bus = event_bus

    def handle_create_order(self, cmd: CreateOrderCommand):
        order_id = uuid4()
        total = sum(item['price'] * item['quantity']
                    for item in cmd.items)

        # Business rule validation
        if total <= 0:
            raise ValueError("Order total must be positive")
        if not cmd.items:
            raise ValueError("Order must have at least one item")

        # Create event
        event = OrderCreatedEvent(
            event_id=uuid4(),
            order_id=order_id,
            customer_id=cmd.customer_id,
            items=cmd.items,
            total_amount=total,
            created_at=datetime.utcnow(),
        )

        # Store + publish event
        self.event_store.append(f"order-{order_id}", [event])
        self.event_bus.publish("order.created", event)

        return order_id

    def handle_cancel_order(self, cmd: CancelOrderCommand):
        # Restore state from existing events
        events = self.event_store.load(f"order-{cmd.order_id}")
        order = Order.from_events(events)

        if order.status == "cancelled":
            raise ValueError("Order already cancelled")

        cancel_event = OrderCancelledEvent(
            event_id=uuid4(),
            order_id=cmd.order_id,
            reason=cmd.reason,
            cancelled_at=datetime.utcnow(),
        )

        self.event_store.append(
            f"order-{cmd.order_id}", [cancel_event]
        )
        self.event_bus.publish("order.cancelled", cancel_event)

3. Implementing the Query Model

The Query model uses a separate data store optimized for reads.

from dataclasses import dataclass

# Read-only model (Materialized View)
@dataclass
class OrderSummary:
    order_id: str
    customer_name: str
    item_count: int
    total_amount: float
    status: str
    created_at: str

class OrderQueryHandler:
    def __init__(self, read_db):
        self.read_db = read_db

    def get_order(self, order_id: str) -> OrderSummary:
        row = self.read_db.execute(
            "SELECT * FROM order_summaries WHERE order_id = %s",
            (order_id,)
        )
        return OrderSummary(**row)

    def list_orders_by_customer(
        self, customer_id: str, limit: int = 20
    ) -> list[OrderSummary]:
        rows = self.read_db.execute(
            """SELECT * FROM order_summaries
               WHERE customer_id = %s
               ORDER BY created_at DESC
               LIMIT %s""",
            (customer_id, limit)
        )
        return [OrderSummary(**r) for r in rows]

    def search_orders(
        self, status: str = None, min_amount: float = None
    ) -> list[OrderSummary]:
        query = "SELECT * FROM order_summaries WHERE 1=1"
        params = []
        if status:
            query += " AND status = %s"
            params.append(status)
        if min_amount:
            query += " AND total_amount >= %s"
            params.append(min_amount)
        return self.read_db.execute(query, params)

4. Event Sourcing Integration

Implementing the Event Store

import json
from datetime import datetime

class PostgresEventStore:
    def __init__(self, conn):
        self.conn = conn

    def create_table(self):
        self.conn.execute("""
            CREATE TABLE IF NOT EXISTS event_store (
                id          BIGSERIAL PRIMARY KEY,
                stream_id   VARCHAR(255) NOT NULL,
                event_type  VARCHAR(255) NOT NULL,
                event_data  JSONB NOT NULL,
                metadata    JSONB DEFAULT '{}',
                version     INTEGER NOT NULL,
                created_at  TIMESTAMP DEFAULT NOW(),
                UNIQUE(stream_id, version)
            );
            CREATE INDEX IF NOT EXISTS idx_event_stream
                ON event_store(stream_id, version);
        """)

    def append(self, stream_id: str, events: list):
        current = self._get_latest_version(stream_id)
        for i, event in enumerate(events):
            version = current + i + 1
            self.conn.execute(
                """INSERT INTO event_store
                   (stream_id, event_type, event_data, version)
                   VALUES (%s, %s, %s, %s)""",
                (stream_id, type(event).__name__,
                 json.dumps(event.__dict__, default=str),
                 version)
            )

    def load(self, stream_id: str) -> list:
        rows = self.conn.execute(
            """SELECT event_type, event_data FROM event_store
               WHERE stream_id = %s ORDER BY version""",
            (stream_id,)
        )
        return [self._deserialize(r) for r in rows]

    def _get_latest_version(self, stream_id: str) -> int:
        result = self.conn.execute(
            "SELECT MAX(version) FROM event_store WHERE stream_id = %s",
            (stream_id,)
        )
        return result or 0

5. Event Synchronization with Kafka

from confluent_kafka import Producer, Consumer
import json

class KafkaEventBus:
    def __init__(self, bootstrap_servers: str):
        self.producer = Producer({
            'bootstrap.servers': bootstrap_servers,
            'acks': 'all',
            'enable.idempotence': True,
        })

    def publish(self, topic: str, event):
        self.producer.produce(
            topic=topic,
            key=str(event.order_id).encode(),
            value=json.dumps(
                event.__dict__, default=str
            ).encode(),
        )
        self.producer.flush()


class ReadModelProjector:
    """Consumes events and updates the Read DB"""

    def __init__(self, read_db, bootstrap_servers: str):
        self.read_db = read_db
        self.consumer = Consumer({
            'bootstrap.servers': bootstrap_servers,
            'group.id': 'read-model-projector',
            'auto.offset.reset': 'earliest',
            'enable.auto.commit': False,
        })
        self.consumer.subscribe([
            'order.created', 'order.cancelled'
        ])

    def run(self):
        while True:
            msg = self.consumer.poll(1.0)
            if msg is None:
                continue
            event = json.loads(msg.value())
            topic = msg.topic()

            if topic == 'order.created':
                self._project_order_created(event)
            elif topic == 'order.cancelled':
                self._project_order_cancelled(event)

            self.consumer.commit()

    def _project_order_created(self, event):
        self.read_db.execute(
            """INSERT INTO order_summaries
               (order_id, customer_id, item_count,
                total_amount, status, created_at)
               VALUES (%s, %s, %s, %s, %s, %s)""",
            (event['order_id'], event['customer_id'],
             len(event['items']), event['total_amount'],
             'active', event['created_at'])
        )

    def _project_order_cancelled(self, event):
        self.read_db.execute(
            """UPDATE order_summaries
               SET status = 'cancelled'
               WHERE order_id = %s""",
            (event['order_id'],)
        )

6. API Layer

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

class CreateOrderRequest(BaseModel):
    customer_id: str
    items: list[dict]
    shipping_address: str

# Command endpoints (POST/PUT/DELETE)
@app.post("/orders")
async def create_order(req: CreateOrderRequest):
    cmd = CreateOrderCommand(
        customer_id=UUID(req.customer_id),
        items=req.items,
        shipping_address=req.shipping_address,
    )
    order_id = command_handler.handle_create_order(cmd)
    return {"order_id": str(order_id), "status": "created"}

# Query endpoints (GET)
@app.get("/orders/{order_id}")
async def get_order(order_id: str):
    result = query_handler.get_order(order_id)
    if not result:
        raise HTTPException(status_code=404)
    return result

@app.get("/customers/{customer_id}/orders")
async def list_customer_orders(customer_id: str, limit: int = 20):
    return query_handler.list_orders_by_customer(customer_id, limit)

7. Quiz

Q1: Why does Eventual Consistency occur in CQRS, and what are the strategies to handle it?

There is a time lag between when a Command saves to the Write DB and when the event is reflected in the Read DB via Kafka. Strategies include:

Read-your-writes: Read data you just wrote directly from the Write DB Polling: The client retries until the Read DB is updated Versioning: Track event versions to verify whether data is up to date WebSocket/SSE: Real-time notifications when updates are complete

Q2: How do you handle event schema changes in Event Sourcing?

Use the Event Upcasting pattern:

Add a version field to events (v1, v2, ...) Write upcasters that transform older version events to the latest schema when loading Never modify existing events (immutability principle)

Alternatively, you can use snapshots to avoid replaying all events.

Q3: When should you NOT apply CQRS?

Simple CRUD apps: When the read/write models are nearly identical Strong consistency is mandatory: When eventual consistency is unacceptable, such as real-time balance checks Small projects: When the complexity of CQRS outweighs its benefits Lack of team experience: Risky to adopt without experience in event sourcing and message queues

Quiz

Q1: What is the main topic covered in "CQRS Pattern Practical Implementation Guide"?

A comprehensive guide to the CQRS pattern — from the principles of Command/Query separation to Event Sourcing integration, Kafka usage, and hands-on implementation examples with Spring Boot.

Q2: What Is CQRS?? CQRS (Command Query Responsibility Segregation) is an architectural pattern that separates data writes (Commands) and reads (Queries) into distinct models. Traditional CRUD vs CQRS In traditional CRUD, a single model handles both reads and writes.

Q3: Explain the core concept of Implementing the Command Model. Defining Commands Command Handler

Q4: What are the key aspects of Implementing the Query Model? The Query model uses a separate data store optimized for reads.

Q5: How does Event Sourcing Integration work? Implementing the Event Store

Comments

No comments yet.

Sign in to leave a comment