In Part 1, we mapped out why monolithic GraphQL APIs break down and how federation distributes ownership across services. The architecture diagram looked clean — four subgraphs, one router, unified schema. But architecture diagrams don't ship.
This article gets into the code. We'll examine how three different language ecosystems implement the same Federation 2 specification, where the ergonomics diverge, and what entity resolution actually looks like when the router sends a __resolveReference call to your service.
The Federation Contract
Every subgraph in a Federation 2 supergraph must fulfill a specific contract. Regardless of language, each service must:
- Declare federation support via a schema
@linkdirective - Mark entities with
@key(fields: "...")to enable cross-service resolution - Implement
__resolveReferencefor each entity it owns - Expose the
_serviceand_entitiesroot fields for introspection and entity resolution
The federation spec doesn't care what language implements these requirements. It only cares about the GraphQL responses. This is what makes polyglot federation possible — and what makes comparing implementations instructive.
All three implementations fulfill the same federation contract. The libraries differ in how much they automate versus require manual wiring.
TypeScript: The Path of Least Resistance
Apollo Server with @apollo/subgraph is the reference implementation for federation. The ergonomics reflect this — federation support is built into the schema builder with minimal configuration.
Schema Definition
The User/Auth service owns User and Review entities and extends Product with reviews:
Entity Resolution
The __resolveReference function is defined as a resolver on each entity type. When the router needs to resolve a User stub (e.g., { __typename: "User", id: "abc" }), it calls this function:
Server Setup
Building a federated subgraph with Apollo Server requires buildSubgraphSchema:
That's it. buildSubgraphSchema handles the _entities and _service root fields automatically. The resolver map follows standard Apollo conventions. If you've written an Apollo Server before, the federation additions are minimal.
Friction points: Drizzle ORM requires mapping database rows to GraphQL types manually when field names don't match. The context typing needs explicit declaration for user headers propagated by the router.
Java: Manual Wiring, Maximum Control
The Java ecosystem uses graphql-java with federation-jvm for federation support, typically paired with Micronaut or Spring as the application framework. The wiring is more explicit than TypeScript — you register each data fetcher individually.
Schema Definition
The Product Catalog service defines its schema in a .graphqls file:
Entity Resolution with federation-jvm
Federation entity resolution in Java requires implementing a TypeResolver and registering data fetchers for the _entities field:
Data Fetchers
Each query and field resolution is an explicit data fetcher:
Friction points: The Federation.transform() API requires manual entity dispatch — you write the switch on __typename yourself. Every field resolver is an explicit registration. There's no convention-over-configuration; you wire everything by hand. The upside is full visibility into the resolution pipeline.
Go: Code Generation Meets Federation
Go's gqlgen takes a fundamentally different approach. You write the GraphQL schema, run go generate, and gqlgen produces Go types and resolver interfaces. You implement the interfaces. The federation plugin handles the entity resolution boilerplate.
Schema Definition
The Order Service schema uses federation directives like the others:
Generated Code and Resolver Implementation
After running go generate ./..., gqlgen creates resolver interfaces that you implement:
Entity Resolution in gqlgen
The federation plugin generates entity resolution scaffolding. You implement a function per entity:
That's the entire entity resolver. The generated code handles the _entities root field, the __typename dispatch, and the batch resolution. You provide the lookup function.
Friction points: gqlgen's code generation creates a generated.go file that can be thousands of lines long. Build times increase with schema size. The model types are generated, so customizing serialization requires gqlgen.yml directives. On the positive side, the federation plugin is mature and handles edge cases like batch entity resolution automatically.
Comparing the Ecosystems
After implementing federation across all three languages, patterns emerge:
| Aspect | TypeScript | Java | Go |
|---|---|---|---|
| Schema location | Inline (tagged template) | External .graphqls file | External .graphqls file |
| Entity resolution | Resolver function on type | Manual switch on __typename | Generated function per entity |
| Type safety | Runtime (zod validation) | Compile-time (strong types) | Compile-time (generated types) |
| Framework coupling | Tight (Apollo Server) | Loose (graphql-java is library) | Medium (gqlgen conventions) |
| Hot reload | Sub-second (tsx watch) | 30-60s (Gradle rebuild) | 5-10s (codegen + go build) |
| Federation overhead | ~10 lines | ~50 lines | ~5 lines (rest generated) |
TypeScript wins on iteration speed. Java wins on explicit control. Go wins on type-safe code generation with minimal manual wiring.
Database-Per-Service Isolation
Each subgraph connects to its own PostgreSQL database. This isn't just organizational — it enforces domain boundaries at the data level:
Each service manages its own schema migrations. TypeScript uses Drizzle ORM with drizzle-kit push. Java uses Flyway with versioned SQL files (V1__create_tables.sql). Go uses golang-migrate with timestamped up/down migrations.
Cross-service data access happens exclusively through federation entity resolution — never through shared database access. If the Order Service needs a product name, it returns a Product stub with just the id, and the router fetches the name from the Product Catalog subgraph.
Looking Forward
With all four subgraphs running, the router can compose them into a supergraph. But this platform doesn't stop at GraphQL. The Product Catalog talks to Inventory over gRPC for high-performance stock checks. The Order Service calls Stripe's REST API for payment processing.
In Part 3, we'll examine why certain service boundaries call for different protocols and how gRPC and REST coexist alongside the federated GraphQL layer.
Further Reading
- Micronaut GraphQL Documentation — The Java framework used for GraphQL subgraph implementation
- Netflix DGS Framework — Alternative Java-native GraphQL federation framework
- gqlgen Documentation (v0.17+) — The Go GraphQL framework used in this series
- Vogels, W. (2009). "Eventually Consistent" — CACM. Relevant to the entity ownership and consistency model in federated architectures
This article is part of the Polyglot GraphQL Federation series. Continue to Part 3: Hybrid Protocols to see how GraphQL, gRPC, and REST coexist in one platform.
