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:
- Fetch from Order Service: get the order, its items, and product IDs
- Parallel fetch from Product Catalog: resolve product names by ID
- Parallel fetch from Inventory: resolve inventory by product ID
- Parallel fetch from User Service: resolve reviews by product ID
- Merge all results into a single response
Steps 2-4 happen concurrently. The client sees a single response with a latency roughly equal to the slowest subgraph, not the sum of all subgraphs.
The Gateway Stack
This platform uses a two-layer gateway architecture that separates API concerns from GraphQL concerns:
Kong API Gateway handles cross-cutting concerns that apply to all traffic:
- JWT validation and user context extraction
- Rate limiting (60 req/min for GraphQL, 20 req/min for auth endpoints)
- CORS enforcement
- Request ID generation for distributed tracing
Apollo Router handles GraphQL-specific concerns:
- Supergraph schema composition from four subgraphs
- Query planning and parallel execution
- Header propagation (
x-user-id,x-user-role,x-request-id) - OpenTelemetry trace export
This separation is deliberate. Kong is a general-purpose API gateway that knows nothing about GraphQL. 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 — Building Federated Subgraphs — 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 (Tempo, Prometheus, Loki, Pyroscope) with spanmetrics connectors, tail sampling, SLO tracking, and cross-signal correlations
Looking Forward
In the next article, we'll step inside each subgraph and examine how Java, Go, and TypeScript each implement Federation 2 directives. We'll see 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.
This article is part of the Polyglot GraphQL Federation series. Continue to Part 2: Three Languages, One Schema to see how each subgraph is built.
