CQRS: Command Query Responsibility Segregation
Introduction to CQRS
Command Query Responsibility Segregation (CQRS) is an architectural pattern that separates read and write operations for a data store. Traditional application architectures often use the same model for both reading and writing data, which can lead to complexity and performance challenges. CQRS provides a solution by distinctly separating these responsibilities.
Key Concepts of CQRS
- Command: An operation that changes the state of the system
- Query: An operation that retrieves data without modifying state
- Separation of Concerns: Distinct models for read and write operations
Why Use CQRS?
CQRS offers several compelling advantages:
- Improved performance and scalability
- More flexible and maintainable architecture
- Better optimization for read and write operations
- Enhanced security and validation
- Easier implementation of complex domain logic
Implementing CQRS in JavaScript
Let's break down a practical implementation of CQRS using JavaScript:
1. Basic CQRS Structure
// Command Handler
class UserCommandHandler {
constructor(userRepository) {
this.userRepository = userRepository;
}
// Command to create a user
createUser(command) {
// Validate command
if (!command.username || !command.email) {
throw new Error('Invalid user creation command');
}
// Create user
const user = {
id: Date.now(),
username: command.username,
email: command.email,
createdAt: new Date()
};
// Persist user
return this.userRepository.save(user);
}
// Command to update user
updateUser(command) {
// Validate command
if (!command.id) {
throw new Error('User ID is required for update');
}
// Retrieve and update user
const user = this.userRepository.findById(command.id);
if (!user) {
throw new Error('User not found');
}
// Apply updates
const updatedUser = {
...user,
...command.updates
};
return this.userRepository.save(updatedUser);
}
}
// Query Handler
class UserQueryHandler {
constructor(userRepository) {
this.userRepository = userRepository;
}
// Query to get user by ID
getUserById(query) {
return this.userRepository.findById(query.id);
}
// Query to list users
listUsers(query) {
const { page = 1, limit = 10 } = query;
const offset = (page - 1) * limit;
return {
users: this.userRepository.findAll(offset, limit),
total: this.userRepository.count()
};
}
// Complex query with filtering
findUsersByCriteria(query) {
const { username, email, status } = query;
return this.userRepository.findByCriteria({
username,
email,
status
});
}
}
// Simple In-Memory Repository (for demonstration)
class UserRepository {
constructor() {
this.users = new Map();
}
save(user) {
if (!user.id) {
user.id = Date.now();
}
this.users.set(user.id, user);
return user;
}
findById(id) {
return this.users.get(id);
}
findAll(offset = 0, limit = 10) {
return Array.from(this.users.values())
.slice(offset, offset + limit);
}
count() {
return this.users.size;
}
findByCriteria(criteria) {
return Array.from(this.users.values())
.filter(user => {
return Object.entries(criteria)
.every(([key, value]) =>
value === undefined || user[key] === value
);
});
}
}
// Usage Example
const userRepository = new UserRepository();
const commandHandler = new UserCommandHandler(userRepository);
const queryHandler = new UserQueryHandler(userRepository);
// Create a user (Command)
const createUserCommand = {
username: 'johndoe',
email: 'john@example.com'
};
const newUser = commandHandler.createUser(createUserCommand);
// Retrieve user (Query)
const getUserQuery = { id: newUser.id };
const retrievedUser = queryHandler.getUserById(getUserQuery);
2. Event-Driven CQRS with Event Sourcing
class UserEvent {
constructor(type, payload) {
this.id = Date.now();
this.type = type;
this.payload = payload;
this.timestamp = new Date();
}
}
class UserAggregate {
constructor() {
this.events = [];
this.state = {
id: null,
username: null,
email: null,
status: 'active'
};
}
// Apply events to reconstruct state
apply(event) {
switch(event.type) {
case 'USER_CREATED':
this.state.id = event.payload.id;
this.state.username = event.payload.username;
this.state.email = event.payload.email;
break;
case 'USER_UPDATED':
Object.assign(this.state, event.payload);
break;
case 'USER_DEACTIVATED':
this.state.status = 'inactive';
break;
}
this.events.push(event);
return this;
}
// Reconstruct state from event history
static fromEvents(events) {
return events.reduce(
(aggregate, event) => aggregate.apply(event),
new UserAggregate()
);
}
}
// Event Store
class EventStore {
constructor() {
this.events = [];
}
// Save event
saveEvent(event) {
this.events.push(event);
return event;
}
// Retrieve events for a specific user
getEventsForUser(userId) {
return this.events
.filter(event => event.payload.id === userId);
}
}
// Usage of Event-Driven CQRS
const eventStore = new EventStore();
// Create user event
const userCreatedEvent = new UserEvent('USER_CREATED', {
id: 1,
username: 'janedoe',
email: 'jane@example.com'
});
eventStore.saveEvent(userCreatedEvent);
// Update user event
const userUpdatedEvent = new UserEvent('USER_UPDATED', {
id: 1,
username: 'janesmith'
});
eventStore.saveEvent(userUpdatedEvent);
// Reconstruct user state
const userEvents = eventStore.getEventsForUser(1);
const userAggregate = UserAggregate.fromEvents(userEvents);
console.log(userAggregate.state);
Advanced CQRS Patterns
1. Read Models
Create optimized read models that can be quickly queried:
class UserReadModel {
constructor(eventStore) {
this.eventStore = eventStore;
this.readModel = new Map();
}
// Build read model from events
rebuild() {
this.readModel.clear();
this.eventStore.events.forEach(event => {
switch(event.type) {
case 'USER_CREATED':
this.readModel.set(event.payload.id, {
id: event.payload.id,
username: event.payload.username,
email: event.payload.email,
createdAt: event.timestamp
});
break;
case 'USER_UPDATED':
const existingUser = this.readModel.get(event.payload.id);
this.readModel.set(event.payload.id, {
...existingUser,
...event.payload
});
break;
}
});
}
// Quick lookup methods
findById(id) {
return this.readModel.get(id);
}
findByUsername(username) {
return Array.from(this.readModel.values())
.find(user => user.username === username);
}
}
Considerations and Best Practices
- Performance: CQRS can introduce complexity, so measure and optimize carefully.
- Eventual Consistency: In distributed systems, read models may lag behind write models.
- Validation: Implement robust validation in command handlers.
- Scalability: Separate read and write databases for maximum performance.
When to Use CQRS
- Complex domain models
- High-performance applications
- Systems with complex business logic
- Applications with different read and write patterns
When to Avoid CQRS
- Simple CRUD applications
- Small projects with minimal complexity
- Limited resources for implementation
Conclusion
CQRS is a powerful architectural pattern that provides flexibility, performance, and separation of concerns. While it may seem complex, the benefits can be significant for large, complex applications with diverse read and write requirements.