Topic Overview
CQRS
Master CQRS (Command Query Responsibility Segregation): separate read and write models, when to use it, and implementation patterns.
CQRS (Command Query Responsibility Segregation) is an architectural pattern that separates read and write operations into different models. Commands (writes) and queries (reads) use different data models optimized for their specific needs.
What is CQRS?
CQRS separates:
- Commands (Writes): Modify state, use write model
- Queries (Reads): Read data, use read model
Traditional (Single Model):
Read → Database → Write
Same model for both
CQRS (Separate Models):
Read → Read Model (optimized for queries)
Write → Write Model (optimized for writes)
Benefits:
- Performance: Optimize each model independently
- Scalability: Scale reads and writes separately
- Flexibility: Different technologies for read/write
- Complexity: Handle complex read requirements
When to Use CQRS
Good Use Cases
1. High Read/Write Ratio
Many reads, few writes
Optimize read model for queries
2. Complex Queries
Read model optimized for complex queries
Write model optimized for domain logic
3. Different Scaling Needs
Reads: Scale horizontally
Writes: Scale differently
4. Event Sourcing
Write: Events
Read: Projections (read model)
Not Good For
1. Simple CRUD
Overhead not worth it
Traditional model sufficient
2. Low Complexity
No complex queries
Single model simpler
CQRS Architecture
Basic CQRS
Commands → Write Model → Database
Queries → Read Model → Database (or cache)
CQRS with Event Sourcing
Commands → Write Model → Events → Event Store
Events → Projections → Read Model → Database
Queries → Read Model
Examples
Basic CQRS Implementation
// Write Model (Commands)
class OrderWriteModel {
async createOrder(command: CreateOrderCommand): Promise<void> {
// Domain logic
const order = new Order(command.userId, command.items);
order.validate();
// Save to write database
await this.writeDatabase.save(order);
// Publish event
await this.eventBus.publish('order.created', order);
}
async updateOrder(command: UpdateOrderCommand): Promise<void> {
const order = await this.writeDatabase.get(command.orderId);
order.update(command.data);
await this.writeDatabase.save(order);
await this.eventBus.publish('order.updated', order);
}
}
// Read Model (Queries)
class OrderReadModel {
async getOrder(orderId: string): Promise<OrderView> {
// Optimized read query
return await this.readDatabase.query(`
SELECT
o.id,
o.user_id,
o.total,
u.name as user_name,
COUNT(oi.id) as item_count
FROM orders o
JOIN users u ON o.user_id = u.id
LEFT JOIN order_items oi ON o.id = oi.order_id
WHERE o.id = ?
GROUP BY o.id
`, [orderId]);
}
async getUserOrders(userId: string): Promise<OrderView[]> {
// Optimized for list queries
return await this.readDatabase.query(`
SELECT * FROM order_views
WHERE user_id = ?
ORDER BY created_at DESC
`, [userId]);
}
}
// Projection (Updates Read Model)
class OrderProjection {
constructor() {
this.eventBus.subscribe('order.created', async (event) => {
await this.updateReadModel(event);
});
this.eventBus.subscribe('order.updated', async (event) => {
await this.updateReadModel(event);
});
}
async updateReadModel(event: Event): Promise<void> {
// Update read model from event
const orderView = this.mapToView(event.data);
await this.readDatabase.upsert('order_views', orderView);
}
}
CQRS with Event Sourcing
// Write Model (Event Sourcing)
class OrderAggregate {
private events: Event[] = [];
async createOrder(command: CreateOrderCommand): Promise<void> {
const event = new OrderCreatedEvent({
orderId: this.generateId(),
userId: command.userId,
items: command.items,
timestamp: Date.now()
});
this.apply(event);
await this.eventStore.append(event);
}
apply(event: Event): void {
this.events.push(event);
// Update aggregate state
}
async reconstruct(orderId: string): Promise<Order> {
const events = await this.eventStore.getEvents(orderId);
let order = new Order();
for (const event of events) {
order.apply(event);
}
return order;
}
}
// Read Model (Projections)
class OrderReadModel {
async updateFromEvent(event: Event): Promise<void> {
switch (event.type) {
case 'order.created':
await this.createOrderView(event);
break;
case 'order.updated':
await this.updateOrderView(event);
break;
}
}
async createOrderView(event: OrderCreatedEvent): Promise<void> {
const view = {
id: event.orderId,
userId: event.userId,
itemCount: event.items.length,
total: this.calculateTotal(event.items),
createdAt: event.timestamp
};
await this.readDatabase.insert('order_views', view);
}
}
Common Pitfalls
- Over-engineering: Using CQRS for simple cases. Fix: Only use when benefits outweigh complexity
- Eventual consistency: Read model may be stale. Fix: Accept eventual consistency, or use synchronous updates
- Complexity: More moving parts. Fix: Start simple, add complexity gradually
- Not handling failures: Projection failures not handled. Fix: Implement retries, error handling
- Read model sync: Read model out of sync. Fix: Monitor lag, implement catch-up
Interview Questions
Beginner
Q: What is CQRS and why is it used?
A:
CQRS (Command Query Responsibility Segregation) separates read and write operations into different models.
Why used:
- Performance: Optimize each model independently
- Scalability: Scale reads and writes separately
- Flexibility: Different technologies for read/write
- Complexity: Handle complex read requirements
Traditional:
Read → Database → Write
Same model for both
CQRS:
Read → Read Model (optimized for queries)
Write → Write Model (optimized for writes)
Example:
Write Model: Normalized, domain logic
Read Model: Denormalized, optimized for queries
Benefits:
- Read model optimized for complex queries
- Write model optimized for domain logic
- Scale independently
- Use different technologies
Intermediate
Q: Explain how CQRS works with event sourcing. How do you keep read and write models in sync?
A:
CQRS with Event Sourcing:
Write Side:
Commands → Write Model → Events → Event Store
Read Side:
Events → Projections → Read Model
Queries → Read Model
How it works:
- Command: Create order
- Write Model: Validates, creates event
- Event Store: Stores event
- Projection: Reads event, updates read model
- Query: Reads from read model
Keeping in sync:
Event Store → Projection → Read Model
Projection subscribes to events
Updates read model asynchronously
Example:
// Write: Create order
await orderService.createOrder(data);
// Event: OrderCreated stored
// Projection: Updates read model
eventBus.subscribe('order.created', async (event) => {
await readModel.update(event);
});
// Read: Query read model
const orders = await readModel.getUserOrders(userId);
Eventual Consistency:
- Read model may be slightly stale
- Acceptable for most use cases
- Can use synchronous updates if needed
Senior
Q: Design a CQRS system for a social media platform with millions of users. How do you handle read/write separation, event projections, and ensure consistency?
A:
class SocialMediaCQRS {
private writeModel: WriteModel;
private readModel: ReadModel;
private eventStore: EventStore;
private projections: Projection[];
constructor() {
this.writeModel = new WriteModel();
this.readModel = new ReadModel();
this.eventStore = new EventStore();
this.projections = [
new UserProjection(),
new PostProjection(),
new FeedProjection()
];
}
// 1. Write Model (Commands)
class WriteModel {
async createPost(command: CreatePostCommand): Promise<void> {
// Domain logic
const post = new Post(command.userId, command.content);
post.validate();
// Create event
const event = new PostCreatedEvent({
postId: post.id,
userId: post.userId,
content: post.content,
timestamp: Date.now()
});
// Store event
await this.eventStore.append(event);
}
async likePost(command: LikePostCommand): Promise<void> {
const event = new PostLikedEvent({
postId: command.postId,
userId: command.userId,
timestamp: Date.now()
});
await this.eventStore.append(event);
}
}
// 2. Read Model (Queries)
class ReadModel {
async getUserFeed(userId: string, page: number): Promise<PostView[]> {
// Optimized query (denormalized)
return await this.readDatabase.query(`
SELECT
p.id,
p.content,
u.name as author_name,
u.avatar,
p.like_count,
p.comment_count,
p.created_at
FROM post_views p
JOIN user_views u ON p.user_id = u.id
WHERE p.user_id IN (
SELECT followee_id FROM follows WHERE follower_id = ?
)
ORDER BY p.created_at DESC
LIMIT 20 OFFSET ?
`, [userId, page * 20]);
}
async getPostDetails(postId: string): Promise<PostDetailView> {
// Optimized for single post
return await this.readDatabase.get('post_detail_views', postId);
}
}
// 3. Projections
class PostProjection {
constructor() {
this.eventStore.subscribe('post.created', async (event) => {
await this.handlePostCreated(event);
});
this.eventStore.subscribe('post.liked', async (event) => {
await this.handlePostLiked(event);
});
}
async handlePostCreated(event: PostCreatedEvent): Promise<void> {
// Update read model
const view = {
id: event.postId,
userId: event.userId,
content: event.content,
likeCount: 0,
commentCount: 0,
createdAt: event.timestamp
};
await this.readDatabase.insert('post_views', view);
}
async handlePostLiked(event: PostLikedEvent): Promise<void> {
// Update like count
await this.readDatabase.increment('post_views', 'like_count', {
id: event.postId
});
}
}
// 4. Event Store
class EventStore {
async append(event: Event): Promise<void> {
await this.database.insert('events', {
id: event.id,
type: event.type,
aggregateId: event.aggregateId,
data: event.data,
timestamp: event.timestamp,
version: event.version
});
// Publish to projections
await this.publish(event);
}
async getEvents(aggregateId: string): Promise<Event[]> {
return await this.database.query(
'SELECT * FROM events WHERE aggregate_id = ? ORDER BY version',
[aggregateId]
);
}
}
// 5. Consistency
class ConsistencyManager {
async checkLag(): Promise<number> {
// Check projection lag
const lastEvent = await this.eventStore.getLastEvent();
const lastProcessed = await this.getLastProcessedEvent();
return lastEvent.version - lastProcessed.version;
}
async catchUp(): Promise<void> {
// Catch up projections
const lag = await this.checkLag();
if (lag > 100) {
await this.replayEvents();
}
}
}
}
Features:
- Write model: Domain logic, event sourcing
- Read model: Optimized queries, denormalized
- Projections: Update read model from events
- Event store: Persistent event storage
- Consistency: Monitor lag, catch up if needed
Key Takeaways
- CQRS: Separates read and write operations into different models
- Commands: Write operations, use write model
- Queries: Read operations, use read model
- Benefits: Performance, scalability, flexibility
- Event sourcing: Write model produces events, projections update read model
- Eventual consistency: Read model may be slightly stale
- When to use: High read/write ratio, complex queries, different scaling needs
- Best practices: Accept eventual consistency, monitor projection lag, handle failures