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 fieldsCommand 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 accountQuery 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: strQuery 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:
CreateAccountCommandUpdateAccountCommandDeleteAccountCommandSendMessageCommand
Characteristics:
- Mutate state
- Use Unit of Work for transactions
- Return created/updated entity
- Handle business rules
Queries (Read Operations)
Examples:
GetAccountQueryListAccountsQueryGetMessagesQuerySearchDocumentsQuery
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