Skip to content

Backing Data

The @backingData directive declares a field whose sole purpose is to fetch data once and share it with other resolvers on the same object. Viaduct guarantees the backing resolver runs at most once per parent object, regardless of how many sibling fields consume it.

Use @backingData when two or more field resolvers on the same type need the same upstream data and you want to avoid duplicate calls.

Schema declaration

Apply @backingData on a field typed as BackingData (a Viaduct built-in marker type, never exposed to clients). The class argument points to the Kotlin data class that holds the fetched data:

  """
  Character IDs fetched once from the film-character relationship store.
  Shared by the characters and characterCountSummary resolvers so both
  fields avoid making separate findCharactersByFilmId calls.
  """
  castData: BackingData
    @resolver
    @backingData(class: "com.example.starwars.modules.filmography.films.models.FilmCastData")

View full file on GitHub

The backing data class

A simple data class holding the pre-fetched data. Keep it minimal, shared state only:

data class FilmCastData(val characterIds: List<String>)

View full file on GitHub

The backing data resolver

A resolver that fetches the data once per parent object. It reads the parent's ID from the object value and calls the repository:

@Resolver(objectValueFragment = "fragment _ on Film { id }")
class FilmCastDataResolver
    @Inject
    constructor(
        private val filmCharactersRepository: FilmCharactersRepository
    ) : FilmResolvers.CastData() {
        override suspend fun resolve(ctx: Context): FilmCastData {
            val filmId = ctx.getObjectValue().getId().internalID
            val characterIds = filmCharactersRepository.findCharactersByFilmId(filmId)
            return FilmCastData(characterIds)
        }
    }

View full file on GitHub

Consuming the backing data

Other resolvers declare castData in their objectValueFragment to receive the pre-fetched data. Viaduct automatically ensures the backing resolver completes before these consumers run:

@Resolver(objectValueFragment = "fragment _ on Film { castData }")
class FilmCharactersResolver
    @Inject
    constructor(
        private val characterRepository: CharacterRepository
    ) : FilmResolvers.Characters() {
        override suspend fun resolve(ctx: Context): List<Character?>? {
            val castData = ctx.getObjectValue().get<FilmCastData>("castData", FilmCastData::class)
            return castData.characterIds.map { id ->
                val character = characterRepository.findById(id)
                    ?: throw IllegalArgumentException("Character with ID $id not found")
                CharacterBuilder(ctx).build(character)
            }
        }
    }

View full file on GitHub

Multiple resolvers can share the same backing data, each declares it in their fragment, but the fetch happens only once:

@Resolver(
    """
    fragment _ on Film {
        title
        castData
    }
    """
)
class FilmCharacterCountSummaryResolver
    @Inject
    constructor() : FilmResolvers.CharacterCountSummary() {
        override suspend fun resolve(ctx: Context): String? {
            val film = ctx.getObjectValue()
            val castData = film.get<FilmCastData>("castData", FilmCastData::class)
            return "${film.getTitle()} features ${castData.characterIds.size} main characters"
        }
    }

View full file on GitHub

How it works at runtime

  1. A client queries both characters and characterCountSummary on a Film.
  2. Viaduct sees both resolvers need castData in their objectValueFragment.
  3. FilmCastDataResolver runs once, fetching character IDs from the repository.
  4. The result (FilmCastData) is injected into both consuming resolvers via ctx.getObjectValue().get(...).
  5. Each consumer uses the shared data without any additional repository call.

Design guidelines

  • Use @backingData when two or more sibling resolvers need the same upstream data.
  • Keep the data class minimal — only the shared state needed by consumers.
  • The backing resolver should do I/O; consumers should do transformation only.
  • The field type must be BackingData — this is a Viaduct marker type that is never exposed in the client schema.
  • Field resolvers@backingData fields always also carry @resolver; see field resolvers for the general resolver pattern