API Gateway Design for Embedded Finance
Letting Non-Banks Plug Into Your Core Without the Risk
Letting Non-Banks Plug Into Your Core Without the Risk
Embedded finance inverts the usual integration model. Instead of your bank consuming a third party's API, you're exposing your own core banking capabilities — account creation, payments, KYC checks, balance inquiries — to a non-bank partner's application, at a scale and trust level well beyond a typical internal service consumer. A fintech, a marketplace, or a payroll platform is now calling into your core, and the gateway sitting in front of that traffic is the single most consequential piece of infrastructure in the whole arrangement. Get it wrong, and a partner's traffic spike, bug, or compromised credential becomes your incident.
This piece covers how we design API gateways for embedded finance programs, from a Spring-based backend perspective: rate limiting strategy, partner isolation, and where liability boundaries actually need to be enforced in code, not just in the partnership agreement.
The Gateway Is a Trust Boundary, Not Just a Router
It's tempting to treat the API gateway as infrastructure plumbing — routing, some auth, maybe response caching. For embedded finance, the gateway is the enforcement point for every trust decision your legal and risk teams made in the partnership agreement. If the contract says a partner can create up to 500 accounts per day, that ceiling needs to be enforced in the gateway layer, not assumed because the partner said they'd respect it.
With Spring Cloud Gateway, this means building custom GatewayFilter implementations that go beyond the built-in routing predicates — filters that check partner-specific entitlements against the request before it ever reaches a core banking service.
@Component
public class PartnerEntitlementFilter implements GatewayFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String partnerId = exchange.getRequest().getHeaders().getFirst("X-Partner-Id");
String requestedScope = resolveScope(exchange.getRequest());
return entitlementService.checkEntitlement(partnerId, requestedScope)
.flatMap(allowed -> {
if (!allowed) {
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
});
}
}
The entitlement check itself should be backed by a data store that's updated when the commercial relationship changes — not a config file that drifts out of sync with what legal actually signed.
Rate Limiting: Per-Partner, Per-Capability, and Layered
Flat, global rate limiting is close to useless in an embedded finance context, because it treats a payroll platform running batch disbursements at 2am identically to a consumer-facing budgeting app making balance checks throughout the day. The rate limiting strategy needs three layers:
Per-partner limits, reflecting the commercial agreement and the partner's demonstrated traffic pattern. A newly onboarded partner should start conservative regardless of what their projected volume claims, and graduate to higher limits as their actual usage validates the projection.
Per-capability limits within a partner's allocation. A partner might be entitled to high-volume balance inquiries but a much tighter ceiling on account creation or payment initiation — the operations with real financial or compliance consequence if abused. We implement this with Spring Cloud Gateway's RequestRateLimiter filter backed by Redis, keyed on a composite of partner ID and capability:
@Bean
public KeyResolver partnerCapabilityKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getHeaders().getFirst("X-Partner-Id")
+ ":" + resolveCapability(exchange.getRequest())
);
}
A global circuit breaker, independent of any single partner's limit, protecting the core banking system itself from aggregate load across all partners combined. This is the layer that saves you when several partners simultaneously spike, none of them individually over their limit, but the sum overwhelming a downstream service. We wire this with Resilience4j alongside the gateway, tripping on core-service latency and error rate rather than partner-specific request counts.
Rate limit responses matter too — a 429 with a Retry-After header and a clear error body isn't just good API citizenship, it's what stops a partner's own retry logic from making the problem worse.
Partner Isolation: No Shared Blast Radius
The failure mode we design against hardest is one partner's misbehavior — a bug, a compromised API key, a runaway retry loop — degrading service for every other partner or for the bank's own channels. This requires isolation at several levels:
Connection pool isolation per partner or partner tier, so a partner exhausting connections to a downstream service doesn't starve the pool for everyone else. Bulkhead patterns (again, Resilience4j fits naturally alongside Spring Cloud Gateway here) scope this per route.
Separate credential scopes per partner, never a shared API key across multiple partner integrations, even when the partners are commercially related. Credential compromise needs to have a blast radius of exactly one partner.
Data-level isolation enforced at the service layer, not just the gateway. The gateway authenticates and authorizes the partner and the capability; it should not be the only thing standing between a partner and another partner's — or another customer's — data. Downstream core banking services need to independently validate that the account or customer being accessed is actually within the requesting partner's authorized scope, because a gateway misconfiguration should degrade to "request rejected," never to "wrong customer's data returned."
Where Liability Boundaries Actually Sit
The partnership agreement will define who's liable for what — but liability in an embedded finance arrangement is only meaningful if the technical architecture can produce evidence of where a failure actually originated. This is where the gateway's logging and observability posture becomes a liability-allocation tool, not just an ops concern.
Every request through the gateway should be logged with enough context to reconstruct, after the fact, exactly what the partner sent, what the gateway decided, and what the core system did with it: partner ID, requested capability, rate limit state at time of request, entitlement check result, and downstream response — correlated with a request ID that flows through the entire call chain.
log.info("gateway_request partner={} capability={} entitlement_result={} " +
"rate_limit_remaining={} request_id={}",
partnerId, capability, entitlementResult, rateLimitRemaining, requestId);
When a dispute arises over a failed transaction — did the partner send malformed data, or did the core system mishandle a valid request — this log trail is frequently the actual evidence base, regardless of what the contract says in the abstract. We treat this logging as a compliance-grade requirement, with retention aligned to the bank's existing regulatory record-keeping obligations, not as debug output that happens to be useful later.
Idempotency enforcement sits in this same category. Payment initiation and account creation endpoints need idempotency key support at the gateway or immediately behind it, because a partner's retry after a timeout — which will happen — needs to produce exactly one transaction, with the gateway able to demonstrate which request was accepted and which were deduplicated. Without this, "did the partner double-submit or did our system double-process" becomes an unanswerable question exactly when it matters most.
The Takeaway
An API gateway for embedded finance is doing more than routing traffic to your core banking services — it's the enforcement point for commercial entitlements, the isolation boundary between partners, and the evidence trail for liability disputes that will eventually happen. Designing it well means building partner- and capability-aware rate limiting, real isolation at the connection and data layers (not just at auth), and logging detailed enough to answer "whose fault was this" months after the fact. Get this right at the architecture stage, and opening your core to non-bank partners stops being a risk you're managing and becomes infrastructure you can scale confidently.