Event Sourcing and CQRS
Published on January 20, 2026
Event Sourcing and CQRS
In most traditional applications, you store the current state of your data. If a user has $1000 in their account, you save that value. If they later make a $100 transaction, you update the value to $900. The transaction history is lost, or stored separately.
But what if you need to know exactly what happened, when it happened, and why it happened? What if you need to be able to reconstruct the state at any point in time? What if you need an immutable history of all changes?
Event Sourcing and CQRS are architectural patterns that solve these problems. They are especially useful for financial systems, e-commerce, and any application that requires complete auditing and traceability.
In this article, I will explain what Event Sourcing and CQRS are, when to use them, and how to implement them in practice.
The problem with the traditional model
In the traditional model, you store the current state:
// Traditional model
class Account {
constructor(id, balance) {
this.id = id;
this.balance = balance; // Only the current state
}
deposit(amount) {
this.balance += amount;
// History is lost
}
withdraw(amount) {
this.balance -= amount;
// History is lost
}
}
Problems:
- You cannot see the change history
- You cannot reconstruct the state at a point in time
- You cannot audit what happened
- If there is an error, you cannot easily revert
What is Event Sourcing?
Event Sourcing is a pattern where, instead of storing the current state, you store a sequence of events that represent all the changes that have occurred.
Basic concept
// Instead of storing: balance = 900
// You store events:
// 1. AccountCreated(id: "123", initialBalance: 1000)
// 2. MoneyDeposited(accountId: "123", amount: 500)
// 3. MoneyWithdrawn(accountId: "123", amount: 600)
// 4. MoneyWithdrawn(accountId: "123", amount: 100)
// The current state is calculated by applying all events
// balance = 1000 + 500 - 600 - 100 = 800
Advantages of Event Sourcing
- Complete history: You have a record of all changes
- Auditing: You can see exactly what happened and when
- Time travel: You can reconstruct the state at any point in time
- Debugging: You can replay events to understand problems
- Flexibility: You can create new views of the state without changing the events
Disadvantages of Event Sourcing
- Complexity: More complex than the traditional model
- Performance: Reconstructing the state can be slow if there are many events
- Storage: It can require more storage
- Learning curve: Requires understanding new concepts
Basic implementation of Event Sourcing
Defining events
// Events are immutable objects representing something that happened
class AccountCreated {
constructor(accountId, initialBalance, timestamp) {
this.type = 'AccountCreated';
this.accountId = accountId;
this.initialBalance = initialBalance;
this.timestamp = timestamp;
}
}
class MoneyDeposited {
constructor(accountId, amount, timestamp) {
this.type = 'MoneyDeposited';
this.accountId = accountId;
this.amount = amount;
this.timestamp = timestamp;
}
}
class MoneyWithdrawn {
constructor(accountId, amount, timestamp) {
this.type = 'MoneyWithdrawn';
this.accountId = accountId;
this.amount = amount;
this.timestamp = timestamp;
}
}
Storing events
class EventStore {
constructor() {
this.events = [];
}
append(streamId, event) {
this.events.push({
streamId,
event,
version: this.events.length + 1
});
}
getEvents(streamId) {
return this.events
.filter(e => e.streamId === streamId)
.map(e => e.event);
}
}
Reconstructing state from events
class Account {
constructor(accountId, eventStore) {
this.accountId = accountId;
this.eventStore = eventStore;
this.balance = 0;
// Reconstruct state from events
this.replayEvents();
}
replayEvents() {
const events = this.eventStore.getEvents(this.accountId);
for (const event of events) {
this.applyEvent(event);
}
}
applyEvent(event) {
switch (event.type) {
case 'AccountCreated':
this.balance = event.initialBalance;
break;
case 'MoneyDeposited':
this.balance += event.amount;
break;
case 'MoneyWithdrawn':
this.balance -= event.amount;
break;
}
}
deposit(amount) {
const event = new MoneyDeposited(
this.accountId,
amount,
new Date()
);
this.eventStore.append(this.accountId, event);
this.applyEvent(event);
}
withdraw(amount) {
if (this.balance < amount) {
throw new Error('Insufficient funds');
}
const event = new MoneyWithdrawn(
this.accountId,
amount,
new Date()
);
this.eventStore.append(this.accountId, event);
this.applyEvent(event);
}
}
Snapshots for optimization
If there are many events, reconstructing the state can be slow. Snapshots help:
class Account {
// ... previous code ...
// Every 100 events, save a snapshot
shouldCreateSnapshot() {
const events = this.eventStore.getEvents(this.accountId);
return events.length % 100 === 0;
}
createSnapshot() {
return {
accountId: this.accountId,
balance: this.balance,
version: this.eventStore.getEvents(this.accountId).length
};
}
loadFromSnapshot(snapshot) {
this.balance = snapshot.balance;
const events = this.eventStore.getEvents(this.accountId);
// Only apply events after the snapshot
const eventsAfterSnapshot = events.slice(snapshot.version);
for (const event of eventsAfterSnapshot) {
this.applyEvent(event);
}
}
}
What is CQRS?
CQRS (Command Query Responsibility Segregation) is a pattern that separates read operations (queries) from write operations (commands).
Basic concept
Instead of using the same model for reading and writing:
// ❌ Traditional model: same model for reading and writing
class User {
updateEmail(newEmail) { /* ... */ }
getProfile() { /* ... */ }
}
You separate them into different models:
// ✅ CQRS: separate models for reading and writing
class UserCommand {
updateEmail(newEmail) { /* ... */ }
}
class UserQuery {
getProfile() { /* ... */ }
}
Advantages of CQRS
- Independent optimization: You can optimize reads and writes separately
- Scalability: You can scale reads and writes independently
- Flexibility: You can have multiple read models for different use cases
- Simplicity: Read models can be simpler (only data, no logic)
Disadvantages of CQRS
- Complexity: More complex than the traditional model
- Eventual consistency: Read models may be slightly outdated
- Synchronization: You need to keep read and write models synchronized
CQRS with Event Sourcing
CQRS and Event Sourcing work very well together:
// Command side: Writes events
class AccountCommandService {
constructor(eventStore) {
this.eventStore = eventStore;
}
async createAccount(accountId, initialBalance) {
const event = new AccountCreated(accountId, initialBalance, new Date());
await this.eventStore.append(accountId, event);
}
async deposit(accountId, amount) {
const event = new MoneyDeposited(accountId, amount, new Date());
await this.eventStore.append(accountId, event);
}
async withdraw(accountId, amount) {
// Validate before writing
const account = this.rebuildAccount(accountId);
if (account.balance < amount) {
throw new Error('Insufficient funds');
}
const event = new MoneyWithdrawn(accountId, amount, new Date());
await this.eventStore.append(accountId, event);
}
rebuildAccount(accountId) {
const events = this.eventStore.getEvents(accountId);
const account = new Account(accountId);
for (const event of events) {
account.applyEvent(event);
}
return account;
}
}
+
// Query side: Reads from optimized projections
class AccountQueryService {
constructor(readModel) {
this.readModel = readModel; // Database optimized for reads
}
async getBalance(accountId) {
return await this.readModel.getBalance(accountId);
}
async getTransactionHistory(accountId) {
return await this.readModel.getTransactions(accountId);
}
}
Projections
Projections are state views created from events:
class AccountProjection {
constructor() {
this.accounts = new Map(); // Optimized read model
}
// Process events to keep the projection updated
handleEvent(event) {
switch (event.type) {
case 'AccountCreated':
this.accounts.set(event.accountId, {
accountId: event.accountId,
balance: event.initialBalance,
transactions: []
});
break;
case 'MoneyDeposited':
const account = this.accounts.get(event.accountId);
account.balance += event.amount;
account.transactions.push({
type: 'deposit',
amount: event.amount,
timestamp: event.timestamp
});
break;
case 'MoneyWithdrawn':
const account2 = this.accounts.get(event.accountId);
account2.balance -= event.amount;
account2.transactions.push({
type: 'withdrawal',
amount: event.amount,
timestamp: event.timestamp
});
break;
}
}
}
When to use Event Sourcing and CQRS
✅ Use Event Sourcing when:
- You need complete auditing: Financial systems, e-commerce, legal systems
- You need time travel: Being able to see the state at any point in time
- You need traceability: Knowing exactly what happened and why
- You need flexibility: Creating new state views without changing events
- You need advanced debugging: Replaying events to understand problems
✅ Use CQRS when:
- Reads and writes have different needs: Much more reads than writes
- You need independent scaling: Scale reads and writes separately
- You need to optimize separately: Different optimizations for reads and writes
- You have multiple views: Different ways of seeing the same data
❌ Do not use when:
- Simple application: If you don’t need these features, the complexity is not worth it
- Small team: Requires more knowledge and maintenance
- Immediate consistency is critical: CQRS has eventual consistency
Practical example: E-commerce System
// Events
class OrderCreated {
constructor(orderId, userId, items, total) {
this.type = 'OrderCreated';
this.orderId = orderId;
this.userId = userId;
this.items = items;
this.total = total;
this.timestamp = new Date();
}
}
class PaymentProcessed {
constructor(orderId, amount, method) {
this.type = 'PaymentProcessed';
this.orderId = orderId;
this.amount = amount;
this.method = method;
this.timestamp = new Date();
}
}
class OrderShipped {
constructor(orderId, trackingNumber) {
this.type = 'OrderShipped';
this.orderId = orderId;
this.trackingNumber = trackingNumber;
this.timestamp = new Date();
}
}
// Command side
class OrderCommandService {
async createOrder(orderId, userId, items, total) {
const event = new OrderCreated(orderId, userId, items, total);
await eventStore.append(orderId, event);
}
async processPayment(orderId, amount, method) {
const event = new PaymentProcessed(orderId, amount, method);
await eventStore.append(orderId, event);
}
}
// Query side
class OrderQueryService {
async getOrderHistory(userId) {
// Read from optimized projection
return await readModel.getUserOrders(userId);
}
async getOrderStatus(orderId) {
// Read from projection
return await readModel.getOrderStatus(orderId);
}
}
// Projection
class OrderProjection {
handleEvent(event) {
switch (event.type) {
case 'OrderCreated':
// Update read model
break;
case 'PaymentProcessed':
// Update read model
break;
case 'OrderShipped':
// Update read model
break;
}
}
}
Best practices
1. Immutable events
Events should never change once created. They are the historical record.
2. Event versioning
If you need to change the structure of an event, create a new version:
class MoneyDepositedV1 {
constructor(accountId, amount) {
this.version = 1;
// ...
}
}
class MoneyDepositedV2 {
constructor(accountId, amount, reason) {
this.version = 2;
// ...
}
}
3. Validation in commands
Validate before creating events:
async withdraw(accountId, amount) {
// Validate first
const account = this.rebuildAccount(accountId);
if (account.balance < amount) {
throw new Error('Insufficient funds');
}
// Only create event if valid
const event = new MoneyWithdrawn(accountId, amount, new Date());
await this.eventStore.append(accountId, event);
}
4. Snapshots for performance
Use snapshots when there are many events to avoid reconstructing from scratch.
5. Eventually consistent projections
Projections can be slightly outdated. Ensure your application can handle this.
My personal perspective
Event Sourcing and CQRS are powerful patterns for systems that need:
- Complete and immutable history
- Auditing and traceability
- Time travel (reconstructing state at any point)
- Independent optimization of reads and writes
They are not for every application. They add complexity, but when you need them, they are invaluable.
I have worked with systems that required complete auditing, and Event Sourcing was the difference between being able to trace exactly what happened and having to guess. I have worked with systems where CQRS allowed scaling reads and writes independently, solving performance problems that the traditional model could not resolve.
For financial systems, e-commerce with auditing requirements, or any application where you need to know exactly what happened and when, Event Sourcing and CQRS can be the difference between a system that can audit and one that cannot.
The key is to understand when these patterns make sense and when the extra complexity is not worth it. For most applications, the traditional model is enough. But when you need these characteristics, Event Sourcing and CQRS are powerful tools that can make the difference.