Viaduct and ViaductBuilder

How the Viaduct runtime is constructed in the Star Wars demo using ViaductBuilder.

This page explains how the Viaduct runtime is built in the Star Wars demo, referencing the configuration code (for example, ViaductConfiguration.kt) and the controller that executes requests (ViaductGraphQLController.kt).

Goal: make it clear what the builder registers, how schemas are defined, and what the runtime looks like when it receives an ExecutionInput to resolve queries and mutations.

High-level flow

  1. Schema registration (IDs, SDL discovery, and scope sets).
  2. Module registration (generated types, resolvers, and package conventions).
  3. Runtime construction via ViaductBuilder.
  4. Execution: the controller creates an ExecutionInput (with schemaId, query, variables, etc.) and calls viaduct.executeAsync(...).

Builder configuration

This excerpt mirrors what happens in configuration (names and constants from the demo):

@Configuration
@Import(ViaductResolverRegistrar::class)
class ViaductConfiguration(
 private val codeInjector: SpringTenantCodeInjector
) {
 @Bean
 fun viaduct(): Viaduct {
     /**
      * The BasicViaductFactory is a utility to create a Viaduct instance with minimal configuration.
      */
     return BasicViaductFactory.create(
         /**
          * StarWars application defines two scoped schemas:
          *
          * 1. PUBLIC_SCHEMA: the base schema with only the default scope
          * 2. PUBLIC_SCHEMA_WITH_EXTRAS: the base schema with the extras header
          */
         // tag::schema_registration[8] Schema registration
         schemaRegistrationInfo = SchemaRegistrationInfo(
             scopes = listOf(
                 DEFAULT_SCHEMA_ID.toSchemaScopeInfo(),
                 EXTRAS_SCHEMA_ID.toSchemaScopeInfo(),
             ),
             packagePrefix = "com.example.starwars", // Scan the entire com.example.starwars package for graphqls resources
             resourcesIncluded = ".*\\.graphqls"
         ),
         /**
          * Tenant registration info is required to let Viaduct discover tenant-specific code such as resolvers.
          *
          * In this configuration, we specify to scan for tenant code in the com.example.starwars package.
          */
         tenantRegistrationInfo = TenantRegistrationInfo(
             tenantPackagePrefix = "com.example.starwars", // Scan the entire com.example.starwars package for tenant-specific code
             tenantCodeInjector = codeInjector
         )
     )
 }

  • PUBLIC_SCHEMA and PUBLIC_SCHEMA_WITH_EXTRAS are schema IDs used by the demo.
  • packagePrefix and resourcesIncluded tells Viaduct where to discover SDL and generated types.
  • The builder creates an immutable runtime that the controller will use to execute requests.

Example: executing requests through the controller

The controller resolves scopes → chooses a schema → builds ExecutionInput → executes:

@RestController
class ViaductRestController {
@Autowired
lateinit var viaduct: Viaduct

@PostMapping("/graphql")
suspend fun graphql(
    @RequestBody request: Map<String, Any>,
    @RequestHeader headers: HttpHeaders
): ResponseEntity<Map<String, Any>> {
    val executionInput = createExecutionInput(request)
    // tag::run_query[7] Runs the query example
    val scopes = parseScopes(headers)
    val schemaId = determineSchemaId(scopes)
    val result = viaduct.executeAsync(executionInput, schemaId).await()
    return ResponseEntity.status(statusCode(result)).body(result.toSpecification())
}

For details on determineSchemaId(scopes) and createExecutionInput(...), see the Scope and Schemas documentation in this set.

Builder best practices

  • Declare schema IDs and their scope sets explicitly.
  • Keep packagePrefix aligned with generated code (com.example.starwars...).
  • Configure directives and modules in the builder when applicable.
  • Avoid conditional logic in the builder; route by scope in the controller instead.