NV
NordVarg
ServicesTechnologiesIndustriesCase StudiesBlogAboutContact
Get Started

Footer

NV
NordVarg

Software Development & Consulting

GitHubLinkedInTwitter

Services

  • Product Development
  • Quantitative Finance
  • Financial Systems
  • ML & AI

Technologies

  • C++
  • Python
  • Rust
  • OCaml
  • TypeScript
  • React

Company

  • About
  • Case Studies
  • Blog
  • Contact

© 2025 NordVarg. All rights reserved.

September 28, 2024
•
NordVarg Team
•

Event Sourcing in Financial Systems: Patterns and Practices

How event sourcing provides auditability, temporal queries, and debugging superpowers in financial applications

ArchitectureEvent SourcingCQRSFinancial SystemsArchitecture
5 min read
Share:

Why Event Sourcing for Finance?#

Financial systems have unique requirements:

  • Auditability: Every change must be traceable
  • Compliance: Regulatory reporting requires historical data
  • Debugging: Reproduce exact system state at any point in time
  • Analytics: Understand how data evolved

Event sourcing naturally satisfies all these requirements.

What is Event Sourcing?#

Instead of storing current state, store the events that led to that state:

typescript
1// Traditional approach: Store current state
2interface Account {
3  id: string;
4  balance: number;
5  updatedAt: Date;
6}
7
8// Event sourcing: Store events
9type AccountEvent = 
10  | { type: 'AccountOpened'; accountId: string; initialDeposit: number }
11  | { type: 'MoneyDeposited'; accountId: string; amount: number; timestamp: Date }
12  | { type: 'MoneyWithdrawn'; accountId: string; amount: number; timestamp: Date };
13

Current state is computed by replaying events:

typescript
1function computeBalance(events: AccountEvent[]): number {
2  return events.reduce((balance, event) => {
3    switch (event.type) {
4      case 'AccountOpened':
5        return event.initialDeposit;
6      case 'MoneyDeposited':
7        return balance + event.amount;
8      case 'MoneyWithdrawn':
9        return balance - event.amount;
10    }
11  }, 0);
12}
13

Key Benefits#

1. Complete Audit Trail#

Every change is permanently recorded:

sql
1SELECT * FROM events 
2WHERE aggregate_id = 'account-123'
3ORDER BY timestamp;
4
5-- Results:
6-- 2024-01-01 10:00:00 | AccountOpened    | { initialDeposit: 10000 }
7-- 2024-01-15 14:30:00 | MoneyDeposited   | { amount: 5000 }
8-- 2024-02-01 09:15:00 | MoneyWithdrawn   | { amount: 2000 }
9-- 2024-02-10 16:45:00 | MoneyDeposited   | { amount: 3000 }
10

2. Temporal Queries#

Answer questions like "What was the balance on January 31st?"

typescript
1function balanceAt(events: AccountEvent[], date: Date): number {
2  const relevantEvents = events.filter(e => e.timestamp <= date);
3  return computeBalance(relevantEvents);
4}
5
6// What was the balance on January 31st?
7const balance = balanceAt(events, new Date('2024-01-31'));
8// Answer: 15000 (10000 + 5000)
9

3. Event Replay for Debugging#

Production bug? Replay events locally to reproduce:

typescript
1// Replay production events in test environment
2const productionEvents = fetchEventsFromProduction('account-123');
3const testAccount = new Account();
4productionEvents.forEach(event => testAccount.apply(event));
5
6// Now debug with exact production state
7

Implementation Patterns#

Pattern 1: Event Store#

Store events in append-only log:

typescript
1class EventStore {
2  async append(streamId: string, event: Event): Promise<void> {
3    await db.query(
4      `INSERT INTO events (stream_id, event_type, data, timestamp)
5       VALUES ($1, $2, $3, NOW())`,
6      [streamId, event.type, JSON.stringify(event)]
7    );
8  }
9  
10  async getStream(streamId: string): Promise<Event[]> {
11    const rows = await db.query(
12      `SELECT * FROM events 
13       WHERE stream_id = $1 
14       ORDER BY timestamp`,
15      [streamId]
16    );
17    return rows.map(r => JSON.parse(r.data));
18  }
19}
20

Pattern 2: Snapshots#

Replaying thousands of events is slow. Use snapshots:

typescript
1class SnapshotStore {
2  async saveSnapshot(
3    streamId: string, 
4    state: any, 
5    version: number
6  ): Promise<void> {
7    await db.query(
8      `INSERT INTO snapshots (stream_id, state, version)
9       VALUES ($1, $2, $3)
10       ON CONFLICT (stream_id) DO UPDATE SET state = $2, version = $3`,
11      [streamId, JSON.stringify(state), version]
12    );
13  }
14  
15  async loadState(streamId: string): Promise<any> {
16    // Load latest snapshot
17    const snapshot = await db.query(
18      `SELECT * FROM snapshots WHERE stream_id = $1`,
19      [streamId]
20    );
21    
22    // Replay events since snapshot
23    const events = await eventStore.getStream(
24      streamId, 
25      snapshot.version
26    );
27    
28    return applyEvents(snapshot.state, events);
29  }
30}
31

