Federation - When One Graph Isn't Enough (A Tale of Microservices)

You've heard the promise: "One graph to rule them all." But your organization has 47 microservices, 12 teams, and zero patience for a monolithic schema. Enter Federation - GraphQL's answer to "it's complicated."
The Monolith Problem
Picture this: You're at BigCorp Inc. Your GraphQL API started beautifully:
type Query {
user(id: ID!): User
product(id: ID!): Product
order(id: ID!): Order
}
Three types. One team. Life was good.
Fast-forward two years:
type Query {
# User team
user(id: ID!): User
users(filter: UserFilter): [User!]!
currentUser: User
# Product team
product(id: ID!): Product
products(filter: ProductFilter): [Product!]!
productsByCategory(category: String!): [Product!]!
searchProducts(query: String!): [Product!]!
# Orders team
order(id: ID!): Order
orders(userId: ID!): [Order!]!
ordersByStatus(status: OrderStatus!): [Order!]!
# Inventory team
inventory(productId: ID!): Inventory
lowStockProducts: [Product!]!
# Reviews team
reviews(productId: ID!): [Review!]!
userReviews(userId: ID!): [Review!]!
# ... 50 more query fields
}
The schema file is 3,000 lines. Merge conflicts happen daily. The "GraphQL team" has become a bottleneck. Teams are angry. Developers are leaving. The CTO wants answers.
The Federation Promise
Federation lets each team own their slice of the graph:
Each service owns its types, extends shared types, and deploys independently.
Federation Concepts
Concept 1: Entity Types
An entity is a type that can be referenced across services. It has a key:
# Users Service
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
The @key directive says: "This type can be looked up by id from any service."
Concept 2: Extending Types
Other services can add fields to entities owned elsewhere. In Federation 2 (the default since 2022), you redeclare the type with @key and add the new field; you don't need extend type or @external on the key in the owning-by-reference subgraph. The examples below use Fed 2 syntax. (If you're on Fed 1, the same examples need extend type ... { id: ID! @external; ... } instead - migrate to Fed 2 if you can.)
# Orders Service (Federation 2)
type User @key(fields: "id") {
id: ID!
orders: [Order!]!
}
type Order @key(fields: "id") {
id: ID!
user: User!
total: Money!
}
The Orders Service doesn't know how to fetch a user's name - but it can add orders to the User type.
Concept 3: The Gateway
A gateway or router (Apollo Router, Cosmo Router, GraphQL Mesh, Hive Gateway, etc.) stitches services together:
# Client's view - seamless!
query UserWithOrdersAndReviews {
user(id: "123") {
name # From Users Service
email # From Users Service
orders { # From Orders Service
total
}
reviews { # From Reviews Service
rating
}
}
}
The client sees one graph. Under the hood, three services collaborate.
Implementing Federation with Spring
Setup: The Users Subgraph
<!-- pom.xml -->
<dependency>
<groupId>com.apollographql.federation</groupId>
<artifactId>federation-graphql-java-support</artifactId>
<!-- Use the latest release; check Maven Central -->
<version>6.1.0</version>
</dependency>
# src/main/resources/graphql/schema.graphqls
type Query {
user(id: ID!): User
users: [User!]!
}
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
createdAt: DateTime!
}
@Controller
public class UserController {
private final UserRepository userRepository;
@QueryMapping
public User user(@Argument String id) {
return userRepository.findById(id).orElse(null);
}
@QueryMapping
public List<User> users() {
return userRepository.findAll();
}
}
// Entity resolver - for federation lookups. Spring for GraphQL has
// a dedicated @EntityMapping annotation for exactly this (backed by
// federation-graphql-java-support; you also declare a
// FederationSchemaFactory bean). The method name matches the entity
// type, and the @key fields arrive as arguments:
@Controller
public class UserEntityController {
private final UserRepository userRepository;
// Resolves the User entity when another subgraph references it
// by its @key fields.
@EntityMapping
public User user(@Argument String id) {
return userRepository.findById(id).orElse(null);
}
}
Setup: The Orders Subgraph
# schema.graphqls
type Query {
order(id: ID!): Order
ordersByUser(userId: ID!): [Order!]!
}
type Order @key(fields: "id") {
id: ID!
userId: ID!
items: [OrderItem!]!
total: Money!
status: OrderStatus!
createdAt: DateTime!
}
# Add `orders` to User (owned by Users Service); Fed 2 syntax
type User @key(fields: "id") {
id: ID!
orders: [Order!]!
}
type OrderItem {
productId: ID!
quantity: Int!
price: Money!
}
type Money {
amount: Float!
currency: String!
}
enum OrderStatus {
PENDING
PAID
SHIPPED
DELIVERED
}
@Controller
public class OrderController {
private final OrderRepository orderRepository;
@QueryMapping
public Order order(@Argument String id) {
return orderRepository.findById(id).orElse(null);
}
@QueryMapping
public List<Order> ordersByUser(@Argument String userId) {
return orderRepository.findByUserId(userId);
}
// Resolve User.orders
@SchemaMapping(typeName = "User", field = "orders")
public List<Order> ordersForUser(User user) {
return orderRepository.findByUserId(user.getId());
}
}
Setup: The Gateway
Using Apollo Router (Rust-based, high performance). Note that subgraph routing URLs live in supergraph.yaml (consumed by rover supergraph compose), not in router.yaml. Routing URLs are baked into the composed supergraph schema. The subgraphs: key in router.yaml is only for per-subgraph runtime overrides like headers and timeouts.
# supergraph.yaml - lists subgraphs and their routing URLs
federation_version: 2
subgraphs:
users:
routing_url: http://users-service:8080/graphql
schema:
subgraph_url: http://users-service:8080/graphql
products:
routing_url: http://products-service:8080/graphql
schema:
subgraph_url: http://products-service:8080/graphql
orders:
routing_url: http://orders-service:8080/graphql
schema:
subgraph_url: http://orders-service:8080/graphql
# router.yaml - runtime configuration
supergraph:
introspection: true
listen: 0.0.0.0:4000
headers:
all:
request:
- propagate:
named: authorization
You compose with rover supergraph compose --config supergraph.yaml > supergraph.graphql, then run the router with --supergraph supergraph.graphql --config router.yaml.
The Query Execution Dance
Let's trace a federated query:
query UserWithOrders {
user(id: "123") {
name
email
orders {
id
total
items {
productId
}
}
}
}
Federation Patterns
Pattern 1: Shared Types
Types that multiple services need to understand:
# Shared schema (published to all services)
type Money @shareable {
amount: Float!
currency: Currency!
}
enum Currency {
USD
EUR
GBP
}
Each service can use Money without owning it.
Pattern 2: Computed Fields
Add computed fields to entities you don't own. @requires declares non-key fields the resolver needs from another subgraph; those fields must be marked @external here. Fed 2 doesn't need extend type; the type is just redeclared with @key.
# Analytics Service (Federation 2)
type User @key(fields: "id") {
id: ID!
signupDate: DateTime! @external
# Score is computed using signupDate fetched from the Users subgraph
engagementScore: Float! @requires(fields: "signupDate")
}
type Product @key(fields: "id") {
id: ID!
# Pure analytics-computed fields; no @requires because they're
# derived from this subgraph's own analytics data.
popularityRank: Int!
conversionRate: Float!
}
Pattern 3: The Reference Resolver
When one service needs minimal data from another, you reference the entity by its key. You don't redeclare fields you're not extending or requiring - the gateway handles cross-subgraph field resolution automatically:
# Orders Service - just reference Product by its @key, no @external needed
type OrderItem {
quantity: Int!
product: Product! # Reference; Product fields are resolved by their owning subgraph
}
type Product @key(fields: "id") {
id: ID!
}
The Orders Service stores productId. The router calls Products Service's entity resolver for any Product fields the client requests.
Pattern 4: Override
When you need to migrate a field's resolver from one subgraph to another. The original subgraph must mark the field @shareable (or be migrated atomically), and the new subgraph claims ownership with @override:
# Products Service (original owner)
type Product @key(fields: "id") {
id: ID!
name: String!
inventory: Int! @shareable # Allow another subgraph to define this too
}
# Inventory Service - takes over inventory at compose time
type Product @key(fields: "id") {
id: ID!
inventory: Int! @override(from: "products")
}
Federation Challenges
Challenge 1: N+1 in Disguise
query UsersWithOrders {
users { # Users Service: 1 call
name
orders { # Orders Service: N calls? Or batched?
total
}
}
}
Solution: The gateway batches entity lookups:
# Gateway calls Orders Service ONCE with all user IDs
_entities(representations: [
{ __typename: "User", id: "1" },
{ __typename: "User", id: "2" },
{ __typename: "User", id: "3" }
]) {
... on User { orders { total } }
}
Make sure your service handles batches efficiently. The actual wiring depends on which library you use - federation-graphql-java-support exposes a batched entity-fetcher hook; the example below shows the conceptual shape instead of a specific annotation:
// Batched entity resolver - adapt to your federation library's API
public List<User> resolveUsers(List<Map<String, Object>> representations) {
List<String> ids = representations.stream()
.map(rep -> (String) rep.get("id"))
.toList();
Map<String, User> usersById = userRepository.findAllById(ids).stream()
.collect(Collectors.toMap(User::getId, u -> u));
return ids.stream()
.map(usersById::get)
.toList();
}
Challenge 2: Circular Dependencies
# Users Service
type User @key(fields: "id") {
id: ID!
favoriteProduct: Product # Depends on Products
}
# Products Service
type Product @key(fields: "id") {
id: ID!
topReviewer: User # Depends on Users
}
This creates a circular dependency. Solutions:
- Accept it - Federation handles cycles
- Break the cycle - Move one field to a third service
- Use interfaces - Abstract the dependency
Challenge 3: Schema Coordination
Who decides what User looks like when 5 services extend it?
Solution: Schema governance
# .github/workflows/schema-check.yml
name: Schema Check
on: [pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Rover Schema Check
env:
APOLLO_KEY: ${{ secrets.APOLLO_KEY }}
run: |
rover subgraph check my-graph@production \
--schema ./schema.graphqls \
--name users
Migration Strategy
Phase 1: Identify Boundaries
| Current Monolith | Potential Service |
|---|---|
| User types | Users Service (Team A) |
| Product types | Products Service (Team B) |
| Order types | Orders Service (Team C) |
| Review types | Reviews Service (Team D) |
| Payment types | Payments Service (Team E) |
| Money (shared) | Shared library / copied |
| Address (shared) | Shared library / copied |
Phase 2: Gateway Introduction
Run federation alongside your monolith:
Compare responses. Ensure compatibility.
Phase 3: Traffic Shifting
Week 1: Gateway → 100% Monolith
Week 2: Gateway → 90% Monolith, 10% Users Service
Week 3: Gateway → 50% Monolith, 50% Users Service
Week 4: Gateway → 0% Monolith (for users), 100% Users Service
Phase 4: Extract More Services
Repeat for Products, Orders, etc.
When Not to Federate
Federation isn't free. Skip it if:
❌ Small team - Coordination overhead > benefits ❌ Simple schema - < 50 types, why complicate? ❌ Same codebase - Services share a repo anyway ❌ Sync-heavy - Types change together frequently
Use federation when:
✅ Multiple teams - Need independent deployment ✅ Large schema - 100+ types, growing ✅ Different tech stacks - Java, Node, Python services ✅ Different data stores - Each service owns its data
Conclusion
Federation is GraphQL's answer to microservices. It lets teams own their domains while presenting clients with a unified graph.
But it's not magic. It adds complexity: gateways, entity resolvers, schema coordination, debugging across services.
The question isn't "Is federation good?" It's "Do we have the problems federation solves?"
If the answer is yes - if you're fighting merge conflicts daily, if teams are blocked on the GraphQL team, if your schema has become unwieldy - federation is your path to sanity.
If not, enjoy your monolith. There's nothing wrong with a well-designed single service.
This blog post was written by a team of microservices, federated through one author's brain. Query plan available upon request.