Skip to content

CQRS

Command Query Responsibility Segregation separates write behavior from read behavior.

This page shows how ForgingBlocks concepts can be projected to support a CQRS-style design.

Important

ForgingBlocks does not require CQRS.
This page presents it as an architectural pattern, not a requirement.


Quick summary

Command Query Responsibility Segregation (CQRS) separates write behavior from read behavior. This page shows how ForgingBlocks concepts can be projected to support CQRS — not required.

Mapping:
- Commands — Express intent to change state (Command, CommandHandlerPort, WriteOnlyRepositoryPort)
- Queries — Retrieve information (Query, QueryHandlerPort, ReadOnlyRepositoryPort)
- Models may diverge over time (separate read/write stores with replication)

Fits when: read/write workloads differ significantly; scalability dominates; eventual consistency acceptable.


Conceptual mapping

  • Commands express intent to change state.
  • Queries retrieve information.
  • Read and write responsibilities are separated.
  • Models may diverge over time.

The diagram below shows a canonical CQRS view from the literature.

---
title: CQRS (Command Query Responsibility Segregation)
---
graph LR
    Client -->|send| CommandHandlerPort[Command Handler]
    Client -->|fetch| QueryHandlerPort[Query Handler]
    CommandHandlerPort -->|save| WriteStore[Write Store<br/>RepositoryPort]
    QueryHandlerPort -->|get_by_id/list_all| ReadStore[Read Store<br/>ReadOnlyRepositoryPort]
    WriteStore -.->|replicate| ReadStore

ForgingBlocks in practice

1. Command and CommandHandler — write side

Commands express intent to change state. The handler uses a write-only
repository to persist the aggregate.

# === Command ===
from forging_blocks.domain.messages.command import Command
from forging_blocks.domain.messages.decorators import command_dataclass


@command_dataclass
class CreateOrder(Command[dict[str, object]]):
    customer_id: str
    items: list[str]


# === CommandHandler ===
from forging_blocks.application.ports.inbound import CommandHandlerPort
from forging_blocks.application.ports.outbound import WriteOnlyRepositoryPort


class CreateOrderHandler(CommandHandlerPort[dict[str, object]]):
    def __init__(self, order_repo: WriteOnlyRepositoryPort["Order", str]) -> None:
        self._order_repo = order_repo

    async def handle(self, command: CreateOrder) -> None:
        order = Order.create(command.customer_id, command.items)
        await self._order_repo.save(order)

2. Query and QueryHandler — read side

Queries retrieve data without side effects. The handler reads from a
dedicated read-only repository, which may target a separate read model.

# === Query ===
from forging_blocks.domain.messages.decorators import query_dataclass
from forging_blocks.domain.messages.query import Query


@query_dataclass
class GetOrderSummary(Query[dict[str, object]]):
    order_id: str


# === QueryHandler ===
from dataclasses import dataclass
from forging_blocks.application.ports.inbound import QueryHandlerPort
from forging_blocks.application.ports.outbound import ReadOnlyRepositoryPort


@dataclass(frozen=True)
class OrderSummary:
    order_id: str
    customer_id: str
    item_count: int


class GetOrderSummaryHandler(QueryHandlerPort[dict[str, object], OrderSummary | None]):
    def __init__(self, read_repo: ReadOnlyRepositoryPort["OrderSummary", str]) -> None:
        self._read_repo = read_repo

    async def handle(self, query: GetOrderSummary) -> OrderSummary | None:
        return await self._read_repo.get_by_id(query.order_id)

3. Separate read and write models

In CQRS, the write model may differ from the read model. The write side
works with the full aggregate; the read side serves a lightweight projection.

# === Write model (command side) — full aggregate ===
from uuid import UUID

from forging_blocks.domain.aggregate_root import AggregateRoot


class Order(AggregateRoot[UUID, dict[str, object]]):
    def __init__(self, order_id: UUID) -> None:
        super().__init__(order_id)
        self._customer_id: str = ""
        self._items: list[str] = []
        self._total: float = 0.0

    def add_item(self, item: str, price: float) -> None:
        self._items.append(item)
        self._total += price


# === Read model (query side) — denormalised projection ===
@dataclass(frozen=True)
class OrderSummary:
    order_id: str
    customer_id: str
    item_count: int
    total: float

When this style fits

  • Read and write workloads differ significantly.
  • Scalability concerns dominate.
  • Eventual consistency is acceptable.