I’ve been building complex applications for years, and one challenge that kept resurfacing was understanding exactly how our data reached its current state. When a customer reported an issue, we’d spend hours digging through logs, trying to piece together what happened. That frustration led me to event sourcing—a pattern that captures every change as an immutable event. Combining this with MongoDB Change Streams in Node.js creates a robust system for real-time data tracking. Today, I want to guide you through building this from scratch.
Event sourcing stores state changes as a sequence of events rather than just the current state. This approach gives you a complete history of every action. You can replay events to reconstruct past states or debug issues. Have you ever wished you could rewind your application to see exactly what went wrong? That’s the power event sourcing provides.
To get started, you’ll need Node.js 18+, MongoDB 4.0+ configured as a replica set, and basic TypeScript knowledge. Let’s set up our project:
mkdir event-sourcing-app
cd event-sourcing-app
npm init -y
npm install mongodb typescript @types/node dotenv uuid
Create a tsconfig.json file:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true
}
}
MongoDB Change Streams notify your application of database changes in real time. Why poll the database when you can react instantly to changes? Here’s how to monitor a collection:
import { MongoClient } from 'mongodb';
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
const collection = client.db('mydb').collection('orders');
const changeStream = collection.watch();
changeStream.on('change', (change) => {
console.log('Change detected:', change);
});
The event store is where all domain events live. Each event represents something meaningful that happened in your system. How would you design a store that preserves event order and supports fast retrieval?
interface DomainEvent {
eventId: string;
aggregateId: string;
eventType: string;
data: any;
timestamp: Date;
}
class EventStore {
private events: DomainEvent[] = [];
append(event: DomainEvent) {
this.events.push(event);
}
getEvents(aggregateId: string): DomainEvent[] {
return this.events.filter(e => e.aggregateId === aggregateId);
}
}
Aggregates handle commands and produce events. They enforce business rules before emitting new events. When a user places an order, the aggregate validates it and creates an OrderPlaced event.
class OrderAggregate {
private state: any = {};
placeOrder(command: PlaceOrderCommand) {
if (command.amount <= 0) throw new Error('Invalid amount');
return new OrderPlacedEvent({
orderId: command.orderId,
amount: command.amount
});
}
}
Projections transform events into read-optimized views. They listen for events and update query models. What if you need to show order history while keeping the main system responsive?
class OrderHistoryProjection {
private orders: Map<string, any> = new Map();
handleOrderPlaced(event: OrderPlacedEvent) {
this.orders.set(event.data.orderId, {
status: 'placed',
amount: event.data.amount
});
}
}
For performance, consider snapshots to avoid replaying all events for large aggregates. Testing becomes straightforward—you can replay events to verify behavior.
describe('OrderAggregate', () => {
it('should place an order', () => {
const aggregate = new OrderAggregate();
const events = aggregate.placeOrder({ orderId: '1', amount: 100 });
expect(events[0].eventType).toBe('OrderPlaced');
});
});
Common pitfalls include not planning for schema evolution. Events are immutable, so how do you handle changes? Use versioning and upcasters to migrate old events to new formats.
I’ve implemented this pattern in production systems, and the audit capabilities alone justify the effort. One project reduced debugging time by 70% because we could trace every state change.
What questions do you have about handling concurrent modifications or scaling this approach? Share your thoughts in the comments—I’d love to hear about your experiences.
If this guide helped you understand event sourcing better, please like and share it with others who might benefit. Your feedback helps me create more valuable content. Let’s keep the conversation going in the comments below!