ADR: Aurora vs DynamoDB for a Double-Entry Ledger in Core Banking
Listen to study
generated on playGenerated only on first play
This ADR evaluates Aurora PostgreSQL and DynamoDB as the persistence engine for a double-entry ledger in a core banking system, weighing strong consistency, access patterns, auditability, and cost. The decision favors Aurora with date-range partitioning and an immutable event layer, acknowledging the horizontal scaling constraints that choice imposes.
Choosing the wrong database for a double-entry ledger is not a performance problem — it is a correctness problem. In this ADR, I document the real forces that shaped the decision between Aurora PostgreSQL and DynamoDB as the persistence engine for a core banking system, and explain why strong consistency and transactional semantics outweighed horizontal scalability in this context.
Case Facts
- System
- Core banking — double-entry ledger (composite scenario)
- Domain
- Data / Financial persistence
- Estimated transaction volume
- ~500k ledger entries/day (reference estimate)
- Regulatory requirement
- Record immutability, audit trail, daily reconciliation
- Evaluated stack
- Amazon Aurora PostgreSQL, Amazon DynamoDB, AWS Lambda, Amazon EventBridge
- AWS Region
- us-east-1 (primary) + us-west-2 (DR)
- Decision status
- Accepted
- Authors
- Fernando F. Azevedo (Solutions Architect)
Context and Forces at Play
A double-entry ledger is, by definition, a system of invariants. Every financial transaction produces exactly two entries — debit and credit — whose values must be equal and opposite. The algebraic sum of all balances must be zero. This property is non-negotiable: any violation, even temporary, represents an accounting error with serious regulatory and operational consequences.
The system in question must guarantee:
- Atomicity — debit and credit are written together or neither is written.
- Isolation — balance reads during an in-flight transaction do not return intermediate states.
- Immutability — written entries cannot be modified; corrections are made by reversal (new entry).
- Auditability — every operation must have a traceable trail with timestamp, operator identifier, and reference to the originating business event.
Beyond these correctness invariants, there are operational forces: entry volume grows with the customer base, read patterns are heterogeneous (point-in-time balance, paginated statement, period-based reconciliation reports), and cost must be justifiable in a financial product with controlled margins.
It was in this scenario that the team faced the classic choice: a relational database with native ACID versus a massively scalable NoSQL database. The pressure to adopt DynamoDB came from engineering teams that operated other platform services with it successfully. The pressure to keep PostgreSQL came from product and compliance teams, who knew the relational model well and distrusted eventual consistency at any layer of the financial stack.
Why Access Patterns Matter More Than Throughput
The most common argument for DynamoDB in financial systems is throughput: the ability to scale to millions of operations per second without degradation. It is a valid argument for very high-volume payment systems — card processors, for example. But for a mid-sized institution's core banking ledger, that argument is, in practice, a red herring.
The real problem is not write throughput. It is the diversity of read patterns.
A ledger needs to answer at least four fundamentally different query classes:
- Current account balance — point-in-time read, latency-critical, high frequency.
- Paginated statement — range scan by
account_id + timestamp, ordered, with optional filters by entry type. - Daily reconciliation — aggregation of all entries in a period, grouped by account and type, potentially crossing millions of rows.
- Specific transaction audit — lookup by
transaction_id, returning both sides of the entry and the associated business event.
In DynamoDB, each of these patterns requires a different key design. The single-access model via partition key + sort key means you need multiple Global Secondary Indexes (GSIs) or data duplication to cover all patterns. GSIs in DynamoDB have eventual consistency by default — acceptable for many cases, but not for balance reads in a system that must guarantee the balance reflects exactly the state after the last confirmed transaction.
You can work around this with strongly consistent reads on the primary index and careful modeling. But the design and operational cost of that solution begins to exceed the cost of simply using a relational database with well-designed indexes. The complexity does not disappear — it migrates from the database into application code and into modeling conventions that must be maintained by the entire team.
Aurora PostgreSQL, on the other hand, treats all these patterns as standard SQL queries. The query optimizer handles execution complexity. The team maintains a single mental model — tables, indexes, transactions — rather than multiple access patterns encoded in composite keys.
Decision Matrix: Aurora PostgreSQL vs DynamoDB
Aurora PostgreSQL
- Native ACID with configurable serializable isolation — no workarounds for double-entry atomicity
- Standard SQL covers all ledger read patterns without data duplication
- Date-range partitioning (declarative in PostgreSQL 10+) maintains reconciliation performance
- Aurora Global Database offers multi-region DR with RPO < 1s
- Familiarity for product and compliance teams — lower operational risk
- Vertical scaling has a practical ceiling — large instances are expensive and have IOPS limits
- Connections are stateful — connection pooling (RDS Proxy) is required in serverless environments
- Horizontal sharding requires significant architectural change if volume exceeds single-instance capacity
- Instance cost is fixed regardless of usage — inefficient for highly variable workloads
Preferred choice for this case — correctness and operability outweigh scaling limitations at projected volume
DynamoDB
- Unlimited horizontal scaling without shard management — massive write throughput
- Consistently low read/write latency at P99 (single-digit ms)
- Pay-per-request cost model eliminates idle capacity cost
- DynamoDB Streams + Kinesis for real-time event capture without polling
- No connection management — native to serverless architectures
- Transactions (TransactWriteItems) limited to 100 items and 4MB — sufficient for simple double-entry, but restrictive for compound entries
- GSIs have eventual consistency — balance reads via GSI are not safe without explicit design
- Analytical and reconciliation queries require export to S3 + Athena or DynamoDB Streams — additional latency
- Complex data modeling — multiple access patterns require GSIs or single-table design with key overloading
- GSI cost is proportional to replicated data volume — can exceed Aurora at medium volumes with multiple indexes
Rejected as primary ledger engine — suitable as downstream event store or balance cache
Detailed Technical Comparison
| Criterion | Aurora PostgreSQL | DynamoDB | |
|---|---|---|---|
| Double-entry atomicity | Native BEGIN/COMMIT — no additional code | TransactWriteItems — works, but has size limits and 2x WCU cost | — |
| Balance read isolation | READ COMMITTED (default) or REPEATABLE READ — configurable per transaction | Strongly consistent reads on primary index only — GSIs are eventually consistent | — |
| Record immutability | Enforced via trigger or application policy — not native, but auditable via WAL | Enforced via IAM policy (deny UpdateItem/DeleteItem) — simpler to implement | — |
| Period-based reconciliation | SQL query with date partition — executes in-database without ETL | Requires export to S3 + Athena or processing via Streams — additional latency and cost | — |
| Write scale | Vertical (up to ~200k IOPS on Aurora I/O-Optimized) — sufficient for ~500k entries/day | Unlimited horizontal — only needed at card processor volumes (>10M tx/day) | — |
| Estimated cost (reference) | ~$400-800/month (db.r6g.xlarge Multi-AZ + storage) — predictable fixed cost | ~$200-600/month (on-demand WCU/RCU + GSIs) — variable, can exceed Aurora with multiple GSIs | — |
| Operational learning curve | Low for teams with relational background — SQL, indexes, explain plan | High — single-table modeling, GSI design, hot partition avoidance, capacity planning | — |
The Immutability and Auditability Question
Neither database offers record immutability as a native primitive in the sense that financial regulators require. Both allow deletion and update by default. The difference lies in where and how you implement the constraint.
In DynamoDB, the cleanest approach is via IAM policy: you create an application role that has PutItem permission but not UpdateItem or DeleteItem. This is elegant and hard to accidentally bypass. The problem is that it does not solve the audit question of who wrote what and when with database semantics — you depend on CloudTrail for that, and CloudTrail has latency and does not capture item content.
In Aurora PostgreSQL, the approach is different: you implement a BEFORE UPDATE OR DELETE trigger that rejects any operation on ledger tables (except by a specific system role for audited reversals). PostgreSQL's WAL (Write-Ahead Log), combined with Aurora's continuous backup, creates a low-level audit trail that captures every change with microsecond precision. For high-level auditing, you add an audit_log table populated by trigger, or use an extension like pgaudit.
The combination of immutability trigger + pgaudit + Aurora Backtrack (which allows rewinding the database to any point in the last 72 hours) creates an auditability layer that is difficult to replicate in DynamoDB without additional infrastructure.
There is a pattern I consider superior for both cases: event sourcing with projection. The primary ledger is a table of immutable events (ledger_events) where each row represents an accounting entry and is never modified. Balances are projections computed over those events — either in real time via query, or materialized in a balance table updated by trigger. This pattern works well in Aurora and would be the only safe way to use DynamoDB for this case. But if you are implementing event sourcing anyway, Aurora offers more tools to maintain projection consistency within the same transaction.
Decision
Core banking system with ~500k ledger entries/day, strong consistency requirement, multiple read patterns (balance, statement, reconciliation, audit), team with relational background, and regulatory auditability requirement. Volume does not justify DynamoDB horizontal scaling within a 3-year horizon.
Adopt **Aurora PostgreSQL** as the persistence engine for the double-entry ledger, with the following complementary design decisions: (1) event sourcing modeling — immutable `ledger_events` table as source of truth, `account_balances` table as materialized projection updated by trigger; (2) declarative date-range partitioning on `ledger_events` to isolate historical reconciliation from current operations; (3) RDS Proxy for connection management in Lambda environment; (4) Aurora Global Database for multi-region DR with RPO < 1s; (5) pgaudit enabled for statement-level audit trail. DynamoDB is reserved for the downstream domain event store (EventBridge Pipes → DynamoDB) where eventual consistency is acceptable.
- ✅ Double-entry atomicity guaranteed by native SQL transactions — zero compensation code in the application
- ✅ All read patterns covered by SQL without data duplication or additional GSIs
- ✅ Complete audit trail via pgaudit + WAL + Aurora Backtrack
- ⚠️ Horizontal scaling requires explicit sharding if volume exceeds single-instance capacity — review point at 18 months
- ⚠️ RDS Proxy is mandatory for serverless workloads — adds ~1-2ms latency and additional cost
- ⚠️ Balance projection trigger creates coupling between write and read within the same transaction — monitor lock contention on high-volume accounts
Resulting Architecture: Double-Entry Ledger with Aurora
Flow of a financial transaction from the API through ledger persistence, balance projection, and downstream event propagation. DynamoDB appears only as the domain event store — outside the consistency-critical path.
- Client · (mobile/web)
- API Gateway · REST
- Lambda · Transaction Handler
- RDS Proxy · Connection Pool
- Aurora Writer · PostgreSQL 15
- ledger_events · (immutable, partitioned)
- account_balances · (materialized projection)
- Aurora Reader · (reconciliation / reports)
- pgaudit · Statement Log
- CloudWatch Logs · Audit Trail
- Lambda · Stream Processor
- EventBridge · Domain Events
- DynamoDB · Domain Event Store
- Aurora Global DB · Secondary Cluster
AWS Well-Architected Framework Assessment
Security
RDS Proxy with IAM authentication eliminates database credentials in environment variables. pgaudit + CloudWatch Logs creates an immutable audit trail. Row-level security in PostgreSQL isolates data per tenant. Aurora Encryption at rest (AES-256) and in-transit (TLS 1.2+) by default.
Reliability
Aurora Multi-AZ with automatic failover in ~30s. Aurora Global Database for multi-region DR with RPO < 1s and RTO < 1 minute (estimate). Aurora Backtrack for logical error recovery without full restore. Date partitioning isolates performance issues in old partitions.
Sustainability
Aurora stores data in shared distributed storage — no data replication between read replicas, reducing storage footprint. Partitioning allows archiving old partitions to S3 Glacier, reducing active data in the cluster.
The decision between Aurora and DynamoDB for a ledger is not a database decision — it is a decision about where you want complexity to live. DynamoDB does not eliminate the complexity of a double-entry ledger; it moves it from the database into key design, into GSIs, into application transaction code, and into your team's operational runbooks. If your team has fluency in that model, that trade-off may be worth it. For most core banking teams I know, it is not.
What I would do differently from what I frequently see: I would not use Aurora as a traditional relational database with mutable balance tables. I would implement event sourcing from the start — ledger_events as append-only, balances as projections. This solves the immutability problem more cleanly than triggers, and opens the door to recalculating balances from events if a projection becomes inconsistent. The cost of implementing this correctly from the start is lower than migrating from a mutable model later.
On DynamoDB: it has a place in this architecture, but not on the consistency-critical path. As a downstream domain event store — where you publish TransactionCompleted, BalanceUpdated — it is excellent. Low latency, horizontal scale, no connection management. The mistake is trying to make it the primary ledger because the rest of the platform already uses DynamoDB. Eventual consistency on bank balances is not an acceptable trade-off, regardless of how elegant the key design is.
A point that is frequently overlooked: the cost of DynamoDB with multiple GSIs at medium volumes can easily exceed Aurora. Eac
Verdict
Aurora PostgreSQL is the correct choice for this double-entry ledger. Not because DynamoDB is inadequate as a database — it is excellent at what it does — but because the invariants of a financial ledger (atomicity, isolation, immutability, auditability with transactional semantics) map directly to the primitives of the relational model. The complexity DynamoDB would require to cover all access patterns of this domain is not justified by the throughput it offers at the projected volume. The decision has a clear review point: if entry volume exceeds single-instance Aurora capacity in 18-24 months, the architecture needs to evolve to horizontal sharding — either with Citus (distributed PostgreSQL), or with a domain separation that isolates high-volume accounts. That is the real cost of the choice: you trade automatic horizontal scaling for native transactional correctness, and you need a plan for when volume eventually justifies the additional complexity. The event sourcing pattern with materialized projection, combined with Aurora's continuous backup and pgaudit, delivers an auditability foundation that satisfies regulatory requirements without additional infrastructure. DynamoDB