The ledger: where the invariant lives
The component with the least room for compromise. Everything the CBA module built rests on a database that can do a conditional multi-row atomic write and survive a node loss without forgetting an acknowledged commit. This part works out which managed services actually offer that, what each one costs, and why the most impressive option on either platform is the wrong one here.
The requirement, restated for a cloud database
Before any service names, restate what Part 1 of the CBA module actually demanded. Most cloud database debates are unresolvable because nobody wrote this down.
- Multi-row atomic writes. Journal row plus two or more entries, all or nothing.
- A conditional write. "Insert this debit only if the balance permits", evaluated under a lock.
- Durability with RPO zero. An acknowledged commit survives node loss.
- Strong reads on the balance path, so read-your-own-writes holds.
- Ad-hoc aggregation, for the invariant check and reconciliation.
- Global distribution. Part 7 made regions a legal boundary, so cross-region consistency is not required and is actively unwanted.
- Unbounded elastic scale. Part 3 sharded it; each shard holds ~34 GB and a few hundred writes a second.
- Schemaless flexibility. A ledger wants a rigid schema that refuses malformed money.
the second list is why Spanner is not automatically the answer and why a sharded managed Postgres is usually enough. state the requirement first, and half the catalogue eliminates itself.
RDS, Aurora, and what Aurora actually changed
Aurora is described as "managed Postgres" and it is architecturally a different thing. The difference matters for exactly the properties a ledger cares about.
standard Postgres write → buffer pool → WAL → fsync → data pages later replica ships and replays the full WAL Aurora write → ships only the redo log to a distributed storage layer storage is 6 copies across 3 AZs, quorum 4/6 for write replicas read the same storage, so they do not replay the database and the storage engine were separated.
| Property | Standard RDS | Aurora |
|---|---|---|
| Replica lag | Replay-bound, can grow | Typically under 100 ms, shared storage |
| Failover | 60 to 120 s typical | Under 30 s, often under 15 |
| Write amplification | Full 8 KB pages to replicas | Redo only, far less network |
| Storage growth | Provisioned in advance | Automatic, to 128 TB |
| Cost | Lower | Roughly 20 to 30% more for equivalent compute |
| Postgres compatibility | Exact | Very high, with some extension gaps |
| Backtrack / PITR | PITR | PITR plus fast in-place rewind on MySQL flavour |
Cloud SQL, AlloyDB, and Spanner
GCP offers three relational answers and they are genuinely different products rather than tiers.
| Cloud SQL | AlloyDB | Spanner | |
|---|---|---|---|
| Engine | PostgreSQL, actual | Postgres-compatible, rearchitected | Not Postgres. Own engine, PG interface |
| Analogue | RDS | Aurora | No AWS equivalent |
| Scale-out writes | No, one primary | No, one primary | Yes, horizontally |
| External consistency | Within instance | Within instance | Global, via TrueTime |
| Cost | Lowest | Moderate | Highest by a distance |
| Fit for our ledger | Sufficient | Better failover | Buys something we do not need |
Spanner: TrueTime, and the one thing it buys you
Worth understanding properly, because it is the clearest example of a cloud-only capability and because explaining it well is a strong interview signal.
the distributed transaction problem: two nodes cannot agree on "now" to better than clock skew so a transaction ordering needs coordination messages TrueTime gives an interval, not a timestamp: TT.now() → [earliest, latest], with a bounded uncertainty ε backed by GPS and atomic clocks in every datacentre commit protocol: pick a timestamp, then wait out ε before acknowledging, so no later transaction can claim an earlier time ε is a few milliseconds, so the commit wait is a few milliseconds. that is the price, and it buys externally consistent global ordering.
- A global ledger with no legal partitioning, where any account may transact with any other atomically.
- Write throughput beyond one node without application sharding.
- A team that cannot operate sharding, where the premium buys away real operational complexity.
DynamoDB as a ledger: the honest version
Part 1 named DynamoDB as a viable alternative and moved on. Here is the full version, because at an AWS-first company this is the real conversation.
| Requirement | DynamoDB answer | Cost |
|---|---|---|
| Multi-row atomicity | TransactWriteItems | 100-item ceiling, and 2x the write cost |
| Conditional write | ConditionExpression | Excellent. Genuinely first-class |
| Durability | 3 AZs synchronously | Strong, no tuning needed |
| Strong reads | ConsistentRead=true | 2x the read cost |
| Ad-hoc aggregation | None | The invariant check needs another path |
| Sharding | Automatic | You never think about it again |
| Hot partition | 3,000 RCU / 1,000 WCU per partition | The Part 3 hot row, again |
- The invariant check moves to a Streams-driven verifier, since there is no
GROUP BY. A Lambda consumes the stream and maintains per-journal sums, alerting on any non-zero. - The balance read is a query on a sort key rather than a
SUM, so snapshots become mandatory rather than an optimisation. - Reconciliation exports to S3 and runs in Athena, because it cannot run in the database.
- Access patterns freeze early. Single-table design means a new query pattern may need a new GSI or a backfill.
Failover behaviour, and measuring real RTO
the documented number is promotion time. the number that matters is customer-visible outage: detection 5 to 30 s ← health check interval promotion 5 to 60 s ← the documented figure endpoint propagation 5 to 40 s ← DNS TTL, often forgotten pool recovery 5 to 30 s ← stale connections backlog drain 10 to 120 s ← queued work, thundering herd total: 30 s to 4 minutes. the documented number was 30 s.
- Short DNS TTL, or an endpoint that does not depend on DNS. RDS Proxy and Cloud SQL Auth Proxy both help here.
- Connection pools that validate before handing out a connection, so a stale one fails fast rather than hanging.
- Fast-failing timeouts, per Part 2, so the backlog does not become the outage.
- Drill it monthly and publish the measured number, per Part 13. The RTO is what the drill says, not what the docs say.
Backup, PITR, and restoring 34 GB in anger
Part 15 warned that growth breaks recovery before it breaks the budget. On managed services the backup is automatic and the restore is still your problem.
restoring one 34 GB shard, measured rather than assumed: snapshot restore initiation ~2 min volume available ~8 min warm-up as pages fault from S3 ~25 min WAL replay to the target time variable verify the invariant before serving ~4 min ~40 minutes for one shard. with 4,096 logical shards over N nodes, a full-region restore is a multi-hour operation.
Connection management: pgbouncer, RDS Proxy, and Lambda
A Postgres connection costs memory and a backend process. A few hundred is comfortable; a few thousand is not. Serverless compute makes this acute.
Postgres: each connection ≈ 5 to 10 MB and one process 500 connections ≈ 3 GB and 500 processes fine 5,000 connections the database falls over Lambda makes this worse structurally: each concurrent execution is its own environment 1,000 concurrent Lambdas → 1,000 connections and they are short-lived, so churn is constant
| Solution | Where it runs | Note |
|---|---|---|
| pgbouncer | You run it | Transaction pooling is the useful mode. Breaks session features like prepared statements unless configured |
| RDS Proxy | AWS managed | Pooling plus failover-aware connection handling. Designed for the Lambda case |
| Cloud SQL Auth Proxy | Sidecar | Primarily auth and encryption; pooling is still yours |
| Application pool | In-process | Correct for long-lived containers, useless for functions |
Sharding on managed databases
Part 3 sharded across 4,096 logical shards mapped to physical nodes. On a managed service, that mapping becomes a set of instances and the routing stays ours.
- The logical shard layer and the routing table, which are application code and fully portable.
- The cross-shard saga with in-transit suspense accounts.
- The invariant check per shard, which is a per-instance query.
- Adding a physical node is minutes rather than a procurement cycle, which makes rebalancing genuinely practical rather than theoretical.
- Each shard is a separate billable instance, so the cost model rewards fewer, larger nodes and the operational model rewards more, smaller ones. Resolve that explicitly.
- Per-shard backup and failover multiply the operational surface: 16 instances means 16 failovers to drill.
- Aurora Limitless and AlloyDB horizontal options exist and are worth evaluating, though both are newer than the workload deserves for a ledger.
a practical starting shape: 4,096 logical shards fixed forever, per Part 3 8 physical instances 512 logical shards each each: ~34 GB × 512 ÷ 4096 ≈ ~4 TB, r6g.4xlarge class each with a synchronous replica in another AZ growth is: add instances, reassign logical ranges, copy, cut over. a config change and a copy, never a rehash. that was the point.
The decision, with numbers
Roughly $14k to $18k per month at this shape, dominated by instance hours rather than I/O.
Roughly $13k to $17k per month, broadly comparable.
| Criterion | Winner | Margin |
|---|---|---|
| Postgres fidelity | Cloud SQL | It is actual Postgres. Aurora and AlloyDB both diverge slightly |
| Failover speed | Aurora | Consistently the fastest of the three |
| Operational tooling | Aurora | More mature, more third-party integration |
| Read scaling | AlloyDB | Columnar engine for analytical reads is genuinely novel |
| Cost | Cloud SQL | Modest difference, not decisive |
| If you need horizontal writes | Spanner | No AWS equivalent, and we do not need it |