Event sourcing pattern
Most people live happy with CRUDs (Create, Read, Update, Delete) but is that the best approach for every app in the world? Understanding other architectural patterns provide you with tools to tackle different situations so let’s get into it.
What is Event Sourcing? #
Event Sourcing is an architectural pattern where all changes to an application’s state are stored as a sequence of immutable events. Instead of persisting just the current state (like CRUD), every state-altering action is captured as an event. This approach provides a complete audit trail, enabling features like auditing, debugging, and the ability to replay events to rebuild the state.
Maybe you are thinking what’s the benefit of this if I can write my logs for every state change in my app? It could sound like the same thing but it’s not, let’s check this first to clear that idea from our mind.
CRUD with Event Logging #
When using CRUD with event logging, specific actions or changes are recorded alongside CRUD operations, typically for auditing or debugging purposes. However, these logs are separate and not used to reconstruct the application’s state, that’s the most important thing, the reconstruction of the system.
In an event source architecture, the state is recovered by replaying previous events, that means that every step is needed to get the same state of the system. We can imagine this with our lives, if “another you” wants to be where you are now, he/she has to live every second of your life exactly like it was, like replaying your life, that’s what event sourcing is all about.
Key Differences #
Aspect | Event Sourcing | CRUD with Event Logging |
---|---|---|
Primary Data Source | Event store (all events) | Main database (current state) |
State Management | Rebuilt from events | Directly updated via CRUD |
Immutability | Events are immutable | Data can be updated/deleted; events logged separately |
Audit Trail | Built-in through events | Separate logs |
Let’s check some code #
What about taking a look to some pseudo code to have a better picture of the idea? For simple purposes we are going to use an array as our store.
CRUD with event logging #
// Main data store
let users = [];
// Event log
let eventLog = [];
// Create User
function createUser(id, name) {
const user = { id, name };
users.push(user);
eventLog.push({ type: "UserCreated", payload: user, timestamp: new Date() });
}
// Update User
function updateUser(id, newName) {
const user = users.find((u) => u.id === id);
if (user) {
user.name = newName;
eventLog.push({
type: "UserUpdated",
payload: { id, newName },
timestamp: new Date(),
});
}
}
// Read User
function getUser(id) {
return users.find((u) => u.id === id);
}
// Delete User
function deleteUser(id) {
users = users.filter((u) => u.id !== id);
eventLog.push({
type: "UserDeleted",
payload: { id },
timestamp: new Date(),
});
}
// Usage
createUser(1, "Alice");
updateUser(1, "Alicia");
console.log(getUser(1)); // { id: 1, name: 'Alicia' }
deleteUser(1);
console.log(users); // []
console.log(eventLog); // Logs of all events
Now let’s take a look an event sourcing example to see how the state is obtained from replyaing all the events.
Event sourcing #
// Event store
let events = [];
// Aggregate root
class User {
constructor() {
this.id = null;
this.name = "";
}
apply(event) {
switch (event.type) {
case "UserCreated":
this.id = event.payload.id;
this.name = event.payload.name;
break;
case "UserUpdated":
this.name = event.payload.newName;
break;
case "UserDeleted":
this.id = null;
this.name = "";
break;
}
}
create(id, name) {
const event = {
type: "UserCreated",
payload: { id, name },
timestamp: new Date(),
};
events.push(event);
this.apply(event);
}
update(newName) {
const event = {
type: "UserUpdated",
payload: { newName },
timestamp: new Date(),
};
events.push(event);
this.apply(event);
}
delete() {
const event = { type: "UserDeleted", payload: {}, timestamp: new Date() };
events.push(event);
this.apply(event);
}
}
// Rebuild state from events
function getCurrentUser() {
const user = new User();
events.forEach((event) => user.apply(event));
return user;
}
// Usage
const user = new User();
user.create(1, "Alice");
user.update("Alicia");
console.log(getCurrentUser()); // User { id: 1, name: 'Alicia' }
user.delete();
console.log(getCurrentUser()); // User { id: null, name: '' }
console.log(events); // All events stored
These pieces of code illustrate pretty well the difference, CRUD with event logging directly modifies the current state and logs events separately while Event sourcing records each change as an event and reconstructs the state by replaying these events.
Databases for Event Sourcing #
Choosing the right database is vital for effectively implementing event sourcing, while there are specific databases for this architecture many of the well known NoSQL options can be used, let’s check the pros and cons of two of them plus an specific database.
MongoDB #
- Pros:
- Flexible Schema: Easily accommodate varying event structures.
- Rich Querying: Powerful aggregation framework for projections.
- Developer-Friendly: Simple integration with various languages.
- Cons:
- Write Scalability: May require complex sharding for extremely high write volumes.
- Operational Overhead: Managing replicas and sharding can be complex.
Cassandra #
- Pros:
- High Write Throughput: Ideal for massive event volumes.
- Distributed Architecture: Excellent fault tolerance and scalability.
- Time-Series Optimization: Efficient handling of time-ordered data.
- Cons:
- Complex Querying: Less flexible querying compared to MongoDB.
- Steeper Learning Curve: Requires deep understanding for effective data modeling.
EventStoreDB #
- Pros:
- Purpose-Built: Specifically designed for event sourcing.
- Stream-Based: Efficiently handles event streams and projections.
- Built-In Features: Supports event versioning, snapshots, and more.
- Cons:
- Niche Use Case: Less versatile outside event sourcing.
- Smaller Community: Fewer resources and integrations compared to MongoDB or Cassandra.
What frameworks for Event Sourcing exist? #
Using a framework can significantly boost your productivity because takes care of common tasks like snapshots and recovering state. Here are some popular choices across different programming languages:
Axon Framework: While primarily for Java, it inspires frameworks.
EventStore: Node.js client for EventStoreDB.
EventFlow: Async/await first CQRS+ES and DDD framework for .NET
Eventuous: A .NET library for event sourcing and CQRS.
Akka Persistence: Provides event sourcing capabilities within the Akka toolkit.
Use Cases #
When to Choose Event Sourcing: #
Event sourcing shines in scenarios where tracking every change is crucial. Here are some prime use cases:
Financial Systems: Banks and trading platforms benefit from the immutable audit trails that event sourcing provides, ensuring compliance and traceability.
E-commerce Platforms: Tracking user actions like orders, cart updates, and payments can help in analytics, personalization, and fraud detection.
Real-Time Analytics: Systems that require real-time data processing can leverage event streams for immediate insights.
Collaborative Applications: Tools like document editors or project management apps can use event sourcing to manage concurrent changes and provide undo functionalities.
Complex Domain Models: Applications with intricate business logic can benefit from the clear separation of commands and events, making the system more maintainable.
What are the challenges? #
If you are thinking about using event sourcing you have to analyze if the challenges that it brings is something your team is able to deal with.
- Complexity: Higher implementation and maintenance complexity.
- Storage Overhead: Storing every event can consume significant storage (increase cost).
- Reconstruction Performance: Rebuilding state from numerous events may require optimizations like snapshots to increase speed.
Is event sourcing suitability for small projects? #
For small projects, CRUD with event logging maybe be more appropriate due to its simplicity and lower overhead. Event Sourcing introduces additional complexity that may not be justified unless you need detailed audit logs or anticipate significant scalability requirements.
Conclusion #
Event sourcing is a powerful architectural pattern that offers robust benefits, especially for systems requiring detailed audit trails, complex state management, and scalability. However, it introduces additional complexity and costs that might not be justified for smaller projects or simpler applications.
The decision to adopt event sourcing should be based on a careful assessment of your project’s specific needs, the expertise of your team, and the long-term benefits versus the initial overhead. When implemented thoughtfully, event sourcing can be a game-changer, offering flexibility and resilience in the face of evolving software demands.