If you've worked with Apollo Federation, you’ve probably used Subgraphs to scale a GraphQL API. But the way entities are resolved between Subgraphs introduces real problems at scale, especially around type safety and correctness.
At WunderGraph, we took a different approach. Instead of resolving entities through GraphQL between services, you can now compile Subgraph SDLs directly into gRPC services. This provides strict typing, built-in batching, and a significantly faster execution model, without compromising the benefits of GraphQL at the router level.
You can read the original deep-dive on our company blog.
Why Entity resolution in Apollo Federation breaks down
In Apollo Federation, each Subgraph defines entities using the @key directive. The router stitches them together using a special _entities field.
The problem? That _entities field accepts a list of _Any. That means there’s no compile-time validation. You can’t tell whether one Subgraph’s resolver will match the expected shape from another.
This often leads to subtle runtime bugs, and we've seen this firsthand with teams using Federation at scale.
What if your router spoke gRPC instead?
Many teams already use gRPC internally and expose GraphQL through shim layers. Backend engineers prefer gRPC’s type safety and performance. Frontend engineers prefer GraphQL’s flexibility.
So we built a system that connects both worlds:
A compiler that turns GraphQL SDLs into gRPC service definitions
A router adapter that translates GraphQL queries into gRPC calls
That means no more GraphQL-to-GraphQL entity lookups between Subgraphs. You can still write GraphQL at the router boundary, but downstream it’s speaking gRPC.
From SDL to gRPC: How it works
Here’s a simple GraphQL Subgraph:
type Query {
me: User!
}
type User @key(fields: "id") {
id: ID!
name: String!
}
Our compiler transforms it into this proto file:
syntax = "proto3";
package service;
service UsersService {
rpc LookupUserById(LookupUserByIdRequest) returns (LookupUserByIdResponse) {}
rpc QueryMe(QueryMeRequest) returns (QueryMeResponse) {}
}
message LookupUserByIdRequestKey {
string id = 1;
}
message LookupUserByIdRequest {
repeated LookupUserByIdRequestKey keys = 1;
}
message LookupUserByIdResponse {
repeated User result = 1;
}
message QueryMeRequest {}
message QueryMeResponse {
User me = 1;
}
message User {
string id = 1;
string name = 2;
}
This solves two problems at once:
- Requests are batched by default, avoiding N+1 issues.
- Everything is strictly typed, so there’s no ambiguity between Subgraphs.
Instead of relying on _Any, you now have a method that guarantees structure and order. Backend engineers no longer need custom data loader logic. The router batches calls automatically.
Mapping GraphQL types to gRPC
Here’s how the translation works:
Object types → protobuf messages
-
Scalars:
- String → string
- ID → string
- Int → int32
- Float → float
- Boolean → bool
Enums → protobuf enums
Input objects → protobuf request messages
Queries and Mutations → gRPC service methods
Example
type Query {
getBook(id: ID!): Book
}
type Book {
id: ID!
title: String!
pages: Int
}
Becomes:
message GetBookRequest {
string id = 1;
}
message GetBookResponse {
Book book = 1;
}
message Book {
string id = 1;
string title = 2;
int32 pages = 3;
}
service QueryService {
rpc GetBook(GetBookRequest) returns (GetBookResponse);
}
Schema evolution with proto lock files
Protobuf enforces field numbers. Once you use a number, you can’t reuse it for another field with a different type.
To manage this, we introduced a proto lock file that reserves previous field numbers:
message User {
reserved 2;
string id = 1;
int32 age = 3;
}
This prevents breaking changes if you remove a field and later add a new one.
Want to try it?
We’ve published a full quickstart tutorial and reference docs.
This is just the first step. We’re building a new Federation model where GraphQL stays at the edge, and the internals run on fast, type-safe gRPC.
Let us know what you think:
Join our Discord
Top comments (0)