Skip to content

Global IDs

Viaduct's identity model assumes that every reference between objects flows through a Global ID. Fields don't carry raw foreign keys — they carry typed Global IDs, and any object that needs to be fetched on its own implements the Node interface and is retrieved via the node(id:) entry point. Adopting this model is what unlocks Viaduct's batching, type-safe references, and storage-independent client contracts.

A Global ID combines two pieces of information:

  • Type: the GraphQL type name (for example, "Character", "Film", "Planet").
  • Internal ID: your application's internal identifier for that entity.

Format and encoding

The raw form is "<Type>:<InternalID>", which is then base64-encoded.

// Encoded form for Character with internal ID "1":
val gid: String = Character.Reflection.globalId("1") // "Q2hhcmFjdGVyOjE="

When building objects in resolvers, use the execution context helper to attach a typed Global ID:

GRTStarship.of(ctx) {
    id(ctx.globalIDFor<GRTStarship>(starship.id))
    name(starship.name)
    model(starship.model)
    starshipClass(starship.starshipClass)
    manufacturers(starship.manufacturers)
    costInCredits(starship.costInCredits?.toDouble())
    length(starship.length?.toDouble())
    crew(starship.crew)
    passengers(starship.passengers)
    maxAtmospheringSpeed(starship.maxAtmospheringSpeed)
    hyperdriveRating(starship.hyperdriveRating?.toDouble())
    MGLT(starship.mglt)
    cargoCapacity(starship.cargoCapacity?.toDouble())
    consumables(starship.consumables)
    created(starship.created.toString())
    edited(starship.edited.toString())
}

View full file on GitHub

Treat Global IDs as opaque at the network boundary. Clients pass them around — in node(id:) queries, in input arguments, in cached responses — but should not parse them. Inside resolvers you can decode them through ctx.id or GlobalID.toInternalID() to recover the type and internal ID; just don't ask clients to do the same.

Using Global IDs in node resolvers

Node resolvers receive a decoded Global ID; use the internal ID to load the entity:

@Resolver
class CharacterNodeResolver
    @Inject
    constructor(
        private val characterRepository: CharacterRepository
    ) : NodeResolvers.Character() {
        // Node resolvers can also be batched to optimize multiple requests
        // tag::node_batch_resolver_example[21] Example of a node resolver
        override suspend fun batchResolve(contexts: List<Context>): List<FieldValue<Character>> {
            // Extract all unique character IDs from the contexts
            val characterIds = contexts.map { it.id.internalID }

            // Perform a single batch query to get film counts for all characters
            // We only compute one time for each character, despite multiple requests
            val characters = characterIds.mapNotNull {
                characterRepository.findById(it)
            }

            // For each context gets the character ID and map to the viaduct object
            return contexts.map { ctx ->
                val characterId = ctx.id.internalID
                characters.firstOrNull { it.id == characterId }?.let {
                    FieldValue.ofValue(
                        CharacterBuilder(ctx).build(it)
                    )
                } ?: FieldValue.ofError(IllegalArgumentException("Character not found: $characterId"))
            }
        }
    }

View full file on GitHub

Client usage via node(id:)

Clients pass a Global ID to retrieve a specific entity, independent of the underlying storage key format:

query ($id: ID!) {
  node(id: $id) {
    ... on Character {
      id
      name
    }
  }
}

Schema hinting with @idOf

Annotate ID fields and arguments with @idOf to bind them to a concrete GraphQL type, enabling type-safe handling in resolvers and tooling:

"""
Search by character ID
"""
byId: ID @idOf(type: "Character")

View full file on GitHub

type Character implements Node @scope(to: ["default"]) @resolver(isBatching: true) {
  """
  The GlobalID of this character - uniquely identifies this Character in the graph (internal use only)
  """
  id: ID!

View full file on GitHub

Do and don’t

  • Do treat Global IDs as opaque and stable across the API surface.
  • Do generate them in resolvers using ctx.globalIDFor or <Type>.Reflection.globalId(...).
  • Do use @idOf on schema fields/arguments carrying Global IDs.
  • Don’t expose internal IDs at the network boundary or ask clients to decode Global IDs. Encoding and decoding happen inside Viaduct on both ends; clients treat them as opaque tokens.
  • Don’t embed business logic or access control information in IDs.

See Best Practices for the consolidated reference. For the encoding format, how to generate and consume GlobalID values in resolvers, and schema hints with @idOf, see the Global IDs developer reference.