Every sufficiently successful GraphQL API eventually becomes a bottleneck. What started as a clean, unified schema serving a handful of frontend queries grows into a sprawling monolith where deployment cadence is dictated by the slowest team, a single resolver bug can take down the entire graph, and nobody remembers who owns ProductRecommendation.score.
This series documents the construction of a production-grade e-commerce platform using GraphQL Federation 2, spread across three backend languages, two API gateways, and a full observability stack. It isn't a toy demo. It covers the real decisions — protocol selection, entity ownership, cross-service tracing — that determine whether federation works in practice or becomes distributed complexity for its own sake.
The Problem with Monolithic GraphQL
GraphQL was designed as a query language for clients. Its strength lies in letting consumers ask for exactly what they need. But the server side tells a different story.
In a typical monolithic GraphQL server, a single codebase resolves every type in the schema. For a small team building a focused product, this works brilliantly. The trouble starts when the organization scales:
A monolithic GraphQL server becomes a coordination bottleneck as teams grow. Every deployment requires synchronization across domain boundaries.
Three pain points emerge consistently:
Ownership ambiguity. When a Product type has fields like name, inventory, reviews, and averageRating, which team owns it? In a monolith, the answer is "everyone and no one." Schema changes require cross-team coordination that doesn't scale.
Deployment coupling. Team A wants to ship a bug fix to user authentication. Team B just introduced a regression in the product search resolver. In a monolith, both changes ship together or neither does. The deployment cadence converges to the slowest, most cautious team.
Language lock-in. Some domains have natural affinities with certain runtimes. A high-throughput inventory system benefits from Java's mature concurrency primitives. A payment processing service might leverage Go's lightweight goroutines. A user-facing auth layer is often fastest to iterate on in TypeScript. A monolith forces a single language choice for all domains.
Federation: Distributed Ownership, Unified Schema
GraphQL Federation solves this by letting each team own a subgraph — an independent GraphQL service that contributes types and fields to a shared supergraph. A federation-aware router composes these subgraphs into a single API that clients query as if it were one server.
The federated architecture. Kong handles cross-cutting API concerns. Apollo Router composes four subgraphs — written in three languages — into a unified supergraph. Each service owns its database.
The key insight of Federation 2 is the concept of entities — types that span service boundaries. An entity is identified by a @key directive and can be extended by any subgraph that knows its primary key.
Consider a Product. The Product Catalog service owns its core fields:
The Inventory service extends Product with stock data without touching the Product Catalog codebase:
The User/Reviews service adds review data:
From the client's perspective, Product is a single type with all of these fields. The router handles the orchestration transparently.
Why Three Languages?
A common reaction to polyglot architecture is skepticism. Why not pick one language and standardize? The answer depends on what you're optimizing for.
This platform uses three languages not for the sake of variety, but because each domain aligns with a runtime's strengths:
| Service | Language | Rationale |
|---|---|---|
| Product Catalog | Java 21 / Micronaut | Mature ecosystem for search integration (Meilisearch), image handling (MinIO), and the JVM's battle-tested concurrency model for high-throughput catalog queries |
| Inventory | Java 21 / Micronaut | Shares the JVM ecosystem with Product Catalog, enables gRPC communication without serialization overhead, and leverages database transactions for stock reservation consistency |
| Order Service | Go 1.23 / gqlgen | Lightweight goroutines for handling concurrent payment processing (Stripe API), fast cold starts, and a generated GraphQL layer that minimizes boilerplate |
| User/Auth + Reviews | TypeScript / Apollo Server | Fastest iteration cycle for auth flows, rich npm ecosystem for JWT handling, and Apollo Server's native federation support |
The platform proves that federation makes the language choice irrelevant to the client. The browser sends a single GraphQL query. Whether it's resolved by a JVM, a Go binary, or a Node.js process is an implementation detail hidden behind the router.
Entity Ownership and the Supergraph
Federation's power comes from clear entity ownership rules. Each entity has exactly one owning service that defines its canonical fields, plus zero or more extending services that contribute additional fields.
Entity ownership in the supergraph. Solid arrows show the owning service. Dashed arrows show extensions. The router uses @key fields to resolve entities across boundaries.
When a client queries across entity boundaries:
The router builds a query plan that orchestrates calls to multiple subgraphs:
The router's query planner decomposes this operation by inspecting the @key directives on each entity. It starts with a sequential fetch to the Order Service because that subgraph owns the Order type and its items field — crucially, this first hop produces the set of product IDs that downstream subgraphs need for entity resolution. Once the router has those IDs, it fans out three parallel _entities requests: one to Product Catalog for name, slug, and price; one to Inventory for stock availability; and one to the User Service for reviews and ratings. Each receiving subgraph implements a reference resolver that accepts an { id } representation and returns the fields it owns. Because steps 2–4 execute concurrently, the total latency is roughly equal to the slowest subgraph rather than the sum of all four.
The Gateway Stack
This platform uses a two-layer gateway architecture that separates API concerns from GraphQL concerns:
Kong Gateway sits at the edge and handles every concern that is protocol-agnostic. Its JWT plugin validates the bearer token on each request and extracts claims — user ID, role, tenant — into forwarded headers. The Rate Limiting plugin enforces per-consumer quotas (60 requests/min for the GraphQL endpoint, 20/min for auth routes). The CORS plugin locks down allowed origins, and the Correlation ID plugin generates an x-request-id that flows through every downstream service for distributed tracing. None of these plugins know or care that the payload is GraphQL; they operate on HTTP primitives.
Apollo Router takes over once the request clears Kong. It composes the four subgraph schemas into a single supergraph at startup, then uses its query planner to decompose each incoming operation into a minimal set of subgraph fetches. The headers that Kong injected — x-user-id, x-user-role, x-request-id — are propagated to every subgraph call, giving each service the auth context it needs without a second token exchange. The Router also exports OpenTelemetry spans for every query plan step, which feed into the Grafana LGTM+ stack covered in Part 5. This separation is deliberate: Kong is a general-purpose API gateway that knows nothing about GraphQL, and the Router is a GraphQL-specific gateway that knows nothing about rate limiting or JWT validation — each layer does one thing well.
What This Series Covers
The remaining articles in this series walk through each layer of the architecture:
- Part 2: Three Languages, One Schema — How each service implements its subgraph with federation directives, entity resolution, and database-per-service isolation
- Part 3: Hybrid Protocols — When GraphQL Meets gRPC and REST — Protocol selection by use case: gRPC for internal Java-to-Java communication, REST for Stripe payments, GraphQL for client-facing APIs
- Part 4: The Gateway Layer — Kong, Apollo Router, and Query Planning — How two gateways compose into a secure, observable API layer with federation query planning
- Part 5: Observability Across the Polyglot Stack — Distributed tracing with OpenTelemetry across three language runtimes, backed by the Grafana LGTM+ stack
Looking Forward
The next article steps inside each subgraph and examines how Java, Go, and TypeScript each implement Federation 2 directives — how @key, @external, and entity reference resolvers work in practice, and where the ergonomics differ significantly across languages and frameworks.
The gap between "federation sounds great in a conference talk" and "federation works in production" is bridged by implementation detail. That's where this series lives.
Further Reading
- Apollo Federation 2 Specification
- GraphQL Specification
- Apollo Router Documentation
- Kong Gateway Documentation
- DGS Framework (Netflix)
- gqlgen (Go)
This article is part of the Polyglot GraphQL Federation series.
