CQRS: Separating Reads from Writes
Command Query Responsibility Segregation (CQRS) is an architectural pattern that separates models for reading data from models for updating data. Learn the pros and cons.
In many applications, the way you read data is fundamentally different from the way you write it. You might have complex validation and business logic for creating an order, but a dozen different screens that need to display order information in various formats. For most systems, reads are far more frequent than writes.
Command Query Responsibility Segregation (CQRS) is a pattern that addresses this by creating two distinct models: one for updating data (the Command model) and one for reading data (the Query model).
The Traditional CRUD Approach
In a typical Create, Read, Update, Delete (CRUD) architecture, a single object model is used for both reading and writing. For example, a data access layer might have a generic UserRepository.
// A single model for both reads and writes
class User {
id: string;
name: string;
email: string;
passwordHash: string;
isActive: boolean;
failedLoginAttempts: number;
}
// A single repository for all operations
class UserRepository {
findById(id: string): User { /* ... */ }
// This method is used for both create and update
save(user: User): void {
// Complex logic: check for uniqueness, hash password, validate, etc.
}
}
This works well for simple applications, but it has limitations:
- Mismatched Representations: The model you use to update a user (which needs a password hash) is different from the model you’d show in a UI (which should never include the hash). You end up creating multiple Data Transfer Objects (DTOs) to handle this.
- Scalability Issues: You can’t scale the read and write operations independently. A surge in read traffic could impact the performance of critical write operations.
- Complexity: The single
Usermodel becomes bloated with properties and methods that are only relevant for either reads or writes, but not both.
The CQRS Approach: Separate Models
CQRS splits the single model into two.
- The Command Model: Handles all create, update, and delete requests. These are called “Commands.” Commands are task-based (e.g.,
ChangeUserEmail,DisableUser) and do not return data. Their job is to change the state of the system. - The Query Model: Handles all read requests. “Queries” only return data and must not change the system’s state. The query model can be highly optimized for the specific needs of the UI.
TypeScript Example
Let’s refactor our user management system using CQRS.
1. The Write/Command Side
Commands are simple objects that represent an intent.
// commands.ts
interface Command { type: string; payload: any; }
class ChangeUserEmailCommand implements Command {
readonly type = 'ChangeUserEmail';
constructor(public payload: { userId: string; newEmail: string; }) {}
}
class DeactivateUserCommand implements Command {
readonly type = 'DeactivateUser';
constructor(public payload: { userId: string; reason: string; }) {}
}
A Command Handler processes the command. This is where the business logic and validation live.
// command-handler.ts
class UserCommandHandler {
private userRepository: WriteRepository<User>; // A repository focused on writes
handle(command: Command): void {
if (command instanceof ChangeUserEmailCommand) {
const user = this.userRepository.findById(command.payload.userId);
// ... lots of business logic ...
user.email = command.payload.newEmail;
this.userRepository.save(user);
} else if (command instanceof DeactivateUserCommand) {
// ... logic to deactivate user ...
}
}
}
2. The Read/Query Side
The query side uses a completely different, optimized data model. Notice it doesn’t contain sensitive information like the password hash.
// read-models.ts
interface UserProfileViewModel {
id: string;
name: string;
email: string;
isActive: boolean;
}
The query handlers fetch data directly into these read models, perhaps from a different, denormalized database or cache.
// query-handler.ts
class UserQueryHandler {
// This might connect to a different database (e.g., a read replica)
private readDb: ReadDatabase;
findUserProfile(userId: string): UserProfileViewModel {
// This query is optimized for reading, it might join several tables
return this.readDb.query("SELECT id, name, email, isActive FROM user_profiles WHERE id = ?", [userId]);
}
findActiveUsers(): UserProfileViewModel[] {
return this.readDb.query("...");
}
}
How the Two Sides are Synchronized
This is the trickiest part of CQRS. The command model is the source of truth. When it changes state, it must update the read model. This is usually done asynchronously.
A common approach is using an event-driven architecture.
- The command handler saves the changes to the write database.
- It then publishes an event, such as
UserEmailChanged. - An event listener subscribes to this event, receives the new data, and updates the read database.
This leads to eventual consistency. The read model might be slightly out of date for a few milliseconds after a write. This is a trade-off you must be willing to accept to get the benefits of CQRS.
Python Example: A Blog Post System
# The Write/Command Side
class PostCommandService:
def create_post(self, author_id: int, title: str, content: str):
print(f"COMMAND: Creating post '{title}'")
# 1. Validate the command
if len(title) < 5:
raise ValueError("Title is too short")
# 2. Save to the "write" database (e.g., a normalized SQL table)
write_db.execute("INSERT INTO posts (title, content, author_id) VALUES (?, ?, ?)", (title, content, author_id))
# 3. Publish an event
event_bus.publish("PostCreated", {"title": title, "author_id": author_id})
# The Read/Query Side
class PostQueryService:
def get_post_for_display(self, post_id: int) -> dict:
print(f"QUERY: Fetching post {post_id} for display")
# This might read from a denormalized cache like Redis or a different table
# that joins post data with author names for efficiency.
return read_db.query_one("SELECT p.title, p.content, a.name FROM post_display p JOIN authors a ON p.author_id = a.id WHERE p.id = ?", (post_id,))
# An event handler to sync the read model
def on_post_created(event_data: dict):
print("EVENT HANDLER: Updating read model for new post")
# Take the data from the event and update the read database/cache
# This might involve pre-calculating values or joining data.
Benefits of CQRS
- Independent Scaling: You can scale your read servers and write servers independently. If 99% of your traffic is reads, you can provision many read replicas without affecting the write master.
- Optimized Data Models: The read and write models can be tailored to their specific tasks. The write model is optimized for transactional consistency, while the read model is optimized for efficient querying.
- Simpler Logic: Commands and queries become simpler because they are more focused. The command side doesn’t worry about formatting data for the UI, and the query side doesn’t contain complex business logic.
- Flexibility: You can even use different database technologies for each model. For instance, a relational database for writes and a document database or search index for reads.
When to Use (and Not Use) CQRS
CQRS is a powerful pattern, but it adds complexity. It’s not the right choice for every project.
Good for:
- Highly collaborative domains where many users work on the same data.
- Applications with high-performance read requirements that differ greatly from the write requirements.
- Systems where you expect to scale the read and write workloads independently.
- Projects that are already using an event-driven architecture.
Avoid for:
- Simple CRUD applications where the read and write models are very similar.
- Projects where the team is not comfortable with the added complexity of event-driven architectures and eventual consistency.
- When you need strong, immediate consistency between reads and writes.
CQRS is an advanced architectural pattern that provides a solution for managing complex domains and scaling high-performance applications. It forces a clear separation of concerns that can lead to a more maintainable and robust system, but at the cost of increased architectural overhead.