Platform Architecture · Data Sovereignty

One core,
many sovereigns.

A metadata-driven platform with hexagonal architecture that keeps a single codebase honest about data residency — worked through health records and cross-border payments.

CORE one codebase EU · Frankfurt GDPR cluster IN · Mumbai DPDP cluster US · Virginia CCPA cluster

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

Make residency a property of infrastructure, not a branch in your code

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.

Residency you can’t accidentally code around is residency a regulator can trust.

The core

A domain that doesn’t know geography exists

Global Gateway + REST adapter INBOUND ADAPTER PORT Domain Core PatientCare · Consent · Records SOVEREIGNTY-AGNOSTIC PORT Region Routing DataSource adapter OUTBOUND ADAPTER EU Frankfurt IN Mumbai US Virginia
Ports & adapters. The core models care, consent and records. It never names a region. The one adapter that does — the routing 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

Resolve the tenant once, at the gateway — from metadata you trust

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.

  TenantRoutingFilter.java — global gateway
// 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

One request, routed to exactly one sovereign cluster

REQUEST JWT · tenant=clinic-IN-204 Gateway resolve → IN TENANT CONTEXT IN · Mumbai DPDP cluster EU · Frankfurt SEALED BOUNDARY
Fail-closed routing. Tenant 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.

  RegionRoutingDataSource.java — data plane
// 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;
}
Implementation reality The gateway is reactive (Spring Cloud Gateway); the data plane is a blocking JDBC service. The seam where a reactive context hands off to a thread-bound 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

Control plane decides; data plane stays identical everywhere

CONTROL PLANE · system of record for sovereignty Tenant Registry region · profile · keyRef · retention Policy & RoPA derived from metadata Edge cache Redis · sub-ms lookup Add a region or a tenant = a config change. reads metadata DATA PLANE · one stateless service, replicated svc @ EU identical build → Frankfurt DB svc @ IN identical build → Mumbai DB svc @ US identical build → Virginia DB
Sovereignty as configuration. The same artifact runs everywhere; it inherits its obligations from the metadata it is handed. The schema for every cluster evolves through the same migration set, so the regions stay shape-identical while their contents never mix.

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

Each obligation, answered by a structural control

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

GDPR

Special category data (Art. 9)Health data carries the strictest processing bar. → EU tenants pinned to the Frankfurt cluster by metadata. Transfer limits (Chapter V)Moving data outside the EEA is constrained. → The routing seal makes a cross-border read unreachable, not just disallowed. Right to erasure (Art. 17)Delete on request, provably. → Erasure routes to the one cluster holding the record; no fan-out.

India

DPDP Act & Rules 2025

Significant Data Fiduciary dutiesAnnual DPIA, independent audit, India-based DPO. → Region-scoped audit logs and RoPA derived from the registry. Conditional localization (Rule 15)Government may restrict specified categories. → Indian health data kept in Mumbai by default — ready before any order, not scrambling after. Consent & data-principal rightsGranular consent, withdrawal, access. → Consent state lives beside the data it governs, in-region.

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

The same pattern, under harder rules: cross-border payments

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.

Three regimes, three different shapes

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

PSD2 → PSD3 / PSR

Strong Customer AuthenticationMulti-factor on payment initiation, over GDPR for the personal data. Directly-applicable PSR (~2027)Expanded fraud liability and open banking, harmonised across member states. → Conduct rules become metadata, not per-country forks.

India

RBI · Storage of Payment System Data

Hard localization (2018)The entire end-to-end payment data must be stored only in India. → Mumbai cluster is the sole book of record; no fallback. 24-hour foreign-purge ruleForeign processing only for the foreign leg; data deleted abroad and returned within a day. → Cross-border modelled as a message, not a copy.

Global · Security

PCI DSS v4

Not residency — securityProtect cardholder data wherever it lives. Scope is the enemyEvery system that touches a PAN is in audit scope. → Tokenize at the edge; collapse scope to one adapter.

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

Tokenization collapses PCI scope into a single adapter

PCI DSS SCOPE · in-region only PAN entry card / terminal Token Vault (CDE) region-local HSM / KMS keys never leave region TOKEN DESCOPED · tokens only Core tokens Ledger svc Analytics Logs / events
Hexagonal architecture, second dividend. The vault is just an outbound adapter behind a port. Cardholder data never crosses into the core, so PCI scope shrinks to the vault — not the whole estate — and the keys stay in-region.

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.

  Payment domain — tokens in, exactly-once out
// 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

The cross-border paradox: money moves, data stays

EU REGION · resident book of record Payer Frankfurt EU ledger PII + full transaction detail STAYS IN EU (GDPR) INDIA REGION · resident (RBI) Payee Mumbai IN ledger full payment data STAYS IN INDIA (RBI) scheme / network rail crosses: tokenized reference · amount · routing never: a copy of either datastore
Two resident records, one message. The payment isn’t a single global write — it’s two region-resident legs joined by a minimal scheme message. RBI’s own carve-out for the foreign leg of a cross-border transaction maps onto this exactly.

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.

Why fail-closed routing is load-bearing here A misrouted health read is a privacy problem. A misrouted payment write is a privacy problem, a corrupted ledger, and possibly a double charge — all at once. The same server-resolved, fail-closed routing from the health case stops being a nicety and becomes the thing that keeps money honest. Because idempotency keys are scoped to the resident cluster, a retry can never land in another region and conjure a phantom second payment.

The honest part

What this design costs you

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:

The principle

Sovereignty done well is invisible

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.

#SoftwareArchitecture #HexagonalArchitecture #DataSovereignty #SpringBoot #GDPR #DPDP #Payments #PCIDSS #Fintech #MultiTenant #PlatformEngineering