# Who Answers \_\_typename? Polymorphism Across Subgraph Boundaries

[Who Answers \_\_typename? Polymorphism Across Subgraph Boundaries](https://iits-consulting.de/en/blog/who-answers-typename-polymorphism-across-subgraph-boundaries)

Kenneth Wußmann

2026-09-25T16:33:34.960Z

When using GraphQL Federation to separate concerns, resolving the \_\_typename of interface implementations can be tricky. Let's see how @interfaceObject solves this problem.

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](https://github.com/KennethWussmann/blog-post-who-answers-typename).

## 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:

```graphql
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:

Single Server Resolving Typename

## 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:

```graphql
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:

```graphql
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:

```graphql
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.

```graphql
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:

Resolving type names via catalog

## 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:

```graphql
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:

```graphql
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 `.

Attaching Fields to Federated Entity

` @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.

