Distributed computing

One Graph, Many Sources

At Allē, a provider’s profile, search results and appointments lived in different services and different databases. GraphQL federation made them one graph, and managed federation, schema checks in CI and a council kept twenty-plus teams from breaking it.

A map before you read

One line per section, written by an AI model running entirely in your browser. Free, private, nothing leaves your device.

At Allē, AbbVie’s platform for aesthetic treatments, the data behind a single screen came from everywhere. A provider’s profile lived in Postgres, in the Provider service I wrote in Go. Their place in search results came from Elasticsearch. Their treatments, their rewards, their appointments and their reviews each belonged to a different service, owned by a different team, often backed by a different database. A page that showed one provider needed answers from half a dozen of them.

The usual answer is a backend for each screen, an API that knows where everything lives and stitches it together. It works until there are many screens and many teams, and then the stitching becomes its own team, permanently behind. GraphQL offers a better answer, and federation makes it work across an organization. The data stays where it lives, owned by whoever owns it, and the client sees one graph.

Why GraphQL Handles Many Sources Well

The trick is the resolver. In GraphQL, every field on every type has its own function that knows how to produce it, and nothing says two fields on the same type have to come from the same place. A provider’s name can come from Postgres while their rating comes from a review service and their distance comes from a search index, and the client asking for all three writes one query and gets one response. It never learns, or needs to learn, where each field lives.

That’s already most of the elegance. The schema is a contract about the shape of the data, not about where the data is stored, so a field can move from one database to another without a single client changing.

Federation: One Graph, Many Owners

A single GraphQL server with resolvers for everything still has one owner, and that owner becomes the bottleneck. Federation splits the graph by domain. In Apollo’s terms, it “enables you to declaratively combine multiple APIs into a single federated GraphQL API,” where the combined graph is a supergraph, the parts are subgraphs, and a router “intelligently orchestrates and distributes the request across your APIs” (Apollo).

Netflix described the same move from the other side. Its original studio API team was “disconnected from the domain expertise and the product needs,” so it kept one unified schema but decided to “decentralize the implementation of the resolvers to their respective domain teams” (Netflix). That’s the whole idea. One graph for the people asking, many owners for the people answering.

What makes the pieces fit is the entity. An entity is a type that more than one subgraph can contribute to, identified by a key. At Allē, Provider was one. My service owned it and resolved its core fields from Postgres:

# Provider subgraph (Go, Postgres)
type Provider @key(fields: "id") {
  id: ID!
  name: String!
  address: Address!
}

Another team’s subgraph could add fields to the same type without touching mine, as long as it could resolve a provider from its key:

# Reviews subgraph (its own service and database)
type Provider @key(fields: "id") {
  id: ID!
  rating: Float
  reviews(first: Int = 10): [Review!]!
}

A client then asks one question across both:

query ProviderPage($id: ID!) {
  provider(id: $id) {
    name
    address { city }
    rating
    reviews(first: 3) { text }
  }
}

The router plans it, asks the Provider subgraph for the name and address, hands the provider’s key to the Reviews subgraph for the rest, and assembles one response. Apollo describes an entity as an object “that can be fetched with one or more unique key fields,” and the representation that passes between subgraphs is just the key plus a __typename (Apollo). Neither team calls the other. The router does.

The provider page sends one query to the router. The router asks the Providers subgraph, backed by Postgres, for the name and address, and passes the provider's key to the Reviews subgraph, backed by its own database, for the rating and reviews. The Search subgraph and Elasticsearch sit idle because the query asks nothing of them. The router returns one response.
One query, two owners, two databases. The router plans which subgraph answers which field; the Search subgraph sits this one out.

The Cost: Fan-Out

That handoff has a cost worth knowing before it bites. When the router asks another subgraph for fields on a list of providers, the subgraph resolves each one through a reference resolver, and Apollo’s own guidance warns that it “doesn’t take a list of keys but rather a single key,” so resolving a field for N providers “results in N database calls” (Apollo). A search results page with fifty providers and ratings on each can quietly become fifty queries against the reviews database.

