Module A-12·27 min read

Aggregates, invariants, Ports & Adapters, and CQRS applied to banking ledgers — keeping domain logic clean under extreme architectural complexity.

Module 11 — Enterprise Architecture for State-Heavy Systems: DDD & Clean Architecture

What this module covers: A blockchain indexer processing 50,000 events/second will accumulate complex business rules over time: transaction validity invariants, account balance constraints, settlement reconciliation logic, fraud detection rules. Without architectural discipline, these rules scatter across route handlers, database queries, and event listeners — untestable, fragile, and impossible to reason about under load. This module covers Domain-Driven Design applied to state-heavy systems, Clean Architecture for transport-agnostic domain logic, and CQRS for systems where the read model and write model need to evolve independently.


Why Architecture Matters More at Scale

At low throughput (< 1K events/sec), architectural shortcuts are invisible. Validation in route handlers, business logic in SQL queries, domain concepts scattered across layers — these are maintainability problems, not performance problems.

At high throughput, architectural shortcuts become performance problems:

javascript

Problems:

  • Untestable: you need a running HTTP server and database to test the business logic
  • Uncacheable: the business rules are coupled to the SQL and HTTP transport
  • Unreusable: the same logic cannot be reused by a gRPC endpoint or a Kafka consumer
  • Unrefactorable: changing the validation requires touching the route handler

Domain-Driven Design Core Concepts

DDD provides vocabulary and patterns for modeling complex business domains in code. For a payment system, the key concepts are:

Entities

Objects with identity that persists over time. An account is an entity — it has an ID, and its state changes.

typescript

Why this matters for high-throughput systems:

The invariants are in the domain object and enforced before any I/O happens. At 50K payments/second, you want to reject invalid payments with zero database roundtrips. The Account.debit() method checks balance without touching the database — if it throws, the payment is rejected before any SQL runs.

Value Objects

Immutable objects without identity. An Amount is a value object — two amounts of 100 USD are identical regardless of which "instance" they are.

typescript

Aggregates: The Consistency Boundary

An aggregate is a cluster of entities and value objects that must be changed together atomically. The aggregate root is the entry point — external code can only access the aggregate through the root.

For a UPI payment:

typescript

Clean Architecture: The Dependency Rule

Clean Architecture enforces that dependencies only point inward — from infrastructure to application to domain. The domain never knows about HTTP, Kafka, or PostgreSQL.

text

Ports and Adapters (Hexagonal Architecture)

typescript
typescript

The transport-agnostic domain: the same ProcessPaymentUseCase works whether triggered by:

typescript

The domain and application layers are untouched. Only the infrastructure adapter changes.


Dependency Injection: Wiring the Layers

typescript

Swap PostgreSQL for an in-memory repository for unit tests without touching any business logic:

typescript

Tests run in milliseconds. No database. No network. Business logic tested in isolation.


CQRS: Separate Write and Read Models

Command Query Responsibility Segregation (CQRS) separates the model that handles writes (commands) from the model that handles reads (queries).

For a payment ledger:

  • Write model: normalized, ACID-consistent. Enforces invariants. Optimized for integrity.
  • Read model: denormalized, eventually consistent. Pre-computed aggregations. Optimized for query speed.
typescript

The read model is updated by an event consumer that processes domain events from Kafka:

typescript

CQRS benefits for high-throughput systems:

  • Write and read databases can be scaled independently
  • Read queries never compete with write transactions
  • The read model can be optimized for specific query patterns (denormalized, indexed differently)
  • If the read model becomes stale, you can rebuild it by replaying events from Kafka

Anti-Corruption Layer for Blockchain RPC

When your indexer calls a third-party blockchain RPC node (Ethereum's eth_getBlockByNumber, Supra's block API), the response structure is the external system's format. Letting that format leak into your domain creates coupling — if the RPC response format changes, your domain objects break.

typescript

The domain Block object uses your types (Buffer for hashes, BigInt for amounts). If Ethereum changes their API response format, you update translateEthBlock — nowhere else.


Production Incident: Domain Logic in a SQL Query

Context: A banking ledger service. Balance calculations were done in SQL via a stored procedure called by the application:

sql

What happened:

A regulatory requirement changed: transactions pending for more than 48 hours must be included in the balance calculation as "provisional debits." The business rule changed from:

Balance = sum of settled transactions

To:

Balance = sum of settled transactions + sum of pending debits older than 48 hours

The stored procedure was updated. But the update missed that pending credits older than 48 hours should also be included (oversight). The bug was in production for 11 days before a reconciliation audit caught it. 847 accounts had incorrect balance calculations.

Root cause: the business rule was in SQL, not in domain code. SQL is not testable with unit tests. The domain rule was invisible to developers who tested in isolation.

The fix — move the rule to the domain:

typescript

The test caught the exact bug — credits and debits treated consistently — before any code reached production.


Summary

ConceptKey Takeaway
EntityObject with identity that changes over time. Enforces its own invariants.
Value ObjectImmutable, identity-less. Money(5000, 'INR') is always equal to another Money(5000, 'INR').
AggregateCluster of entities with one root. Changed atomically. Emits domain events.
InvariantRule the domain enforces. Never in SQL. Always in domain code. Testable without I/O.
Clean ArchitectureDomain → Application → Infrastructure. Dependencies flow inward only.
Ports and AdaptersPort = interface the domain defines. Adapter = infrastructure implementation. Swappable.
DI containerWire adapters to ports at startup. Swap for test doubles in tests.
CQRSWrite model: normalized, strict, ACID. Read model: denormalized, eventual, fast.
Anti-corruption layerTranslate external formats at the boundary. External changes never reach domain objects.
Domain eventsEmitted by aggregates after state changes. Published to Kafka by the infrastructure layer.

The architecture is clean. Module 12 covers how to keep it healthy under production load — CPU profiling, flame graph reading, ELU monitoring, and the full clinic.js diagnostic workflow for Node.js services under high-throughput stress.

Next: Module 12 — Production Observability, Performance Profiling & Flame Graphs →


Knowledge Check

In Domain-Driven Design (DDD), what is the difference between an Entity and a Value Object?


Which rule is fundamental to Clean Architecture?


What is the primary benefit of Command Query Responsibility Segregation (CQRS) in a state-heavy system?

Test your knowledge with more question sets

Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.