Marcio Cunha

Implementation of Event Sourcing with Asynchronous Projections in High-Availability NoSQL Databases

Learn how to structure event-driven architectures by persisting immutable states and generating asynchronous projections in NoSQL databases to ensure high performance, eventual consistency, and extreme scalability.

Marcio Cunha•5 min
Also available in:EspañolPortuguês
Summary
  • Immutable event storage guarantees complete auditing and temporal-historical traceability in critical systems.
  • Asynchronous projections decouple data writing from reading operations, optimizing overall application performance.
  • NoSQL databases offer schema flexibility ideal for storing custom projection documents tailored by context.
  • Eventual consistency requires handling strategies to manage delays in propagating updates between the log and read database.
  • Idempotency and versioning strategies prevent data corruption during reprocessing cycles and network failures.

What is Event Sourcing and why abandon traditional table models

In conventional software engineering, we usually save only the current state of a record in a relational database. If a user changes their delivery address, the system overwrites the old data, and history disappears forever. In the architecture pattern known as Event Sourcing, this logic is inverted: instead of saving the static picture of the present, we record every modification as an immutable event, a kind of accounting ledger entry that can never be erased or altered.

In practice, this means the current state of any entity in the system is not stored directly, but calculated on demand by chronologically replaying this trail of past occurrences. This approach transforms complex systems into auditable and resilient structures, where human errors or code bugs can be corrected by rolling back the time pointer and re-executing business logic cleanly. However, this freedom brings a considerable computational price for fast queries, requiring the use of auxiliary mechanisms called projections.

The critical role of asynchronous projections in performance

Calculating the current state of a bank account or shopping cart by summing thousands of events every time a user opens the screen is unfeasible in terms of performance. To solve this bottleneck, we use the concept of projection, which consists of reading the sequence of raw events, processing the business rules, and saving the summarized result in an optimized table or document for reading. When we say this projection is asynchronous, it means it happens in the background, decoupled from the exact moment the user performs an action in the system.

In practice, the flow works like this: the user clicks buy, the purchase event is saved instantly in the event store, and a success response is returned to the client in milliseconds, without waiting for report calculations or inventory updates. Next, an autonomous background process captures this new event, updates the read database, and leaves everything ready for the next query. This decoupling ensures that traffic spikes on the read side do not crash the writing engine, isolating responsibilities gracefully.

Choosing the ideal NoSQL database for high availability and scalability

To support massive event flows and fast projections, high-availability NoSQL databases emerge as natural partners due to their schema flexibility and distributed capacity across multiple servers. Unlike rigid relational databases, where adding a new column requires altering entire tables, document-oriented or column-family NoSQL databases allow storing complex JSON structures that evolve alongside the application, facilitating the modeling of read views.

In practice, when choosing a technology like MongoDB, Cassandra, or DynamoDB, we must weigh the trade-offs of consistency and partitioning. The database chosen to store events must guarantee absolute durability and extremely fast sequential writes, while the projection database must support ultra-low latency reads. Geographic replication and automatic partitioning of these NoSQL databases prevent single points of failure, ensuring the system remains operational even if an entire datacenter goes offline.

Handling eventual consistency and its impact on user experience

When adopting asynchronous projections, we enter the territory of eventual consistency, a concept that scares developers accustomed to the rigidity of traditional relational databases. In simple terms, eventual consistency means data is not instantly identical across the entire system right after writing; there is a small time window, measured in milliseconds or seconds, between the event happening and the projection reflecting that change on the user's screen.

In practice, this requires special care in interface design and business logic to prevent confusion. If a user updates their profile and immediately reloads the page, they might not see the change right away if the asynchronous projection has a slight processing backlog. To mitigate this effect, engineers use strategies like optimistic UI updates on the client side, temporarily maintaining local state until the backend confirms that the complete projection cycle has finished.

Practical step-by-step to implement an event consumer in Node.js

To illustrate the mechanics on the development bench, let's structure a simple component in Node.js that consumes raw events from a bus and updates a projection in a document-oriented NoSQL database. The code below demonstrates listening logic, idempotency handling, and decoupled writing of the projected state.

const { MongoClient } = require('mongodb');

async function processOrderEvent(event, db) {
  const collection = db.collection('projected_orders');
  
  // Idempotency check to avoid duplicate processing
  const existing = await collection.findOne({ eventId: event.id });
  if (existing) {
    console.log('Event already processed previously.');
    return;
  }

  // Projection update based on event type
  if (event.type === 'ORDER_CREATED') {
    await collection.updateOne(
      { orderId: event.payload.orderId },
      {
        $set: {
          status: 'CREATED',
          customer: event.payload.customer,
          total: event.payload.total,
          updatedAt: new Date()
        },
        $setOnInsert: { createdAt: new Date() }
      },
      { upsert: true }
    );
  }
}

module.exports = { processOrderEvent };

This code snippet encapsulates the core of a robust asynchronous projection. The idempotency check prevents network failures and message resends from corrupting the read database state, ensuring that the exact same event processed twice produces the exact same final result.

Common pitfalls and mitigation strategies in production

Implementing event-driven systems and asynchronous projections in production environments requires extreme attention to silent failures and message loss. The most common issue is event out-of-order delivery: if the network delays an earlier event and delivers a later one first, the projection might calculate an invalid state, such as sending a confirmation email before the order is officially created.

To shield the architecture against these scenarios, it is crucial to implement strict sequence numbers per entity or precise timestamps at the source. Additionally, maintaining monitoring tools and alerts for bottlenecks in the processing queue ensures that the engineering team identifies backed-up queues before end-users notice any perceptible slowness in the system.

Final considerations on resilience and data-driven architectures

The combination of Event Sourcing with asynchronous projections in NoSQL databases represents one of the most powerful approaches for building scalable, auditable, and resilient systems in modern software engineering. Although it brings an additional layer of conceptual and operational complexity compared to traditional CRUD models, the benefits in terms of decoupling, query flexibility, and evolution capability far outweigh initial costs.

Ultimately, mastering this architectural pattern empowers development teams to handle massive traffic volumes and constant business requirement changes without compromising historical data integrity. The secret to success lies in careful planning of context boundaries, rigorous idempotency handling, and pragmatic acceptance of eventual consistency principles.