Event Sourcing in Production: EventStoreDB, Apache Kafka & CQRS Materialized Views in 2026

A comprehensive distributed systems engineering guide to Event Sourcing and CQRS in 2026: EventStoreDB aggregate streams, Apache Kafka projections, schema evolution upcasters, and snapshotting.
Event Sourcing in Production: EventStoreDB, Apache Kafka & CQRS Materialized Views in 2026
In traditional database architectures (CRUD), application state is stored as a mutable snapshot of the present:
- When a user updates their shipping address or withdraws money from a bank account, an
UPDATE users SET balance = 500 WHERE id = 984overwrites the past. - The previous historical data is erased forever unless captured in brittle audit tables.
- Reconstructing what the database looked like at 2:15 PM last Tuesday for compliance, financial dispute resolution, or fraud forensics is impossible.
In 2026, mission-critical enterprise systems (fintech, banking, logistics, insurance, and medical EHR) rely on Event Sourcing and Command Query Responsibility Segregation (CQRS).
Under Event Sourcing, Application state is never mutated; instead, every business transaction is stored as an immutable, append-only sequence of domain events:
AccountOpened,MoneyDeposited,CardSwiped,AddressUpdated.- The current state is simply a projection calculated by replaying the event stream from the beginning of time.
- EventStoreDB handles strong-consistency aggregate writes and optimistic concurrency, while Apache Kafka broadcasts events to build ultra-fast, read-optimized CQRS Materialized Views (in PostgreSQL, Elasticsearch, or ClickHouse).
In this deep backend engineering guide, we break down production Event Sourcing patterns, schema evolution with Runtime Upcasting, and Aggregate Snapshotting based on high-scale financial platforms engineered at MojoStudio.
1. The 2026 Event Sourcing & CQRS Production Architecture
+-----------------------------------------------------------------------------------------+
| Enterprise Event Sourcing & CQRS Topology |
+-----------------------------------------------------------------------------------------+
[COMMAND / WRITE SIDE (Strong Consistency)]
[User / API Client] ---> (Command: 'WithdrawMoney { amount: $50 }')
|
v
+-----------------------------------------------------------------+
| Command Handler & Domain Aggregate (Account Aggregate): |
| 1. Replays events to verify invariant: balance >= $50. |
| 2. Appends new event: 'MoneyWithdrawn { amount: $50, v: 4 }' |
| 3. Optimistic Concurrency Check (Expected Version: 3). |
+--------------------------------+--------------------------------+
|
v (Appends to Immutable Stream: /account-984)
[WRITE STORE: EventStoreDB / PostgreSQL Outbox]
|
v (Real-Time Change Data Capture / Event Stream)
[EVENT BROKER: Apache Kafka / Redpanda]
|
+------------------------+------------------------+
| (Subscription 1) | (Subscription 2)
v v
+---------------------------------+ +---------------------------------+
| PROJECTION WORKER (PostgreSQL) | | PROJECTION WORKER (Elasticsearch|
| - Updates SQL table: | | - Indexes transaction search: |
| 'account_balances_view' | | 'transactions_audit_index' |
+----------------+----------------+ +----------------+----------------+
| |
v v
[READ SIDE: Fast REST / GraphQL Queries] [READ SIDE: Full-Text Audit Logs]2. EventStoreDB vs Apache Kafka: Choosing the Right Tool
A common architectural disaster is attempting to use Apache Kafka as the primary transactional Event Store:
- Kafka is designed for high-throughput topic partitions, not millions of fine-grained per-aggregate event streams (
/order-101,/order-102). - Kafka lacks native per-stream Optimistic Concurrency Control (OCC).
+-----------------------------------------------------------------------------------------+
| EventStoreDB vs Apache Kafka Responsibility Matrix |
+-----------------------------------------------------------------------------------------+| Dimension | EventStoreDB (Purpose-Built Event Store) | Apache Kafka (Event Streaming Platform) |
|---|---|---|
| Primary Domain | Write Side (Domain Aggregates) | Read Side Integration & Downstream Sync |
| Stream Granularity | Millions of individual aggregate streams | Topics with fixed partition counts |
| Concurrency Control | Native Optimistic Concurrency (expectedRevision) | Difficult (Requires custom lock tables) |
| Replay by Entity | Instant (Read stream /account-984) | Must scan whole topic partition |
| Materialized Views | Catch-up subscriptions | Kafka Connect + Kafka Streams Engine |
3. Aggregate Lifecycle & Optimistic Concurrency in TypeScript
Here is a production TypeScript implementation of a Bank Account Aggregate running on EventStoreDB:
// domain/AccountAggregate.ts
export interface DomainEvent {
type: string;
data: any;
metadata?: any;
}
export class BankAccountAggregate {
public id: string;
public balance: number = 0;
public isClosed: boolean = false;
public version: number = -1;
constructor(id: string) {
this.id = id;
}
// 1. Rehydrate state by replaying historical events
public loadFromHistory(events: DomainEvent[]) {
for (const event of events) {
this.applyEvent(event);
this.version++;
}
}
// 2. Command: Withdraw Money (Enforces Business Invariant!)
public withdrawMoney(amount: number): DomainEvent {
if (this.isClosed) {
throw new Error("Cannot withdraw from a closed account!");
}
if (this.balance < amount) {
throw new Error(`Insufficient funds: Balance is `{this.balance}, requested `{amount}`);
}
const event: DomainEvent = {
type: "MoneyWithdrawn",
data: { accountId: this.id, amount, timestamp: Date.now() },
};
this.applyEvent(event);
return event;
}
// 3. State Mutation Mutator
private applyEvent(event: DomainEvent) {
switch (event.type) {
case "AccountOpened":
this.balance = event.data.initialDeposit;
break;
case "MoneyDeposited":
this.balance += event.data.amount;
break;
case "MoneyWithdrawn":
this.balance -= event.data.amount;
break;
case "AccountClosed":
this.isClosed = true;
break;
}
}
}Writing to EventStoreDB with Optimistic Concurrency:
import { EventStoreDBClient, jsonEvent, NO_STREAM } from "@eventstore/db-client";
const client = EventStoreDBClient.fromConnectionString("esdb://localhost:2113?tls=false");
export async function handleWithdrawCommand(accountId: string, amount: number) {
const streamName = `account-${accountId}`;
// 1. Read existing event stream
const events = client.readStream(streamName);
const aggregate = new BankAccountAggregate(accountId);
const history = [];
for await (const resolvedEvent of events) {
history.push({ type: resolvedEvent.event!.type, data: resolvedEvent.event!.data });
}
aggregate.loadFromHistory(history);
// 2. Execute Business Logic & Produce New Event
const newEvent = aggregate.withdrawMoney(amount);
// 3. Append to EventStoreDB with Optimistic Locking!
// If another thread modified the account in the meantime, it throws WrongExpectedVersion!
await client.appendToStream(streamName, [jsonEvent({ type: newEvent.type, data: newEvent.data })], {
expectedRevision: aggregate.version === -1 ? NO_STREAM : BigInt(aggregate.version),
});
return { success: true, newBalance: aggregate.balance };
}4. Aggregate Snapshotting: Taming Long-Lived Streams
If a high-frequency trading account or IoT sensor accumulates 50,000 events, replaying all 50,000 events on every single command takes 800ms, destroying latency.
Snapshotting calculates and persists the aggregate state every $N$ events (e.g. every 100 events):
+-----------------------------------------------------------------------------------------+
| Aggregate Snapshotting Acceleration Model |
+-----------------------------------------------------------------------------------------+
[STREAM: 10,240 Events Total]
- Events 0 to 10,200: Pre-calculated into Snapshot: { balance: $94,200, isClosed: false }
- Events 10,201 to 10,240: (Only 40 events to replay!)
|
v
[Rehydration Time Drops from 450ms down to 1.8ms!]5. Schema Evolution: Runtime Event Upcasting
In Event Sourcing, you can never alter past events stored on disk—they are immutable legal facts.
When your application schema evolves (e.g., merging firstName and lastName into fullName in UserRegistered.v2), we use an Upcaster Pipeline:
+-----------------------------------------------------------------------------------------+
| Runtime Event Upcaster Architecture |
+-----------------------------------------------------------------------------------------+
[Raw Event on Disk: UserRegistered.v1 { firstName: "Sachin", lastName: "Sharma" }]
|
v (Read by Upcaster Middleware)
[Upcaster Transforms: Appends 'fullName: "Sachin Sharma"', Removes deprecated fields]
|
v
[Aggregate receives modern UserRegistered.v2 Schema seamlessly in memory!]// upcasters/userRegisteredUpcaster.ts
export function upcastUserRegistered(event: DomainEvent): DomainEvent {
if (event.type === "UserRegistered.v1") {
return {
type: "UserRegistered.v2",
data: {
userId: event.data.userId,
fullName: ``{event.data.firstName} `{event.data.lastName}`, // Transformed on the fly!
email: event.data.email,
},
};
}
return event;
}6. Architectural Trade-Offs: CRUD vs Event Sourcing
| Dimension | Traditional CRUD | Event Sourcing & CQRS |
|---|---|---|
| Data Integrity | Past states overwritten & lost | 100% Complete Immutable Historical Audit Log |
| Temporal Querying | Impossible without snapshots | Native ("Time-Travel" to any past second) |
| Read/Write Scaling | Shared DB bottleneck | Independent Scaling (Kafka + Read Projections) |
| Schema Evolution | ALTER TABLE SQL migrations | Upcasting Pipelines & New Projections |
| System Complexity | Low | High (Requires Event Store + Projections) |
Conclusion: Absolute Auditability at Scale
Event Sourcing and CQRS represent the gold standard of distributed data architecture for domains where data loss, audit failure, and concurrency race conditions cannot be tolerated.
By capturing domain transactions as immutable event streams in EventStoreDB, broadcasting events via Apache Kafka, building specialized materialized read projections, and maintaining speed with snapshotting and upcasting, engineering teams achieve total financial auditability, high-throughput writes, and sub-millisecond read queries.
At MojoStudio, our distributed systems engineering team designs enterprise Event Sourcing architectures, EventStoreDB clusters, Kafka streaming pipelines, and CQRS read projections. Contact our team to architect high-integrity event-driven systems today.
Frequently Asked Questions
1. What is Event Sourcing?
Event Sourcing is an architectural pattern where state changes are stored as an immutable, append-only sequence of domain events rather than overwriting current state in a relational database table.
2. What is CQRS (Command Query Responsibility Segregation)?
CQRS separates the application architecture into two models: a Command Model (optimized for writing transactional business logic and enforcing invariants) and a Query Model (optimized for fast, read-only materialized views).
3. Why shouldn't Apache Kafka be used as a primary aggregate Event Store?
Kafka is optimized for high-volume topic partitioning and pub/sub transport, but lacks native per-aggregate stream lookups, fine-grained optimistic concurrency control, and stream rehydration primitives found in purpose-built event stores like EventStoreDB.
4. What is Optimistic Concurrency Control (OCC) in Event Sourcing?
OCC checks that the aggregate's current version matches the expected version before appending a new event. If another concurrent request appended an event in the meantime, the write is rejected to prevent race conditions.
5. What is an Upcaster in Event Sourcing?
An Upcaster is a middleware component that intercepts historical event records during retrieval from disk and transforms their schema on-the-fly to match the latest application code version without mutating past database logs.
6. What is Aggregate Snapshotting?
Snapshotting periodically records the computed in-memory state of an aggregate (e.g. every 100 events) so that rehydration only requires loading the latest snapshot and replaying events that occurred after it, keeping load times sub-millisecond.
7. How does Event Sourcing handle data deletion (e.g. GDPR Right to be Forgotten)?
GDPR compliance is typically solved using Cryptographic Erasure: encrypting each user's PII with a unique per-user encryption key stored in a separate key management service. When a user requests deletion, the key is permanently destroyed, rendering past events unreadable.
8. What is a Projection in CQRS?
A projection is an event consumer worker that listens to domain events and updates a read-optimized database (such as PostgreSQL, MongoDB, or Elasticsearch) to serve high-speed query endpoints.
9. What is Eventual Consistency in CQRS?
Because read projections are updated asynchronously after an event is appended to the write store, there is a brief millisecond delay before the read model reflects the latest write.
10. How does MojoStudio help companies adopt Event Sourcing & CQRS?
MojoStudio engineers custom EventStoreDB architectures, Kafka streaming projections, Axon Framework integrations, and high-performance read models. Explore our Backend & Web Services to learn more.
Frequently Asked Questions
Event Sourcing is an architectural pattern where state changes are stored as an immutable, append-only sequence of domain events rather than overwriting current state in a relational database table.