•
4 min read
removing a network hop with graphql federation
graphql federation backend

The CRM’s create, read, update, and delete traffic ran through a legacy backend-for-frontend. Any read that crossed a domain boundary, a contact that the home-search side also knew about, for instance, meant one service calling another over HTTP inside the request, to resolve data the graph could have resolved in place. Latency on every request, plus coupling: the CRM backend depended on the home-search backend’s API, and the reverse.

I moved the CRUD operations off the BFF into the federated GraphQL graph and federated the contact entity across the subgraphs that share it. This post is what that means mechanically and why it produced the number it did.

what a federated entity is

In a federated graph, several services each publish a piece of the schema, and a router composes them into one. An entity is a type that more than one service knows about, identified by a key. One subgraph owns the entity and resolves it by key. Other subgraphs extend it with the fields they own, and the router stitches the pieces together when a query asks for fields from both.

The contact was the obvious candidate. The CRM owns its identity and its CRM-side fields. The home-search side owns the fields about what that person has saved and searched. Before federation, a query that needed both made the CRM service call the home-search service. After, each subgraph resolves only its own fields for a given key, and the router does the composition at the edge.

what the hop cost

An inter-service HTTP call inside a request is a network round trip, a serialization boundary, a connection from a pool, and a second service’s request pipeline, all on the critical path of the caller. Removing it does not make the underlying database reads faster. It removes everything around them.

Measured before and after in APM on the operations whose hop was removed: cross-service p95 down about 30%. The mechanism explains the magnitude. One network hop per request is the kind of change that produces a number like that, and if someone asks how it was measured, the answer is the same traces, same operations, before and after.

the coupling is the bigger win

The latency number is what goes on a resume. The coupling change is what an engineer on either team notices.

Before, the CRM backend had a client for the home-search backend’s HTTP API, and a change to that API was a change to the CRM’s code. After, each domain owns its fields on the shared entity and publishes them to the graph. Neither service imports the other’s client. The router is the only thing that knows both exist.

The legacy BFF also stopped accreting routes. Once the CRUD operations lived in the graph, there was no reason to add a new endpoint to the BFF for the next feature, and it began the slow retirement that legacy aggregation layers get when the thing that replaces them is already in place.

what federation asks in return

Entity ownership has to be explicit. Two subgraphs cannot both claim the same field, and the composition step refuses a supergraph where they try. That refusal is a feature: an ownership dispute surfaces at build time as a composition error rather than at runtime as two services disagreeing about a value.

The subgraph boundary is where N+1 hides. A query for a list of contacts with home-search fields on each becomes one entity-resolution call per contact unless the resolving subgraph batches by key. Data loaders at the entity resolver are not optional in a federated service, and the chat backend I work on now resolves every relation through one for that reason.

Composition can break on deploy. A subgraph that publishes a schema change the supergraph cannot compose is rejected by the router, which is the safe failure, and which also means a deploy pipeline needs a composition check before the subgraph rolls out.

None of that is a reason to avoid federation. It is the list of things the BFF was doing implicitly, badly, and in one team’s code. Federation makes each of them explicit and puts it in the graph, where every team can see it.