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")
The backing data class¶
A simple data class holding the pre-fetched data. Keep it minimal, shared state only:
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)
}
}
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)
}
}
}
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"
}
}
How it works at runtime¶
- A client queries both
charactersandcharacterCountSummaryon a Film. - Viaduct sees both resolvers need
castDatain theirobjectValueFragment. FilmCastDataResolverruns once, fetching character IDs from the repository.- The result (
FilmCastData) is injected into both consuming resolvers viactx.getObjectValue().get(...). - Each consumer uses the shared data without any additional repository call.
Design guidelines¶
- Use
@backingDatawhen 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.
Related¶
- Field resolvers —
@backingDatafields always also carry@resolver; see field resolvers for the general resolver pattern