Skip to content

Extending types across subgraphs

One of the main benefits of Fusion is the ability to compose data that lives in different services into a single GraphQL graph. Instead of duplicating data or creating direct service dependencies, a service can contribute additional fields to an entity defined in another subgraph.

Why extend types?

In a microservices architecture, a business entity often spans multiple bounded contexts. For example, a LandingRequest might be defined in one service, while tracking information about it lives in a completely separate Tracking service.

Without type extensions, you'd have two options — both problematic:

  • Duplicate the data: the Tracking service re-exposes LandingRequest fields that belong to a different service.
  • Create a direct dependency: one service calls the other internally, coupling them together.

Type extensions solve this by letting the Tracking service contribute fields to LandingRequest without redefining it. Fusion handles the composition at the gateway level, presenting a unified type to clients.

How it works

A service that wants to contribute fields to an entity defined elsewhere needs two things:

  1. An [ObjectType] that holds only the minimum information needed to identify the entity, plus the new fields being contributed.
  2. A [Lookup] resolver that tells Fusion how to create a minimal reference to the entity so it can delegate resolution of those fields to this service.

Example: Tracking service extending LandingRequest

C#
[ObjectType]
public record LandingRequest(Guid CorrelationId)
{
    [UseSuiteSingleOrDefault]
    public Task<IQueryable<Domain.Tracking>> Tracking(
        [Parent] LandingRequest landingRequest,
        IQueryableFactory factory)
    {
        return factory.Execute(
            new CorrelatedEntityByIdFilterSpecification<Domain.Tracking>(
                landingRequest.CorrelationId));
    }
}

public class LandingRequestResolvers : IQuery
{
    [Lookup]
    [Internal]
    public LandingRequest GetLandingRequestById(Guid correlationId)
        => new LandingRequest(correlationId);
}

A few things to note:

  • The [ObjectType] record contains the minimum information required to identify the entity (CorrelationId in this example) as well as any fields contributed by this subgraph. It should not duplicate fields owned by other subgraphs, as doing so introduces redundancy and may lead to composition conflicts.
  • The [Parent] parameter in Tracking(...) receives the resolved LandingRequest instance, making its identity properties available to fetch related data.
  • The [Lookup] resolver creates a minimal LandingRequest reference — it doesn't load real data, it just gives Fusion the identity it needs to delegate resolution of tracking to this service.
  • The [Internal] attribute ensures the lookup is not exposed through the composed gateway schema. It remains available within the subgraph and is used by Fusion for entity resolution and composition.

During schema composition, Fusion merges both definitions into a single LandingRequest type exposed by the gateway.

Auto-discovery with [ObjectType]

Any type decorated with [ObjectType] is automatically discovered by the Suite GraphQL infrastructure. This means types contributing fields to an existing entity do not need to be manually registered in the service's GraphQL configuration.

C#
1
2
3
4
5
6
// No manual registration needed — [ObjectType] handles discovery automatically
[ObjectType]
public record LandingRequest(Guid CorrelationId)
{
    // contributed fields...
}

This keeps the extending service's configuration clean and avoids boilerplate.

How resolution works at runtime

When a client queries data that spans multiple subgraphs:

GraphQL
1
2
3
4
5
6
7
{
  landingRequest(correlationId: "...") {
    tracking {
      status
    }
  }
}

Fusion uses the [Lookup] resolver in the Tracking service to create a minimal LandingRequest reference, then delegates resolution of the tracking field to that service. From the client's perspective, the data looks like a single unified object — even though it was resolved by two different services.

Testing type extensions locally

It's worth being precise about what [Internal] actually does, because it often causes confusion. The attribute affects schema composition, not the subgraph itself: when Fusion composes the gateway's public schema, schema elements marked [Internal] are excluded so clients can't call them directly. They remain part of the subgraph and are still fully accessible and executable from that service's own playground.

Because [Internal] only affects the composed schema, local testing of type extensions remains straightforward. You don't need to comment out [Internal] or change anything—you just query the lookup directly against the extending service.

Steps

  1. Run the extending service locally (in this example, the Tracking service).
  2. Open the playground at /graphql.
  3. Call the [Lookup] resolver directly, passing the correlationId of an existing entity. Even though the lookup is [Internal], it's reachable here because you're hitting the subgraph and not the composed gateway schema.
  4. Request the contributed fields alongside the identity fields, and confirm they resolve with the expected data.

Example

GraphQL
query {
  landingRequestById(correlationId: "...") {
    correlationId
    tracking {
      code
      displayName
      correlationId
    }
  }
}

This query goes straight to the Tracking service. If tracking resolves with the expected values, the extension is wired up correctly and will compose properly at the gateway.

When to use type extensions

Type extensions are the right tool when:

  • A business entity spans multiple bounded contexts.
  • Additional information belongs to a different service than the one that defined the entity.
  • Multiple services need to contribute fields to the same GraphQL type.
  • You want to expose a unified API without introducing direct service-to-service dependencies.