Event Sourcing : Node.js

Event Sourcing is a design pattern used to capture and store the state of an application as a series of events. Rather than storing the current state directly, this approach records each change as an immutable event, allowing for a historical view and the recreation of the application’s state at any point in time. Event sourcing is particularly beneficial in distributed systems, where maintaining consistency and reconstructing state are often challenging.

Key Concepts

1. Events: Events represent state changes. Instead of modifying the state directly, events capture the intended change, storing it as an immutable record.


2. Event Store: An event store is the storage layer for persisting events. This could be a specialized database (e.g., EventStore) or a general-purpose database designed to handle sequential data.


3. Event Stream: Each entity or aggregate maintains its own event stream, an ordered sequence of all events affecting that entity, such as a user account or an order.


4. Event Replay: The entire state of an entity can be reconstructed by replaying its event stream, allowing applications to derive the current state dynamically from historical data.



Benefits of Event Sourcing

Traceability: By storing every state change, developers can analyze historical data for debugging, auditing, or understanding user behavior.

Consistency: Distributed systems benefit from event sourcing since changes are recorded and propagated as discrete, immutable events.

Flexibility: Different views or projections can be generated from the event history, allowing alternative perspectives without altering the source of truth.


Event Sourcing Implementation in Node.js

Let’s implement a simple example in Node.js where we track a user account with balance updates.

1. Setting up the Project

mkdir event-sourcing-node
cd event-sourcing-node
npm init -y
npm install uuid

2. Defining Event Classes

Each event needs a class to represent it. Here, we’ll use AccountCreated and BalanceUpdated events.

const { v4: uuidv4 } = require(‘uuid’);

class AccountCreated {
    constructor(userId, initialBalance) {
        this.id = uuidv4();
        this.userId = userId;
        this.initialBalance = initialBalance;
        this.timestamp = Date.now();
    }
}

class BalanceUpdated {
    constructor(userId, amount) {
        this.id = uuidv4();
        this.userId = userId;
        this.amount = amount;
        this.timestamp = Date.now();
    }
}

module.exports = { AccountCreated, BalanceUpdated };

3. Creating the Event Store

The event store will be a simple in-memory storage for demonstration purposes.

class EventStore {
    constructor() {
        this.events = [];
    }

    saveEvent(event) {
        this.events.push(event);
    }

    getEvents(userId) {
        return this.events.filter(event => event.userId === userId);
    }
}

module.exports = EventStore;

4. Aggregating State from Events

To calculate the current balance, we replay the stored events.

class Account {
    constructor(userId, eventStore) {
        this.userId = userId;
        this.balance = 0;
        this.eventStore = eventStore;
    }

    loadFromHistory() {
        const events = this.eventStore.getEvents(this.userId);
        events.forEach(event => {
            if (event instanceof AccountCreated) {
                this.balance = event.initialBalance;
            } else if (event instanceof BalanceUpdated) {
                this.balance += event.amount;
            }
        });
    }
}

module.exports = Account;

5. Running the Example

const { AccountCreated, BalanceUpdated } = require(‘./events’);
const EventStore = require(‘./eventStore’);
const Account = require(‘./account’);

const eventStore = new EventStore();

// Create an account
const userId = ‘user123’;
const accountCreated = new AccountCreated(userId, 100);
eventStore.saveEvent(accountCreated);

// Update the balance
const depositEvent = new BalanceUpdated(userId, 50);
const withdrawalEvent = new BalanceUpdated(userId, -20);
eventStore.saveEvent(depositEvent);
eventStore.saveEvent(withdrawalEvent);

// Load account and calculate balance
const account = new Account(userId, eventStore);
account.loadFromHistory();
console.log(`User balance: ${account.balance}`); // Expected output: User balance: 130

Explanation of the Code

1. Event Definitions: Each event class (e.g., AccountCreated, BalanceUpdated) captures relevant data and ensures immutability by storing a timestamp and unique ID.


2. Event Store: A simplistic EventStore class holds all events in an array. For production, consider a persistent database.


3. Account Class: The account reconstructs its balance by loading and processing all events related to a specific userId.


4. Example Execution: By creating events (account creation and balance updates) and then replaying them, we reconstruct the user’s account balance.


Conclusion

Event Sourcing offers a powerful pattern for capturing and rebuilding state in distributed systems, providing a complete, traceable history of changes. With Node.js, we can implement a basic event sourcing architecture using classes for events, event storage, and aggregates. For scalable solutions, more advanced event storage and retrieval mechanisms (like databases optimized for event storage) are recommended.

The article above is rendered by integrating outputs of 1 HUMAN AGENT & 3 AI AGENTS, an amalgamation of HGI and AI to serve technology education globally.

(Article By : Himanshu N)