Federation Foibles

Table of Contents

Federated GraphQL solves a real organizational problem: it lets independent teams each own a slice of one graph, without funneling every change through a single schema and a single deployment. It also comes with a collection of sharp edges that stay invisible right up until you catch one with your shin.

This article is a tour of the federation footguns I've run into — how each one shows up, why it happens, and what to do instead.

Client Normalized Store + Merging

Many of the first-party Apollo GraphQL clients implement a normalized store over objects. Every object fetched as a result of GraphQL queries gets merged into the normalized Apollo store in the clients, which then access that store to render UI elements.

By default, the merging logic merges objects at a field level. For example, if one query returns:

Event {
  id: 1,
  teams: [Team { id: 2, name: "Super Fish"}, Team {id: 3, name: "Awesome Turtles"}]
}

And another query (or another part of the same query) returns:

Event {
  id: 1,
  time: "Monday at noon"
}

The resulting store will have:

Event {
  id: 1,
  teams: [Team { id: 2, name: "Super Fish"}, Team {id: 3, name: "Awesome Turtles"}],
  time: "Monday at noon"
}

This is useful because it allows data to load in slices as needed.

The footgun here is how it merges lists.

If one request retrieves:

Event {
  id: 1,
  teams: [Team { id: 1, name: "Jolly Jaguars"}, Team {id: 2, name: "Super Fish"}],
}

And another query retrieves:

Event {
  id: 1,
  time: "Monday at noon",
  teams: [Team { id: 2, name: "Super Fish"}, Team {id: 3, name: "Awesome Turtles"}],
}

The resulting store will have:

Event {
  id: 1,
  teams: [Team { id: 2, name: "Super Fish"}, Team {id: 3, name: "Awesome Turtles"}],
  time: "Monday at noon"
}

The second list replaces the first — Team { id: 1, name: "Jolly Jaguars" } is silently lost.


Non-specific IDs

A pattern that Federated GraphQL allows is using the @requires directive to require that another subgraph supply certain fields necessary for resolving other fields. An example is the first approach we used for resolving the standings that a scoreboard widget needed for an event.

We created a StandingsLookup type that the Scoreboard subgraph would supply on an Event. Scoreboard resolved the type, @requires-d it on the Stats subgraph, and marked it @inaccessible to clients. It gave Stats the extra parameters needed to resolve a relatedStandings field on Event.

This allowed passing arguments to Stats to provide the event_id and the filters needed to query the underlying data store for the exact standings and rankings required.

The problem is that the client is unaware of this type and runs into a similar issue as described above. If you have two types with the same ID (in this case, Event ID) but different StandingsLookup arguments and different sets of relatedStandings, one object will overwrite the other and the relatedStandings set won't be correct.

Solution

Don't use this pattern. Ensure that your id is fully unique and that any version of the entity with that ID would have the same values all the way down its tree.

The specific solution chosen was to encode all necessary parameters into the id and base64 encode it. This way the IDs for two versions of the entity are distinct and won't get conflated in the Apollo Store.


Federated Subscriptions - What Updates

Federated subscriptions are only updated when the service handling the root subscription updates. This has characteristics that API consumers may not expect.

Example: Say you subscribe to an event's score and also ask for the team and their logo. A federated subscription only pushes an update when its root — the score — changes. If the logo changes on its own, nothing happens: the client holds onto the stale logo indefinitely. The new logo does eventually arrive, but only as a side effect of the next score update, whenever that happens to come along. The fields you tacked on update on the root's schedule, not their own.

Recommendations

  1. Name subscriptions specifically for what is updated in them. For example: ScoreUpdatedSubscription.

  2. Create new types for the data you are returning.

    • This may not be ideal for every type, but you could return a ScoreUpdate type rather than a Score type.
    • The new type would only include fields that the root subgraph can provide.
    • This puts extra work on the client to merge updates into the store, but the implications are clear.
  3. Have the backend publish updates when other potential pieces of the object tree update.

    • For example, the Scores subgraph could have a Kafka consumer that consumes team logo updates and republishes all scores.
    • This gives the effect of the subscription updating when any part of it updates.
    • It requires the backend supporting the root subscription to know about all possible child types provided by other subgraphs — not terribly maintainable.
    • Has performance implications for the root subscription subgraph.
    • May be useful in specific scenarios.

