Skip to main content

Entity ownership is an org problem wearing a schema

Federation's hardest questions are never syntax. They're "who owns Product?" — and that's a people question.

The gotcha

Every team touching an entity adds "just one field" to it. Two years later, Product is contributed to by nine subgraphs, no team owns its lifecycle, and a rename requires a nine-team change train. Federation didn't decouple your teams; it gave them a shared object to fight over.

What actually works

One entity, one owning subgraph. The owner defines the keys and the core identity fields. Everyone else extends with fields they genuinely resolve from their own domain:

# products subgraph — the OWNER
type Product @key(fields: "id") {
id: ID!
name: String!
category: Category!
}

# reviews subgraph — a CONTRIBUTOR
type Product @key(fields: "id") {
id: ID!
reviews(first: Int!): ReviewConnection!
averageRating: Float
}

Rules we enforce in schema review:

  • A contributing subgraph may only add fields it is the source of truth for. If the resolver just calls the owner's service, the field belongs in the owner's subgraph.
  • Keys are chosen by the owner and are stable business identifiers — never database sequence IDs that differ per environment.
  • Adding a new contributing subgraph to an entity requires the owner's review. Composition passing is not approval.

The @key trap: resolvable references

Every @key you declare is a promise that your subgraph can resolve that entity from the key alone, at any time, under full production load. Teams declare keys casually, then discover their reference resolver:

  • does a full table scan because the key isn't indexed,
  • gets called in batches of 1 by a naive gateway plan (see N+1 at the gateway),
  • or fails for entities created before some migration.

Load-test your reference resolvers in isolation. They're the hottest code path in a federated graph and the least-tested in most codebases.

Smells that ownership has drifted

  • A "shared" or "common" subgraph exists whose only job is holding types nobody wanted.
  • Fields whose resolver is a passthrough HTTP call to another subgraph's service.
  • Composition succeeds, but nobody can answer "if Product.name is wrong, which team do I page?"

If any of these are true, you have an ownership problem — and no amount of directives will fix it. Fix the ownership map first; the schema follows.