Architectural Paradigm Shift: Why Traditional CRUD Fails at Scale

In high-throughput enterprise systems, classic Create-Read-Update-Delete (CRUD) architectures suffer from severe structural limitations: relational locks during concurrent updates, lack of auditability, and database contention between intensive write streams and analytical read patterns. By decoupling state mutation from query representations using Command Query Responsibility Segregation (CQRS) combined with Event Sourcing (ES), engineering teams build systems capable of infinite replayability, linear horizontal read scaling, and zero write lock contention.

Published by Saizul Amin based on production benchmark telemetry across distributed financial ledgers and SaaS transaction pipelines.

Core Architectural Concepts: Event Sourcing & CQRS

In Event Sourcing, application state is not stored as mutable rows in a SQL database. Instead, every state change is recorded as an immutable domain event in an append-only append log (Event Store). Current state is reconstructed by projecting past events sequentially.

Dimension Traditional CRUD Architecture Event Sourcing + CQRS Pattern
State Representation Current snapshot only (overwrites previous state) Immutable chronological sequence of domain events
Write Scalability Constrained by ACID row-level and table-level locks High append-only write throughput without row locks
Auditability & History Requires external audit log tables or trigger hacks Inherent 100% complete audit trail and temporal queries
Read Model Flexibility Normalized relational schema or complex denormalized views Specialized projections optimized for specific UI screens
Failure Recovery Database point-in-time snapshot restore Replay event stream with patched projection logic

Production Implementation: TypeScript Aggregate Root & Projections

Below is a production-grade implementation of a financial account aggregate using TypeScript, enforcing business invariants and emitting domain events.


// Domain Events Definition
export interface DomainEvent {
  readonly aggregateId: string;
  readonly eventType: string;
  readonly timestamp: number;
  readonly version: number;
  readonly payload: Record<string, any>;
}

export interface MoneyDepositedEvent extends DomainEvent {
  readonly eventType: '\''MoneyDeposited'\'';
  readonly payload: {
    readonly amountCents: number;
    readonly transactionReference: string;
  };
}

export interface MoneyWithdrawnEvent extends DomainEvent {
  readonly eventType: '\''MoneyWithdrawn'\'';
  readonly payload: {
    readonly amountCents: number;
    readonly transactionReference: string;
  };
}

// Account Aggregate Root
export class BankAccountAggregate {
  private _id: string;
  private _balanceCents: number = 0;
  private _version: number = 0;
  private _uncommittedEvents: DomainEvent[] = [];

  constructor(id: string) {
    this._id = id;
  }

  get id(): string { return this._id; }
  get balanceCents(): number { return this._balanceCents; }
  get version(): number { return this._version; }

  // Command Handler: Withdraw
  public withdraw(amountCents: number, reference: string): void {
    if (amountCents <= 0) {
      throw new Error("Withdrawal amount must be strictly positive");
    }
    if (this._balanceCents < amountCents) {
      throw new Error(`Insufficient funds: Balance ${this._balanceCents} cents < Requested ${amountCents} cents`);
    }

    const event: MoneyWithdrawnEvent = {
      aggregateId: this._id,
      eventType: '\''MoneyWithdrawn'\'',
      timestamp: Date.now(),
      version: this._version + 1,
      payload: { amountCents, transactionReference: reference }
    };

    this.apply(event);
    this._uncommittedEvents.push(event);
  }

  // State Transition Logic
  private apply(event: DomainEvent): void {
    switch (event.eventType) {
      case '\''MoneyDeposited'\'':
        this._balanceCents += event.payload.amountCents;
        break;
      case '\''MoneyWithdrawn'\'':
        this._balanceCents -= event.payload.amountCents;
        break;
    }
    this._version = event.version;
  }

  public loadFromHistory(history: DomainEvent[]): void {
    for (const event of history) {
      this.apply(event);
    }
  }

  public getUncommittedEvents(): DomainEvent[] {
    return [...this._uncommittedEvents];
  }

  public clearUncommittedEvents(): void {
    this._uncommittedEvents = [];
  }
}

Handling Read Projections and Eventual Consistency

In CQRS, read projections asynchronously subscribe to the event stream published by the event bus. A Redis or PostgreSQL read replica updates query-optimized representations:


export class AccountSummaryProjection {
  constructor(private readonly redisClient: any) {}

  public async handle(event: DomainEvent): Promise<void> {
    const key = `view:account:${event.aggregateId}`;
    
    if (event.eventType === '\''MoneyDeposited'\'') {
      await this.redisClient.hincrby(key, '\''balanceCents'\'', event.payload.amountCents);
      await this.redisClient.hset(key, '\''lastUpdated'\'', event.timestamp);
    } else if (event.eventType === '\''MoneyWithdrawn'\'') {
      await this.redisClient.hincrby(key, '\''balanceCents'\'', -event.payload.amountCents);
      await this.redisClient.hset(key, '\''lastUpdated'\'', event.timestamp);
    }
  }
}

Operational Pitfalls: Concurrency and Schema Evolution

When implementing Event Sourcing, two major architectural challenges must be proactively addressed:

  • Optimistic Concurrency Control (OCC): Ensure that when appending new events, the event store verifies that the expected aggregate version matches the stored version. If another worker appended an event concurrently, reject with a concurrency conflict exception and trigger an aggregate replay retry.
  • Event Schema Versioning (Upcasting): Domain events are immutable once stored. As business logic evolves, old event schemas must be mapped to current formats using in-flight Upcasters rather than destructive historical mutations.

Adopting CQRS and Event Sourcing eliminates complex data anomalies and empowers engineering teams to build self-healing, auditable distributed enterprise backends.

NEW BLOGS. REAL STRATEGIES. REAL RESULTS.

Join our community and receive powerful SEO tips, web optimization guides, and growth strategies as soon as they’re published.

Don’t just read blogs — stay ahead of the competition.

We don’t spam! Read our privacy policy for more info.

Share.
Leave A Reply