The fix is old and still the right one. Batch inside each subgraph with a per-request loader, so fifty lookups by ID become one query with fifty IDs. And watch the router itself: Apollo’s gateway documentation notes that throughput “drops linearly as subgraph latencies rise,” so one slow subgraph slows every query that touches it.

0102030405001020304050Providers on the pageQueries to the reviews DBone at a time: 50 queriesbatched with a loader: 1 query
Queries the reviews database runs for one page. Resolved one key at a time, they grow with the page; batched by a per-request loader, they don’t. Illustrative numbers.

Managed Federation: Governance in the Pipeline

The harder problem at twenty-plus subgraphs isn’t performance. It’s change. Any team can rename a field, tighten a type or remove something another team’s screen depends on, and in a federated graph that breaks someone else’s product.

We used Apollo’s managed federation, where subgraphs publish their schemas to a registry and the router pulls the composed supergraph from it, and the part that mattered most was the checks. Every schema change ran through them in CI before it could ship. A build check “verifies that changes you make to a subgraph schema are valid GraphQL definitions” and that the subgraphs still compose into one graph. An operations check goes further and asks “whether any clients would be negatively affected by the proposed schema changes,” by testing the change against the operations clients are actually sending (Apollo). In CI, that’s one command per subgraph:

rover subgraph check alle-graph@prod --name providers --schema ./schema.graphql

A change that would break composition failed the build. A change that would break a real client’s query failed too, unless someone with the authority approved it, which turned a breaking change from an accident into a decision.

A subgraph schema change runs through rover subgraph check in CI: a build check asks whether the graph still composes, then an operations check asks whether it breaks a real client. A safe change is published to the registry. A breaking one goes to council review and is published only if approved. The registry composes the supergraph and the router pulls it.
The path every schema change took. Safe changes publish themselves; breaking ones need a person to say yes.

The Council

Checks catch what’s already broken. They don’t decide what the graph should look like. After building the first new service since the platform’s MVP, I pointed out that we had no written standards and no review for the schema, and I helped found a GraphQL Council to fix that. It set the standards for the graph and reviewed every GraphQL change across a federated schema with more than twenty services contributing to it.

The council’s work was the part no tool does: naming, so the same idea wasn’t called three things in three subgraphs; ownership, so every type had one team responsible for it; and judgment on the changes the checks flagged, so a breaking change happened on purpose, with the teams it affected in the room. Apollo’s guidance points the same way, suggesting teams mark ownership explicitly in the schema with a custom directive and route reviews to the owning team (Apollo). The Principled GraphQL principles put it more simply: “one unified graph,” whose implementation “should be federated across multiple teams,” with “a single source of truth for registering and tracking the graph” (Principled GraphQL).

Takeaways

If I were building a multi-source platform again, I’d put GraphQL in front of it for the same reason we did, because a resolver per field lets every piece of data stay where it lives while clients see one shape. I’d federate as soon as more than one team owned the data, and make the most-shared types entities with a clear owner. I’d batch every reference resolver from the start, before a list page finds the N+1 for me. I’d run composition and operation checks in CI on every subgraph, so breaking changes need an approval rather than an apology. And I’d stand up the council early, because the tools tell you what will break, and only people can decide what the graph should mean.

Federation made many sources one graph. Governance kept it one graph.

Sources

  1. Apollo, “Introduction to Apollo Federation”
  2. Apollo, “Introduction to Entities”
  3. Apollo, “Handling the N+1 Problem”
  4. Apollo, “Schema Checks”
  5. Apollo, “Enforcing Entity Ownership”
  6. Netflix Technology Blog, “How Netflix Scales its API with GraphQL Federation (Part 1),” November 2020
  7. Principled GraphQL, “Integrity Principles”