Multi-region replication for ledgers
Eventual consistency is fine for blog posts. It's not fine for double-spend prevention.
By Solomon Ajayi · Free to read, no signup
Your fintech scales beyond one region. A user in Lagos wants ledger reads from a Lagos-region replica (sub-100ms latency); a user in Nairobi wants the same from Nairobi. Easy enough for read replicas: stream WAL from primary to followers. But WRITES are tricky, you cannot allow two regions to independently post journal entries against the same user (double-spend on rebalance). The pattern: a SINGLE WRITE REGION (the primary), READ REPLICAS in other regions for low-latency reads. Writes must go to the primary even from Nairobi, taking the cross-region latency hit. Or: SHARD by user-id so each user lives in ONE write region and reads from the local replica. This lesson posts a write that flows through the primary-write topology.
Going multi-region is easy for reads and treacherous for writes, and a ledger is mostly writes that must be exactly right. Reads scale by streaming the write-ahead log to followers in every region, so a user in Nairobi sees their balance from a local replica in single-digit milliseconds. Writes cannot fan out the same way, because if two regions could each post entries against the same wallet, a rebalance race could let a user spend the same money twice. Eventual consistency is fine for a blog post; it is a double-spend bug for a balance.
So the rule is one source of truth per logical unit. In this lesson the topology is a single primary write region with read replicas elsewhere: a deposit initiated in Nairobi makes a cross-region call to the primary, eats roughly 200 milliseconds on that one write, and returns durable. Then the entry replicates outward, and the next thousand reads from Nairobi each cost a few milliseconds against the local copy. You pay latency once on the write to buy correctness, and you recover it on every read.
Notice the replica catch-up step posts no new entry: replication happens at the log level, beneath your API, so the row that landed in the primary simply appears in the replica a few hundred milliseconds later. That gap is the replication lag, and it is why a read right after a write can momentarily miss it. If geography matters more than that single write penalty, the alternative is sharding writes by user so each user's authoritative region is their own, which trades operational complexity for local write latency.
Worked example, step by step
Write in primary region (us-east-1): ₦5,000 deposit
User in Nairobi initiates a deposit. The app server in Nairobi RPC-calls the ledger write API in us-east-1 (the primary write region). 200ms cross-region latency on the write, but the WRITE returns success. The journal entry is now durable in the primary.
| Account | Debit | Credit |
|---|---|---|
| Bank Account (1200) | ₦5,000.00 | |
| User Wallet (2000) | ₦5,000.00 |
Standard ₦5,000 deposit entry. The location of the write is invisible at the journal-entry level, the same Bank UP + Wallet UP shape. The architectural concern is WHICH region's DB serves this entry's storage.
Replica catches up (af-south-1, Nairobi)
WAL streaming: the journal entry replicates to the Nairobi read replica within a few hundred milliseconds. Subsequent READS from Nairobi (e.g., 'what's my wallet balance now?') hit the local replica with single-digit-ms latency. The write took 200ms; the next 1000 reads each take 5ms.
| Account | Debit | Credit |
|---|---|---|
| Bank Account (1200) | ₦0.01 | |
| User Wallet (2000) | ₦0.01 |
No new journal entry on the replica, replication is at the WAL level, not the API level. The journal_line row that landed in us-east-1 is now ALSO present in af-south-1, accessible to reads. The READ projection (wallet.balance) is similarly replicated.
Takeaway
Multi-region ledgers: writes must go through a single source of truth per logical unit (per user, per account, per shard) to prevent double-spend. Reads can fan out to local replicas for latency. Three viable topologies: (a) single global write region + read replicas everywhere (simplest, write latency penalty for non-primary regions), (b) sharded writes by user-id with each user routed to their own region's primary (better latency, more operational complexity), (c) consensus-replicated writes (CockroachDB, Spanner) where every node can write but consensus takes a round trip (best UX, highest cost). Pick based on geographic concentration of users, fintech with users in one country goes (a); pan-African fintech with users in 10 countries goes (b).
Practice this on a real ledger
Reading is half of it. Open this lesson in the lab to post the entries yourself against a real Postgres-backed double-entry ledger, with the validation on. Free, your sandbox is yours.