Two application servers in two regions, one client writing through a VPN, and a question nobody wrote down: which component decides that the write happened? Routing gets the request to a process. A transaction boundary decides when the change becomes durable and visible. Those are different jobs performed by different systems, and the gap between them is where most multi-region write bugs live.

Start from the business rule, not the map

Start with a sentence about the business rule rather than about geography: which reads and writes have to succeed together, and which concurrent changes must be impossible while they do. For an inventory reservation that means the stock check and the reservation either both take effect or neither does, and no second order may consume the same unit in between.

RequirementWhat satisfies itWhat does not
All-or-nothing applicationA transaction inside one owning systemA VPN path between regions
No double spendIsolation strong enough to exclude the conflictAtomicity alone
Survive a region lossReplication the database itself documentsTwo independent databases behind one client

That middle row is the one teams skip. Atomicity without adequate isolation lets two transactions each commit correctly and still produce a state the business considers impossible, and no amount of retry logic repairs it afterwards.

One authoritative boundary beats two regional servers

Regional application servers can stay where they are, as long as they submit the operation to a service that performs it inside a single supported transaction boundary. The application tier becomes a router of intent; the data tier owns the truth.

Keep that boundary short and close to the data it touches. A transaction held open while a human answers a prompt, or while a call crosses two regions, spends its time holding locks and increasing the chance that something else times out behind it. That failure looks like a database problem and is really a design problem wearing one. Moving the decision into the data tier usually removes it entirely.

BEGIN;
UPDATE inventory SET reserved = reserved + 1
 WHERE sku = 'A-100' AND available >= 1;
INSERT INTO reservation (sku, order_id) VALUES ('A-100', 'ord-9f');
COMMIT;

Read that sketch as a boundary, not as a performance recommendation. The point is that the check and the insert share one commit, so a concurrent order either sees the stock consumed or never sees it at all.

What a replicated database does and does not give you

A replicated database can supply a wider boundary when its own transaction model supports it. Spanner documents atomic read-write transactions within a single database across tables, with defined isolation and retry behaviour. That is a guarantee you read, test and rely on deliberately.

It is also a guarantee that comes from the product, not from the route. A VPN between regions changes how packets travel and how failures present themselves. It does not extend a transaction boundary by a single row, no matter how healthy the tunnel looks. Confusing the two produces architectures where every component passes its own check and the data still disagrees.

Two databases do not become one system

Two independently managed databases do not acquire a shared transaction manager because clients can reach both of them. If the business genuinely needs atomic commitment across the pair, the coordination mechanism, the participants and the recovery process all have to be chosen and qualified rather than assumed.

Think about the moment a participant or a link disappears after work has been prepared. A supported distributed commit can leave in-doubt state that needs a recovery procedure and a human decision, and it comes with no promise that every region stays writable through every partition.

If asynchronous coordination is acceptable instead, say so explicitly and write down the intermediate business states plus the reconciliation rules. A chain of locally committed steps with compensation is a legitimate design; it is simply not one atomic transaction wearing a different name.

Design the retry before the first outage

A client can lose the response after the operation has committed, so the caller cannot treat a timeout as a failure. Preserve a durable request identity and use the service outcome or idempotency mechanism before retrying a mutation, otherwise the retry becomes a second write.

Automatic transaction callbacks can also execute more than once after a retryable abort, which makes them a poor home for irreversible external effects. Keep those outside the callback unless the workflow defines an explicit safe protocol for them. The same caution applies to anything that costs money or sends a message. A duplicate charge is a much harder conversation than a duplicate log line.

Acceptance should replay four scenarios: two clients racing for the same row, an abort the service says can be retried, a commit whose answer never arrives, and the loss of a region or the link between them. Record the committed business result and the unresolved states next to the routing design, so nobody mistakes the network for the component that provides the guarantee.

Amanqephe ahlobene