Identifying Entity Ownership with Domain-Driven Design

Deciding which subgraph owns which entity is the one federation decision you cannot cheaply revise, and it is usually made by whoever happens to be writing the first schema. Domain-driven design gives you a better basis than convenience: ownership follows the bounded context that decides what a concept means, not the service that stores its rows. This page turns that principle into a procedure. It sits under Defining Subgraph Boundaries for Microservices.

When to use this pattern

  • Use it before the first @key is written, when ownership is still free to move and no client depends on it.
  • Use it when two teams both claim a type — the disagreement is almost always that they are describing two different concepts with one name.
  • Skip the ceremony for a type only one team will ever touch. Modelling effort is worth spending on shared concepts, not on a subgraph’s private types.

Prerequisites

Ownership follows meaning, not storage

The instinct is to give a type to whichever service owns its database table. That produces a graph shaped like your infrastructure rather than your domain, and it breaks the first time two services legitimately store rows about the same concept.

The domain-driven answer is that a type belongs to the bounded context that decides what it means — the context whose language defines its states, whose rules govern its transitions, and whose team is asked when someone wants to change it. A shipping service that stores a row per order does not own Order; it owns a shipment that references one.

The useful test is a question about authority rather than data: if the definition of this concept changed tomorrow, whose approval would be required? That team owns the type. Everyone else contributes fields to it.

Applying this consistently produces something that surprises teams new to federation: many concepts that look shared turn out to be several distinct concepts wearing one name. The Customer that billing cares about is a party with a payment relationship; the Customer that support cares about is a person with a contact history. Modelling them as one type forces two contexts to agree on a definition neither wants, and the resulting schema serves both badly.

Storage is not authority Three services all store rows about an order. Only one of them decides what an order is, what states it can be in, and when it may transition, and that is the one that should own the type. Three services store orders; one defines them service stores order rows? decides what an order is? order management yes yes — this one owns the type shipping yes, a projection no — contributes shipment fields analytics yes, a copy no — contributes metrics data warehouse yes, everything no — and is not in the graph Storage answers "who has a copy"; the graph needs "who is the authority".

Implementation walkthrough

The procedure is four steps and produces SDL as its output.

Step one — list the concepts and the language around each. For every candidate type, write the sentence a domain expert would use to define it. If two teams write incompatible sentences, you have found two concepts, not one owner dispute.

Step two — find the decider. For each concept, name the team whose approval a definitional change would need. That team owns the type.

Step three — assign the key. The owning context chooses the identifier, and it should be one it can guarantee: a surrogate id it issues, not a natural key borrowed from elsewhere.

Step four — let everyone else contribute. Other subgraphs declare the type with the key marked @external and add only their own fields.

# order-management subgraph — the owner
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.9", import: ["@key"])

type Order @key(fields: "id") {
  id: ID!                 # issued here, meaningful only here
  status: OrderStatus!    # this context defines the states
  placedAt: DateTime!
}

enum OrderStatus { PLACED CONFIRMED CANCELLED }
# shipping subgraph — a contributor, not an owner
extend schema
  @link(url: "https://specs.apollo.dev/federation/v2.9",
        import: ["@key", "@external"])

type Order @key(fields: "id") {
  id: ID! @external
  # Shipping's own concept, attached to the order it references.
  shipment: Shipment
}

type Shipment @key(fields: "id") {
  id: ID!
  # Shipping defines these states; order management has no opinion on them.
  state: ShipmentState!
}

Notice what shipping did not do: it did not add a fulfilmentStatus to Order, which would have put a second status vocabulary on a type it does not own. Its states live on its own type, and the relationship is a field. That distinction is what keeps two contexts from fighting over one enum for the next two years.

Verification steps

The strongest verification is social rather than technical, but there are mechanical checks worth running.

# Every entity should have exactly one subgraph declaring its key without @external.
rover supergraph compose --config supergraph.yaml > supergraph.graphql
grep -B2 "@key" supergraph.graphql | grep -c "join__type.*resolvable: false"

Then check your git history, which is the cheapest evidence available. For each entity, look at whether changes to its fields historically arrive in one repository or several. A type whose changes always touch three repositories is not owned; it is shared, and the boundary is in the wrong place.

Finally, run the question past the people involved. Ask each team “if we changed what this means, who would you need to ask?” and see whether the answers converge on one name. Divergent answers are the single most reliable signal that the type is really two types, and finding that out before composition is far cheaper than discovering it through a year of composition conflicts.

When one name is really two concepts A single Customer type serving billing and support forces both contexts to share a definition. Splitting into a billing account and a support contact, linked by a reference, lets each context own its own meaning. Two contexts, one contested name contested type Customer — owned by nobody billing wants payment terms; support wants contact history. every change needs both teams separated BillingAccount SupportContact each context owns its own type and references the other by key. changes are independent again Renaming is usually cheaper than negotiating a shared definition neither side wanted.

The output of this exercise should be one artifact, not a shared feeling, and the artifact is small enough to fit on a page. Four columns per entity are enough to answer nearly every question that would otherwise resurface in a design review six months from now.

What to write down Entity name, owning context, key, and a one-line definition are enough to answer most ownership questions later. The ownership table, in four columns column example why it earns its place entity Order the concept, named once owning context order management who approves a definitional change key id, issued here what other subgraphs reference it by one-line definition a customer's committed purchase settles disputes before they start The last column is the one people skip and the one that resolves arguments — most disputes are definitional. Keep it where a new team will read it before declaring a type, not in an architecture deck.

Common mistakes & gotchas

Assigning ownership by database. It yields a graph shaped like your infrastructure, which is the shape most likely to change and least meaningful to clients.

Keeping one name for two concepts. The resulting type accumulates fields from both contexts and belongs to neither, and every change needs a meeting.

Choosing a natural key from another context. Keying Order on an external reference number makes your entity depend on someone else’s identifier scheme, which they will eventually change.

Frequently Asked Questions

What if two teams genuinely share a concept?

Then one of them owns it and the other contributes fields, which federation supports directly. Genuine sharing is a contributor relationship rather than a co-ownership one, because co-ownership means nobody can decide anything alone. Pick the context whose language defines the concept, and make the other’s additions explicit fields rather than edits to the shared core.

How do we handle a legacy monolith that owns everything?

Treat it as one bounded context to begin with and carve contexts out of it one at a time, exactly as described in how to split a monolith GraphQL schema into subgraphs. Ownership analysis is most useful during that carving, because it tells you which piece to extract next.

Does ownership have to be permanent?

No, and @override exists precisely to move it — see migrating field ownership with the @override directive. But moving a whole entity is much harder than moving a field, because the key travels with it. That asymmetry is why the initial decision deserves real thought and later field-level adjustments do not.

Is one entity per team a good rule?

No, and it is a surprisingly common one. A team may own several entities, and some entities are owned by a team that owns nothing else. Ownership follows concepts, and concepts do not distribute themselves evenly across an org chart.

How long should this analysis take?

An afternoon for a graph of a dozen entities, and it should produce a written artifact rather than a shared understanding. One table — entity, owning context, key, and the sentence defining what it means — answers most of the questions that would otherwise resurface in every design review for the next year. The value is not in the analysis being deep but in the answer being written down somewhere a new team can read it before they declare a type.

What if the analysis says a type belongs to a team that does not want it?

That is useful information rather than a dead end, and it usually means one of two things. Either the concept is less central than the team assumed, in which case ownership can move to whoever cares most about it; or the team is under-resourced for something the business depends on, which is worth surfacing regardless of what the graph does. What does not work is assigning ownership to a willing team that lacks the authority to decide, because every definitional change then routes through someone who cannot make it.