AVOID Using Interfaces in Federated Schemas to Enforce Field "Contracts"

GraphQL interfaces — and in particular federated Entity Interfaces (interface types that have a @key directive) — can become difficult to manage.

Alternative: Leverage unions where possible. If you have an interface that defines some fields, consider adding a type to represent those common fields, and a union of "extension types" to handle the differences.

Avoid:

interface Player {
   fullName: String!
}

type BaseballPlayer implements Player {
   fullName: String!
   battingAverage: Float!
}

type FootballPlayer implements Player {
  fullName: String!
  touchdownsScored: Int!
}

Consider:

type Player {
  fullName: String!
  playerExtensions: PlayerData
}

union PlayerData = FootballPlayerData | BaseballPlayerData

type FootballPlayerData {
  touchdownsScored: Int!
}

type BaseballPlayerData {
  battingAverage: Float!
}

Acceptable Use: If the interface provides value from the perspective of the consumer, feel free to add it. One way to verify this is to look at whether the interface is used as an output type for queries or mutations, and whether the client will have to specify fragments for implementations of that interface.


AVOID Using Directives that Introduce Dependencies Between Subgraph Schemas

Certain federated GraphQL directives introduce dependencies between subgraph schemas. Directives that introduce coupling should be used only as a last resort, to avoid consequences that can include:

  1. Preventing one subgraph from evolving independently
  2. Introducing deployment dependencies
  3. Reducing team autonomy by introducing development dependencies

These problems are especially pernicious during the development phase, when schemas can evolve and revert far more frequently than they would in production.

@external / @requires — Both of these directives specify field dependencies on fields defined by other subgraphs.

Alternative: Architect your solution so that the underlying data values that are @required are available to your subgraph directly. In many environments, the underlying values are available in a shared data store, making this a viable solution for many areas. Or, write a Kafka consumer for the relevant topics and store the necessary values locally, removing the dependency on values resolved by other services.

Acceptable Use: If another subgraph produces a value using business logic that you do not want to replicate in your subgraph, this pair of directives may be an acceptable path forward.

@interfaceObject — It's tempting to lump this in with the other directives in this section, but @interfaceObject actually reduces coupling rather than introducing it. It lets a subgraph contribute fields to every implementation of an entity interface without redefining — or even knowing about — each concrete type. The alternative is to declare every implementing type as an entity in your own subgraph and add the field to each one, which couples you to the full set of implementations and forces a coordinated change every time another team adds a new implementation.

The one dependency it does create is on the interface itself: your subgraph relies on the interface (and its @key) continuing to exist in the owning subgraph. We got bitten by exactly that during development — a team removed an interface that another subgraph was extending, which caused rework and coordinated deployments across each environment. The lesson there is narrower than "avoid the directive": treat the interfaces you extend as a contract, and don't churn them while others depend on them.

When to be cautious: If an interface genuinely isn't stable yet, extending the concrete implementations as entities is the more defensive option — at the cost of the tighter coupling described above.


HTTP/Multipart Subscription Limit

HTTP/Multipart subscriptions create a separate request for every subscription. These requests use http2 as their transport. Each request is a "stream" in http2 parlance. While the spec doesn't specify an upper bound to the number of concurrent streams over one connection, in practice there is a limit. The spec allows clients and servers to negotiate this limit based on the SETTINGS_MAX_CONCURRENT_STREAMS setting.

In practice, most browsers and platforms limit this to 100. There may be ways to override this from the server side, but this is inadvisable — it's unclear whether a server-side override would properly impact all clients (all browsers, all mobile HTTP implementations, etc.). Treat 100 as the lowest common denominator and the practical upper limit.

Once the number of streams is exhausted, any further requests will block — including standard request/response calls (like GET and POST) — until some of those 100 streams free up. From the user's perspective nothing has errored; the app has simply, quietly, stopped.