Pattern separating read and write operations into distinct models, optimizing each independently for performance and scalability.
CQRS (Command Query Responsibility Segregation) is an architectural pattern that separates write operations (commands) from read operations (queries) into completely distinct models. Unlike traditional CRUD systems where the same model handles both operations, CQRS allows each side to use its own database, schema, technology, and scaling strategy.
The pattern builds on Bertrand Meyer's CQS (Command Query Separation) principle but extends it to the architectural level. Commands modify system state without returning data, while queries return data without modifying state. This separation enables specific optimizations for each type of operation.
CQRS doesn't necessarily require event sourcing, although both patterns complement each other naturally. It can be implemented with traditional relational databases, using materialized views or synchronization processes to keep read models updated.
// Command side - Write model
interface CreateOrderCommand {
customerId: string;
items: OrderItem[];
shippingAddress: Address;
}
class OrderCommandHandler {
constructor(
private orderRepository: OrderRepository,
private eventBus: EventBus
) {}
async handle(command: CreateOrderCommand): Promise<void> {
const order = new Order(
command.customerId,
command.items,
command.shippingAddress
);
await this.orderRepository.save(order);
// Publish event for read model projection
await this.eventBus.publish(new OrderCreatedEvent(order));
}
}
// Query side - Read model
interface OrderSummaryQuery {
customerId: string;
status?: OrderStatus;
dateRange?: DateRange;
}
class OrderQueryHandler {
constructor(private readDatabase: ReadDatabase) {}
async handle(query: OrderSummaryQuery): Promise<OrderSummary[]> {
return this.readDatabase.query(`
SELECT
order_id,
customer_name,
total_amount,
status,
created_at
FROM order_summaries
WHERE customer_id = $1
AND ($2::text IS NULL OR status = $2)
AND created_at BETWEEN $3 AND $4
ORDER BY created_at DESC
`, [query.customerId, query.status, query.dateRange?.start, query.dateRange?.end]);
}
}
// Event handler for projection
class OrderProjectionHandler {
async on(event: OrderCreatedEvent): Promise<void> {
await this.readDatabase.execute(`
INSERT INTO order_summaries (
order_id, customer_id, customer_name,
total_amount, status, created_at
) VALUES ($1, $2, $3, $4, $5, $6)
`, [
event.orderId,
event.customerId,
event.customerName,
event.totalAmount,
'PENDING',
event.timestamp
]);
}
}Immediate projection: Updates the read model in the same transaction. Guarantees consistency but reduces performance and increases coupling.
Eventual projection: Uses asynchronous events to update the read model. Higher performance and decoupling, but requires handling eventual consistency.
class OrderQueryService {
async getOrderWithFallback(orderId: string): Promise<OrderView> {
// Try read model first
const orderView = await this.readModel.findById(orderId);
if (orderView) {
return orderView;
}
// Fallback to write model if not yet projected
const order = await this.writeModel.findById(orderId);
if (!order) {
throw new OrderNotFoundError(orderId);
}
// Build view on-the-fly
return this.buildOrderView(order);
}
private buildOrderView(order: Order): OrderView {
return {
id: order.id,
customerName: order.customer.name,
status: order.status,
totalAmount: order.calculateTotal(),
createdAt: order.createdAt
};
}
}CQRS can be implemented without event sourcing using traditional data synchronization:
class OrderService {
async createOrder(command: CreateOrderCommand): Promise<void> {
const transaction = await this.database.beginTransaction();
try {
// Update write model
const order = await this.orderRepository.create(command, transaction);
// Update read model in same transaction
await this.orderSummaryRepository.create({
orderId: order.id,
customerId: order.customerId,
totalAmount: order.totalAmount,
status: order.status
}, transaction);
await transaction.commit();
} catch (error) {
await transaction.rollback();
throw error;
}
}
}| Criteria | CQRS Recommended | Traditional CRUD |
|---|---|---|
| Read/write ratio | > 10:1 | < 5:1 |
| Query complexity | High, multiple views | Simple, basic CRUD |
| Scaling requirements | Independent per operation | Uniform scaling |
| Consistency | Eventual acceptable | Immediate required |
| Team experience | Distributed systems | Monolithic applications |
| Business domain | Complex, multiple contexts | Simple, single context |
Over-engineering: Applying CQRS to simple CRUD systems adds unnecessary complexity. Not every bounded context needs CQRS.
Synchronous projections: Updating read models synchronously eliminates scalability benefits and can create bottlenecks.
Normalized read models: Maintaining normalization in read models wastes the opportunity to optimize for specific queries.
Lack of versioning: Not versioning events or projection schemas complicates system evolution and migrations.
CQRS enables independent optimization of reads and writes, which is critical in systems where access patterns differ significantly. In microservices architectures, it enables each service to optimize its data model for its specific domain. The separation facilitates horizontal scaling: read models can be replicated geographically while write models remain centralized. Combined with event-driven architecture, CQRS enables building resilient systems that can rebuild state from events, facilitating debugging, auditing, and historical analysis.
Architectural pattern where components communicate through asynchronous events, enabling decoupled, scalable, and reactive systems.
Software design approach centering development on the business domain, using a ubiquitous language shared between developers and domain experts.
Pattern where application state is derived from an immutable sequence of events, providing complete audit trail and the ability to reconstruct state at any point in time.
Pattern for managing distributed transactions in microservices through a sequence of local transactions with compensating actions to handle failures.