Platform Architecture · Data Sovereignty
A metadata-driven platform with hexagonal architecture that keeps a single codebase honest about data residency — worked through health records and cross-border payments.
Every platform that crosses a border eventually arrives at the same fork in the road, and most teams pick the wrong branch. You can fork the codebase — a separate deployment per country, each drifting on its own release train until you are running five products that happen to share a logo. Or you can run one global service that treats all data the same, which works right up until a regulator asks where a patient’s record physically lives.
For health data, that question is not academic. Under the GDPR, information about someone’s health is a special category — the most tightly governed class of personal data in Europe, with hard limits on moving it outside the EEA. Under India’s regime, whose implementing Rules were notified in November 2025 and are now rolling out in phases, a healthcare aggregator is a prime candidate to be designated a Significant Data Fiduciary, with annual impact assessments, independent audits, an India-resident data protection officer, and the prospect of category-specific localization for sensitive data.
So the architecture has to do the thing most architectures quietly avoid: make geography a first-class concern — without smearing it across every line of business logic. This is a design note on how to get there with two ideas that are stronger together than apart.
The thesis
The first idea is a metadata-driven core. Which tenant belongs to which region, what compliance profile applies, which encryption key and retention clock govern their data — all of it is data the platform reads, not logic the platform compiles. Onboarding a customer or opening a new region becomes a configuration change, not a code change.
The second is hexagonal architecture — ports and adapters. The domain core knows nothing about databases, regions, or HTTP. Everything sovereign — every fact about where bytes are allowed to rest — lives in adapters at the edge of the hexagon.
Combine them, and you get the property that makes the whole system defensible: a request’s data residency is determined by the infrastructure it flows through, resolved from metadata at the boundary, never decided by an if statement buried in a service method.
The core
DataSource — is the only place residency is decided.The payoff is reasoning isolation. A PatientRecordRepository port exposes save and findById; it has no opinion about whether there are one or ten clusters behind it. Your domain tests run against an in-memory adapter and never think about Frankfurt. And the day you add a sixth region, the hexagon doesn’t change — you wire one more target into a single adapter.
The edge
A single global API gateway is the only front door. It validates the access token, reads the tenant identity from signed claims, and looks up that tenant’s region in control-plane metadata. The region is then attached to the request as it travels downstream.
The security crux lives in one sentence: the routing key is derived on the server, from metadata — never from anything the caller can set. A client cannot ask to be routed to a different region by flipping a header, because the gateway resolves region from the tenant registry and strips any client-supplied region hint. And when resolution fails, the request is rejected. There is no default cluster, because a default cluster is a data-residency incident waiting for a quiet Tuesday.
// The gateway is the single ingress. Region comes from trusted metadata,
// not from the request — and a miss fails closed.
public Mono<Void> filter(ServerWebExchange ex, WebFilterChain chain) {
String tenantId = TenantClaims.fromValidatedJwt(ex.getRequest());
RegionKey region = tenantRegistry.regionFor(tenantId) // control-plane lookup (cached)
.orElseThrow(UnresolvedTenantException::new); // no default region, ever
ServerHttpRequest routed = ex.getRequest().mutate()
.headers(h -> h.remove("X-Region")) // strip any client-supplied hint
.header("X-Resolved-Region", region.code()) // server-asserted, signed downstream
.build();
return chain.filter(ex.mutate().request(routed).build());
}
The mechanism
clinic-IN-204 resolves to India; the data source binds the Mumbai cluster. The path to EU data isn’t merely forbidden by policy — it is unreachable by construction.Downstream, the resolved region lives in a request-scoped context for the duration of the call. Spring’s AbstractRoutingDataSource reads that context to pick the connection pool — one pool per region, each pinned to its own cluster — with lenient fallback switched off, so a missing key fails loudly instead of silently borrowing a neighbour’s database.
// Request-scoped, server-trusted. Set from X-Resolved-Region; cleared after.
public final class TenantContext {
private static final ThreadLocal<RegionKey> CURRENT = new ThreadLocal<>();
public static RegionKey require() {
RegionKey r = CURRENT.get();
if (r == null) throw new UnresolvedTenantException(); // fail closed
return r;
}
}
// One application, many regional clusters. No code branch chooses the region.
public class RegionRoutingDataSource extends AbstractRoutingDataSource {
protected Object determineCurrentLookupKey() {
return TenantContext.require(); // resolved upstream, never from the body
}
}
@Bean
DataSource dataSource(EuDataSource eu, InDataSource in, UsDataSource us) {
var routing = new RegionRoutingDataSource();
routing.setTargetDataSources(Map.of(
RegionKey.EU, eu, // Frankfurt — HikariCP pool
RegionKey.IN, in, // Mumbai
RegionKey.US, us)); // Virginia
routing.setLenientFallback(false); // never cross a sovereign boundary silently
return routing;
}
ThreadLocal is exactly where region can be dropped silently — propagate it explicitly across async boundaries and assert it on the way in. The downstream service must also treat X-Resolved-Region as trusted only because the gateway is the sole ingress and the header is stripped at every other edge.
The metadata
This split is what buys you the “single codebase” promise without the asterisk. The binary is the same in Frankfurt and Mumbai; what differs is the metadata each instance reads and the cluster it is wired to. Records of Processing Activity, retention clocks, and consent rules stop being tribal knowledge in a wiki and become derived views over the registry — which is also what makes an annual audit survivable.
The compliance map
The reason to push residency into infrastructure is that infrastructure can offer guarantees that policy documents cannot. Here is how two of the regimes a global health platform must answer to map onto the architecture.
European Union
India
Note the deliberate restraint in the India column: DPDP’s regime is not blanket localization — cross-border transfer is permitted unless the government restricts a specified category. Designing for in-region residency anyway is a posture, not a literal mandate. It is cheaper to keep sensitive health data home from day one than to re-architect the day a category order lands.
A second domain
Health data was a useful first case because residency there is mostly about where a record sleeps. Payments raise the stakes on every axis — the data is more sensitive, the consistency requirements are unforgiving, and in at least one major market the localization mandate is absolute rather than conditional. The same metadata-driven hexagon handles it. What changes is the metadata it carries and the adapters at its edge.
A global payments platform answers to a security standard, a clutch of regional conduct rules, and at least one hard localization law — often for the same transaction.
European Union
India
Global · Security
Look closely at the India column against the health example: in the same country, payment data is localized absolutely while health data is only conditionally so. Residency isn’t a property of a region — it’s a property of a region and a data category together. That is precisely the kind of fact a metadata-driven core exists to hold, and precisely what a hard-coded approach gets wrong the first time the rules diverge.
Data security
A card number enters a small, isolated, in-region cardholder data environment and a token comes back. The domain core, every downstream service, your analytics, and your logs only ever hold that token. Because the vault sits behind a port like any other adapter, your PCI audit boundary is a small box you can reason about — and the region-local HSM means key material satisfies residency and PCI key-management in a single stroke.
// The core deals in tokens — never a PAN. PCI scope stops at this port.
public interface CardVaultPort {
CardToken tokenize(Pan pan); // in-region, PCI-scoped adapter + HSM
MaskedCard reveal(CardToken token); // last-4 only, for receipts
}
// The write lands in the resident cluster, exactly once.
@Transactional // ACID within a region — never across clusters
public Receipt capture(PaymentCommand cmd) {
RegionKey region = TenantContext.require(); // resident cluster, fail-closed
return ledger.appendIfAbsent(cmd.idempotencyKey(), // idempotency scoped to this region
cmd.withToken()); // no PAN leaves the vault
}
The hard case
Here is the puzzle a payments platform must answer that a health platform can sidestep: a payment, by its nature, may cross a border. Payer in Frankfurt, payee in Mumbai. If the data must stay resident, how does the money move?
You stop treating a cross-border payment as one global transaction and model it as two coordinated, region-resident legs joined by a message. Each region’s cluster is the book of record for the leg that originates there. What crosses the boundary is the minimum-necessary scheme message — tokenized references, amount, routing data — over the card network or correspondent rail, not a copy of either cluster’s datastore. RBI’s allowance for storing the foreign leg abroad while keeping the domestic data in India is no longer a special case to engineer around; it is the default the architecture already produces.
And crucially, you never run a distributed transaction across sovereign clusters — that would itself be the cross-border transfer you are avoiding. Within a region, the ledger is strict, double-entry, ACID. Across regions, settlement is reconciled, not two-phase-committed.
The honest part
No architecture is free, and an approach that looks this clean on a diagram earns the right to be trusted only by naming what it gives up. Five things to walk into with eyes open:
SELECT across sovereign clusters, because that is the cross-border transfer you outlawed. Cross-region analytics moves to a separate warehouse fed by de-identified, aggregated rollups — never raw personal data.The principle
When this works, the domain code reads as though there were a single database in the world — clean, geography-free, easy to reason about. Meanwhile the infrastructure quietly guarantees there are several, each sitting where the law says it must. The engineers maintaining business logic never think about Frankfurt or Mumbai; the regulator auditing residency never has to take a developer’s word for it.
That is the whole trade of pushing geography to the edges: one codebase to reason about, many sovereigns respected by construction. You stop choosing between operational scale and regional integrity, because you have arranged the system so that honouring the border is the path of least resistance — and crossing it takes a deliberate act that the architecture simply does not offer.