Pattern 3: CQRS (Command Query Responsibility Segregation)#

Separate write model (events) from read model (projections):

typescript
1// Write side: Append events
2class AccountCommandHandler {
3  async deposit(accountId: string, amount: number): Promise<void> {
4    const event = {
5      type: 'MoneyDeposited',
6      accountId,
7      amount,
8      timestamp: new Date()
9    };
10    await eventStore.append(accountId, event);
11  }
12}
13
14// Read side: Materialized view
15class AccountProjection {
16  async updateFromEvent(event: AccountEvent): Promise<void> {
17    switch (event.type) {
18      case 'MoneyDeposited':
19        await db.query(
20          `UPDATE accounts 
21           SET balance = balance + $1 
22           WHERE id = $2`,
23          [event.amount, event.accountId]
24        );
25        break;
26    }
27  }
28}
29

Real-World Example: Transaction Processing#

Here's how we implemented event sourcing for a banking system:

typescript
1// Domain events
2type TransactionEvent =
3  | { type: 'TransactionInitiated'; id: string; from: string; to: string; amount: number }
4  | { type: 'FundsReserved'; id: string; from: string; amount: number }
5  | { type: 'FundsTransferred'; id: string; to: string; amount: number }
6  | { type: 'TransactionCompleted'; id: string; completedAt: Date }
7  | { type: 'TransactionFailed'; id: string; reason: string };
8
9// Aggregate root
10class Transaction {
11  private events: TransactionEvent[] = [];
12  
13  initiate(from: string, to: string, amount: number): void {
14    this.apply({
15      type: 'TransactionInitiated',
16      id: uuid(),
17      from,
18      to,
19      amount
20    });
21  }
22  
23  private apply(event: TransactionEvent): void {
24    this.events.push(event);
25    // Update internal state based on event
26  }
27  
28  getUncommittedEvents(): TransactionEvent[] {
29    return this.events;
30  }
31}
32

Challenges and Solutions#

Challenge 1: Event Schema Evolution#

Events are immutable, but requirements change.

Solution: Event upcasting:

typescript
1class EventUpcaster {
2  upcast(event: any): Event {
3    // V1: { type: 'MoneyDeposited', amount: number }
4    // V2: { type: 'MoneyDeposited', amount: number, currency: string }
5    
6    if (event.version === 1) {
7      return {
8        ...event,
9        currency: 'USD', // Add default
10        version: 2
11      };
12    }
13    return event;
14  }
15}
16

Challenge 2: Performance#

Event replay can be slow for long-lived aggregates.

Solution: Snapshots every N events:

typescript
1const SNAPSHOT_FREQUENCY = 100;
2
3if (events.length % SNAPSHOT_FREQUENCY === 0) {
4  await snapshotStore.save(streamId, currentState, events.length);
5}
6

Challenge 3: Eventual Consistency#

Read models are eventually consistent with write model.

Solution: Version tracking and conflict resolution:

typescript
1class ReadModel {
2  private lastProcessedVersion = 0;
3  
4  async update(event: Event): Promise<void> {
5    if (event.version <= this.lastProcessedVersion) {
6      return; // Already processed
7    }
8    
9    // Apply update
10    await this.applyEvent(event);
11    this.lastProcessedVersion = event.version;
12  }
13}
14

When to Use Event Sourcing#

✅ Good fit:

  • Financial systems (audit trail required)
  • Systems with complex business logic
  • Need for temporal queries
  • Compliance-heavy domains

❌ Poor fit:

  • Simple CRUD applications
  • High-volume, low-value events
  • Team unfamiliar with the pattern

Conclusion#

Event sourcing is powerful for financial systems:

  • Complete audit trail
  • Temporal queries
  • Debugging superpowers
  • Natural fit for finance domain

Start small, master the basics, then expand.

Resources#

  • Event Sourcing Basics
  • CQRS Journey
  • Versioning in an Event Sourced System

Building event-sourced systems? Get in touch to discuss your architecture.

NT

NordVarg Team

Technical Writer

NordVarg Team is a software engineer at NordVarg specializing in high-performance financial systems and type-safe programming.

Event SourcingCQRSFinancial SystemsArchitecture

Join 1,000+ Engineers

Get weekly insights on building high-performance financial systems, latest industry trends, and expert tips delivered straight to your inbox.

✓Weekly articles
✓Industry insights
✓No spam, ever

Related Posts

Oct 28, 2024•7 min read
Microservices vs Monoliths: What We Learned Building Trading Systems
Practical insights on when to use microservices and when a well-structured monolith is the better choice
ArchitectureArchitectureMicroservices
Dec 30, 2024•6 min read
Building a Real-Time Risk Dashboard: From Data to Visualization
Architecturerisk-managementreal-time
Dec 29, 2024•5 min read
Time Synchronization in Distributed Trading Systems
Architecturetime-synchronizationptp

Interested in working together?