Imagine we are building a music streaming provider. Users want to save songs in playlists, obviously. But podcasts are also a giant market with a lot of value, so your company decides to add them to their content catalog. Playlists can now contain songs and podcasts, easy.
You model them behind a GraphQL interface, split the graph across services and everything works. Six months later, another team adds audiobooks. Your graph is evolving fast and GraphQL can adapt to this, if you do it right. We modelled it as an interface, so the playlists can automatically account for audiobooks as well, right? RIGHT?
With GraphQL Federation there is a lot of autonomy on the graph. Not every team should be concerned about changes to the entire graph. That's the whole point you use GraphQL Federation. But suddenly a user puts an audiobook into a playlist and the whole playlist comes back as null. What happened here?
The playlists service knows which items belong in a playlist. Why should it also need to know every kind of media the company might ship? @interfaceObject comes in handy to help solve this problem. Let's find out how one audiobook destroys the entire playlist and how you model this correctly on GraphQL Federation.
The examples have been executed and the described behaviour validated against Cosmo Router v0.346.1 with Federation v2.7 SDL. Find the repo with all examples here.
With one service, __typename is free
We start at the beginning, with one GraphQL server to rule them all, no federation. Let's take a closer look at how a single GraphQL service would model a playlist with a mixed set of items:
interface MediaItem {
id: ID!
title: String!
}
type Song implements MediaItem {
id: ID!
title: String!
artist: String!
}
type Podcast implements MediaItem {
id: ID!
title: String!
showName: String!
}
type Playlist {
id: ID!
name: String!
items: [MediaItem!]!
}The playlist holds items as a generic MediaItem. When the GraphQL server needs to resolve items, it still has to return each object's concrete type because they have different fields. So the playlists subgraph has to know the kind of media it's saving to lists, including all metadata about every item. In GraphQL, the __typename identifies the concrete type of an object. That's especially important when talking about interface implementations. As this is happening locally in one service, the answer is pretty easy. The __typename could be inferred from a business object, DTO or data model. That's done via a generic type resolver or a __typename field on the returned object:

The playlist breaks when the set grows
If we now split this single server into multiple GraphQL servers using GraphQL Federation, answering the __typename question isn't quite as easy anymore. Multiple teams could be responsible for songs, podcasts or audiobooks and the playlists service doesn't want to know every single kind of media.
So the playlists team needs to return a generic [MediaItem!]! but owns none of the implementations. The obvious way to do that is to declare all the types it knows:
interface MediaItem {
id: ID!
title: String!
}
type Song implements MediaItem @key(fields: "id") {
id: ID!
title: String! @external
}
type Podcast implements MediaItem @key(fields: "id") {
id: ID!
title: String! @external
}
type Playlist @key(fields: "id") {
id: ID!
name: String!
items: [MediaItem!]!
}We can now store songs and podcasts in playlists. This isn't much different to how a single server solved this concern, just that the implementations are now federated entities.
When another subgraph adds Audiobook, the playlists graph still composes just fine. But once a user puts an Audiobook into a playlist, everything falls apart. Its local executor rejects the type:
Abstract type "MediaItem" was resolved to a type "Audiobook" that does not exist
inside the schema.Only the composed supergraph on the router knows that Audiobook exists in another subgraph. The playlists schema doesn't have it. Even if its resolver returns __typename: "Audiobook", that is an invalid answer against this subgraph's schema.
The composition can validate that all declared schemas in the registry are valid. Which they are. The playlists subgraph doesn't have to declare Audiobook just because it uses the same MediaItem interface. But if it allows the user to put any MediaItem into a playlist, it might not be able to return the entire list anymore.
A playlist contains items: [MediaItem!]!. So a non-null list of non-nullable items. The new Audiobook that one user put in a playlist now makes the entire playlist come back as null. As the type is not known to the playlists subgraph, it would have to resolve to null, which is against the non-nullable schema.
Changing the items type to [MediaItem]! would at least preserve the known items, with the Audiobook being null alongside an error. It doesn't really fix the issue with the type resolution. The audiobook is stuck in the playlist and the user cannot see it anymore.
The playlists team would have to keep up with the rest of the company to add Audiobook to their schema before a user tries to put one in a playlist. Playlists should store collections of media IDs plus metadata about the playlist itself. It shouldn't matter whether it's a song, podcast or audiobook.
Let playlists return what it actually knows
With @interfaceObject, playlists can return a MediaItem reference without knowing a concrete type. Instead we add a catalog subgraph that knows all implementations of MediaItem and which can help us answer the __typename question.
In this new catalog subgraph we define MediaItem as an entity interface with a @key directive:
interface MediaItem @key(fields: "id") {
id: ID!
title: String!
}
type Song implements MediaItem @key(fields: "id") {
id: ID!
title: String!
artist: String!
}
type Podcast implements MediaItem @key(fields: "id") {
id: ID!
title: String!
}
type Audiobook implements MediaItem @key(fields: "id") {
id: ID!
title: String!
}The catalog subgraph is the one place that needs to declare every implementation, including types which get later extended by other subgraphs.
Every implementation has to support the interface's key. So the ID of a MediaItem must be unique across songs, podcasts and audiobooks.
This isn't just to please the composition. The catalog is asked by the router to resolve the __typename of any MediaItem by its ID.
Due to the catalog now taking on the source of truth which type a MediaItem has, we can make the playlists subgraph truly generic across implementations with the help of the @interfaceObject directive.
type MediaItem @key(fields: "id", resolvable: false) @interfaceObject {
id: ID!
}
type Playlist @key(fields: "id") {
id: ID!
name: String!
items: [MediaItem!]!
}We have to use type, not interface here. The playlists subgraph handles MediaItem locally as a concrete object and returns playlist items containing only an ID.
The playlists subgraph also declares that it cannot resolve MediaItem via resolvable: false. The concrete type is now resolved by the catalog subgraph.
Now the responsibility is much cleaner. Playlists can store arbitrary implementations without needing to know their concrete type. The catalog is the source of truth when it comes to answering the __typename question:

