How Java, Go, and TypeScript each implement GraphQL Federation 2 subgraphs with entity resolution, schema directives, and database-per-service isolation in a real e-commerce platform.
React to this article
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.
Every subgraph in a Federation 2 supergraph must fulfill a specific contract. Regardless of language, each service must:
Declare federation support via a schema @link directive
Mark entities with @key(fields: "...") to enable cross-service resolution
Implement __resolveReference for each entity it owns
Expose the _service and _entities root 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.
Loading diagram...
All three implementations fulfill the same federation contract. The libraries differ in how much they automate versus require manual wiring.
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.
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:
Building a federated subgraph with Apollo Server requires buildSubgraphSchema:
import { ApolloServer } from '@apollo/server';import { buildSubgraphSchema } from '@apollo/subgraph';const server = new ApolloServer({ schema: buildSubgraphSchema([{ typeDefs, resolvers }]),});
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.
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.
Each query and field resolution is an explicit data fetcher:
@Singletonpublic class ProductDataFetcher { private final ProductService productService; public DataFetcher<Product> getById() { return env -> { String id = env.getArgument("id"); return productService.findById(UUID.fromString(id)).orElse(null); }; } public DataFetcher<List<Product>> getAll() { return env -> { String categoryId = env.getArgument("categoryId"); Integer limit = env.getArgumentOrDefault("limit", 20); Integer offset = env.getArgumentOrDefault("offset", 0); return productService.findAll(categoryId, limit, offset); }; } public Product findById(String id) { return productService.findById(UUID.fromString(id)).orElse(null); }}
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'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.
The federation plugin generates entity resolution scaffolding. You implement a function per entity:
// services/order-go/graph/entity.resolvers.gofunc (r *entityResolver) FindOrderByID(ctx context.Context, id string) (*model.Order, error) { // Called by the router when resolving Order entity references return r.Query().Order(ctx, id)}
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.
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.
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.