Documentation

CQRS — Commands & Queries

Separating write operations (commands) from read operations (queries) — why this simplifies the application layer rather than adding complexity.

Status: Extracted
Purpose: Command/Query Separation principles, command handlers, and query patterns.


CQRS Overview

Command/Query Separation

Principle: Separate commands (write operations) from queries (read operations).

Benefits:

  • Clear separation: Write and read operations handled differently
  • Optimization: Optimize reads and writes independently
  • Scalability: Scale reads and writes separately
  • Simplicity: Clearer code organization

Command Pattern

Commands

Definition: Intentions to perform an action (write operations).

Characteristics:

  • Mutate state: Change system state
  • Return void or result: May return created/updated entity
  • Idempotent: Should be safe to retry
  • Transactional: Executed within transaction

Example:

@dataclass
class CreateAccountCommand:
    """Command to create account."""
    phone: str
    name: str
    # ... other fields

Command Handlers

Location: application/commands/ or application/services/

Pattern:

class AccountService:
    """Account service with command handlers."""
    
    async def create_account(
        self,
        command: CreateAccountCommand,
        uow: IUnitOfWork,
    ) -> Account:
        """Handle create account command."""
        async with uow:
            account = Account.create(
                phone=command.phone,
                name=command.name,
            )
            uow.accounts.add(account)
            # Transaction commits automatically
            return account
    
    async def update_account(
        self,
        command: UpdateAccountCommand,
        uow: IUnitOfWork,
    ) -> Account:
        """Handle update account command."""
        async with uow:
            account = await uow.accounts.get_by_id(command.account_id)
            if not account:
                raise AccountNotFoundError(command.account_id)
            
            account.update_phone(command.phone)
            account.update_name(command.name)
            # Transaction commits automatically
            return account

Query Pattern

Queries

Definition: Requests for data (read operations).

Characteristics:

  • No side effects: Don't change system state
  • Return data: Return query results
  • Idempotent: Safe to execute multiple times
  • No transaction required: Read-only operations

Example:

@dataclass
class GetAccountQuery:
    """Query to get account."""
    account_id: str

Query Handlers

Location: application/queries/ or application/services/

Pattern:

class AccountService:
    """Account service with query handlers."""
    
    async def get_account(
        self,
        query: GetAccountQuery,
        account_repository: IAccountRepository,
    ) -> Account | None:
        """Handle get account query."""
        return await account_repository.get_by_id(query.account_id)
    
    async def list_accounts(
        self,
        query: ListAccountsQuery,
        account_repository: IAccountRepository,
    ) -> list[Account]:
        """Handle list accounts query."""
        return await account_repository.get_all(
            limit=query.limit,
            offset=query.offset,
        )

Command vs Query Separation

Commands (Write Operations)

Examples:

  • CreateAccountCommand
  • UpdateAccountCommand
  • DeleteAccountCommand
  • SendMessageCommand

Characteristics:

  • Mutate state
  • Use Unit of Work for transactions
  • Return created/updated entity
  • Handle business rules

Queries (Read Operations)

Examples:

  • GetAccountQuery
  • ListAccountsQuery
  • GetMessagesQuery
  • SearchDocumentsQuery

Characteristics:

  • No side effects
  • Return data
  • No transaction required
  • Can be cached

CQRS Benefits

Separation of Concerns

  • Clear boundaries: Commands and queries handled separately
  • Focused handlers: Each handler has single responsibility
  • Easier testing: Test commands and queries independently

Optimization

  • Read optimization: Optimize queries for performance
  • Write optimization: Optimize commands for consistency
  • Caching: Cache query results independently

Scalability

  • Scale reads: Scale query handlers independently
  • Scale writes: Scale command handlers independently
  • Different strategies: Different scaling strategies for reads vs writes

Related Documents

  • Domain-Driven Design - DDD patterns
  • Layered Architecture - Layer structure
  • Repository Pattern - Repository implementation

Reference implementation: application/<feature>/create.py, application/<feature>/query_handlers.py