Adding fields to an entity-interface
Just like the playlists subgraph, the reviews subgraph shouldn't need to know all implementations. It wants to serve ratings for any MediaItem regardless of its concrete implementation.
Its schema can use the same interface object. This time it can resolve a MediaItem by its ID:
type MediaItem @key(fields: "id") @interfaceObject { # no "resolvable: false"
id: ID!
averageRating: Float
reviewCount: Int!
}Federation automatically adds the new fields to the interface and all its implementations. Reviews only has to implement them once on its type. When a new implementation enters the catalog, the reviews subgraph doesn't need another concrete-type resolver or a deployment just to support the new implementation.
The router is sending an _entities query to the reviews subgraph generically to retrieve the newly added fields:
query($representations: [_Any!]!) {
_entities(representations: $representations) {
... on MediaItem {
averageRating
reviewCount
}
}
}The fragment is on MediaItem, not Song or Audiobook. Reviews never needs to resolve a Song.

@interfaceObject is a good fit for ratings, favourites or view counts that generally apply to all MediaItem implementations.
Takeaways
- A service that stores media IDs shouldn't have to know every media type. Let the entity-interface owner resolve the concrete type.
- The playlists subgraph returns references, the reviews subgraph adds ratings and neither needs to know whether an item is a song or an audiobook.
- The entity-interface owner still has to know every implementation and needs to be able to return its type confidently by its primary ID. New implementations need to be registered here.
Über den Autor
Kenneth (Ken) is a Staff Software Engineer at MOIA with over 9 years of professional experience. His journey began in the Java EE ecosystem, but over time he shifted to TypeScript and to building distributed serverless applications on AWS with GraphQL. From the start, he's been drawn to the way APIs allow systems to connect with each other, and GraphQL Federation makes this a whole lot more interesting. At MOIA, he maintains a graph that spans more than 50 services across multiple teams and powers autonomous mobility in Hamburg. This brings challenges and lessons that he can't stop talking about. He's also fascinated by chess and its parallels to software development, like thinking far ahead and weighing short-term wins against long-term structure.



