CQRS (Command Query Responsibility Segregation) and Event Sourcing help with scalability, maintainability, and performance in systems where reads and writes have different shapes and loads. Here's what they are, why they help, and how they fit together.
What is CQRS?
CQRS separates read and write operations for a data store. Traditionally, CRUD (Create, Read, Update, Delete) runs against a single model, which gets complex and inefficient at scale.
Benefits of CQRS
- Scalability: Reads and writes scale independently.
- Performance: Read models optimized for queries; write models optimized for updates.
- Simpler models: Separation of concerns keeps the codebase cleaner.
Implementing CQRS
Commands (writes) and queries (reads) use different models:
- Command Model: State-changing operations (create, update, delete).
- Query Model: Read-only operations (fetch data).
Basic TypeScript example:
// Command Model
class CreateUserCommand {
constructor(public name: string, public email: string) {}
}
class UserCommandHandler {
handle(command: CreateUserCommand): void {
// Logic to handle user creation
// e.g., save to database
}
}
// Query Model
class UserQuery {
constructor(public id: string) {}
}
class UserQueryHandler {
handle(query: UserQuery): User | null {
// Logic to fetch user data
// e.g., retrieve from database
return { id: query.id, name: "John Doe", email: "john.doe@example.com" };
}
}
What is Event Sourcing?
Event Sourcing captures state changes as a sequence of events. Instead of storing only the current state, the system stores events that describe each transition.
Benefits of Event Sourcing
- Auditability: Every change is recorded — a full audit trail.
- Event Replay: Reconstruct system state by replaying events.
- Decoupling: Events integrate with other systems asynchronously.
Implementing Event Sourcing
State changes are represented as events:
// Event
class UserCreatedEvent {
constructor(public id: string, public name: string, public email: string) {}
}
// Event Store
class EventStore {
private events: Array<any> = [];
save(event: any): void {
this.events.push(event);
}
getEvents(): Array<any> {
return this.events;
}
}
// Aggregate
class User {
private id: string;
private name: string;
private email: string;
constructor(private eventStore: EventStore) {}
create(id: string, name: string, email: string): void {
const event = new UserCreatedEvent(id, name, email);
this.apply(event);
this.eventStore.save(event);
}
private apply(event: UserCreatedEvent): void {
this.id = event.id;
this.name = event.name;
this.email = event.email;
}
}
// Usage
const eventStore = new EventStore();
const user = new User(eventStore);
user.create("1", "John Doe", "john.doe@example.com");
console.log(eventStore.getEvents());
Combining CQRS and Event Sourcing
They complement each other: Event Sourcing fits the write side of CQRS (commands produce events that are stored and processed), while the read side builds query models from those events.
Example
// Command Model (Event Sourced)
class UserCommandHandler {
constructor(private eventStore: EventStore) {}
handle(command: CreateUserCommand): void {
const event = new UserCreatedEvent(command.name, command.email);
this.eventStore.save(event);
}
}
// Query Model
class UserReadModel {
private users: { [key: string]: User } = {};
handleEvent(event: UserCreatedEvent): void {
this.users[event.id] = new User(event.id, event.name, event.email);
}
getUser(id: string): User | null {
return this.users[id] || null;
}
}
// Usage
const eventStore = new EventStore();
const userCommandHandler = new UserCommandHandler(eventStore);
const userReadModel = new UserReadModel();
userCommandHandler.handle(new CreateUserCommand("John Doe", "john.doe@example.com"));
eventStore.getEvents().forEach(event => userReadModel.handleEvent(event));
console.log(userReadModel.getUser("1"));
Top comments (0)