Skip to content

Viaduct Builder

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 (ViaductRestController.kt or ViaductRestController.java).

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 operation text and variables, selects a schema ID, and calls viaduct.execute(...) (Kotlin) or viaduct.executeAsync(...) (Java).

Builder configuration

Both demos build Viaduct with ViaductBuilder, the Micronaut injector factory, and every declared scoped schema. Module discovery uses the generated META-INF/viaduct/modules/ configuration on the classpath.

This excerpt is the demo's configuration:

class StarWarsSchemas(
    val default: SchemaScopeInfo,
    val extras: SchemaScopeInfo,
)
@Factory
class ViaductConfiguration(
    val tenantModuleInjectorFactory: MicronautTenantModuleInjectorFactory,
) {
    @Singleton
    fun providesStarWarsSchemas(): StarWarsSchemas =
        StarWarsSchemas(
            default = SchemaScopeInfo.fromResources(setOf("publicSchema")).single(),
            extras = SchemaScopeInfo.fromResources(setOf("publicSchemaWithExtras")).single(),
        )
    @Bean
    fun providesViaduct(): Viaduct =
        ViaductBuilder()
            .withTenantModuleInjectorFactory(tenantModuleInjectorFactory)
            .withScopedSchemas(SchemaScopeInfo.fromResources())
            .withFlagManager(MAT_RESOLUTION_FLAG_MANAGER)
            .build()
}

View full file on GitHub

@Factory
public final class ViaductConfiguration {
  public record StarWarsSchemas(SchemaScopeInfo defaultSchema, SchemaScopeInfo extras) {}
  private final MicronautTenantModuleInjectorFactory tenantModuleInjectorFactory;
  public ViaductConfiguration(MicronautTenantModuleInjectorFactory tenantModuleInjectorFactory) {
    this.tenantModuleInjectorFactory = tenantModuleInjectorFactory;
  }
  @Singleton
  public StarWarsSchemas providesStarWarsSchemas() {
    return new StarWarsSchemas(
        SchemaScopeInfo.fromResources(Set.of("publicSchema")).get(0),
        SchemaScopeInfo.fromResources(Set.of("publicSchemaWithExtras")).get(0));
  }
  @Bean
  public Viaduct providesViaduct() {
    return new ViaductBuilder()
        .withTenantModuleInjectorFactory(tenantModuleInjectorFactory)
        .withScopedSchemas(SchemaScopeInfo.fromResources())
        .withFlagManager(
            flag ->
                flag == FlagManager.Flags.ENGINE2_ENABLED
                    || FlagManager.Default.INSTANCE.isEnabled(flag))
        .build();
  }
}

View full file on GitHub

  • providesStarWarsSchemas selects the two scoped schemas the controller routes between, publicSchema and publicSchemaWithExtras, from what scopes.yaml declared.
  • withTenantModuleInjectorFactory lets Micronaut construct resolver classes.
  • withScopedSchemas(SchemaScopeInfo.fromResources()) registers every declared scoped schema.
  • The builder creates an immutable runtime that the controller will use to execute requests.

Example: executing requests through the controller

Each route passes its schema ID → the controller builds ExecutionInput → executes:

@Controller
class ViaductRestController(
    private val viaduct: Viaduct,
    private val schemas: StarWarsSchemas,
    private val securityAccessService: SecurityAccessContext
) {
    @Post("/graphql")
    suspend fun graphql(
        @Body request: Map<String, Any>,
        @Header("security-access") securityAccess: String?
    ): HttpResponse<Map<String, Any?>> = execute(request, schemas.default.schemaId, securityAccess)
    @Post("/extras/graphql")
    suspend fun extrasGraphql(
        @Body request: Map<String, Any>,
        @Header("security-access") securityAccess: String?
    ): HttpResponse<Map<String, Any?>> = execute(request, schemas.extras.schemaId, securityAccess)
    private suspend fun execute(
        request: Map<String, Any>,
        schemaId: SchemaId,
        securityAccess: String?
    ): HttpResponse<Map<String, Any?>> {
        val query = request[QUERY_FIELD] as? String
            ?: return HttpResponse.badRequest(
                mapOf("errors" to listOf(mapOf("message" to "Missing 'query' field")))
            )
        securityAccessService.setSecurityAccess(securityAccess)
        val executionInput = createExecutionInput(query, request)
        val result = viaduct.execute(executionInput, schemaId)
        return HttpResponse.ok(result.toSpecification())
    }

View full file on GitHub

@Controller
public final class ViaductRestController {
  private final Viaduct viaduct;
  private final ViaductConfiguration.StarWarsSchemas schemas;
  public ViaductRestController(Viaduct viaduct, ViaductConfiguration.StarWarsSchemas schemas) {
    this.viaduct = viaduct;
    this.schemas = schemas;
  }
  @Post("/graphql")
  public CompletableFuture<HttpResponse<Map<String, Object>>> graphql(
      @Body Map<String, Object> request,
      @Header("security-access") @Nullable String securityAccess) {
    return execute(request, schemas.defaultSchema().getSchemaId(), securityAccess);
  }
  @Post("/extras/graphql")
  public CompletableFuture<HttpResponse<Map<String, Object>>> extrasGraphql(
      @Body Map<String, Object> request,
      @Header("security-access") @Nullable String securityAccess) {
    return execute(request, schemas.extras().getSchemaId(), securityAccess);
  }
  private CompletableFuture<HttpResponse<Map<String, Object>>> execute(
      Map<String, Object> request, SchemaId schemaId, @Nullable String securityAccess) {
    Object queryValue = request.get("query");
    if (!(queryValue instanceof String query)) {
      return CompletableFuture.completedFuture(
          HttpResponse.badRequest(
              Map.of("errors", List.of(Map.of("message", "Missing 'query' field")))));
    }
    ExecutionInput executionInput =
        ExecutionInput.builder()
            .operationText(query)
            .variables(readVariables(request))
            .requestContext(
                Map.of(
                    SecurityAccessContext.REQUEST_CONTEXT_KEY,
                    new SecurityAccessContext(securityAccess)))
            .build();
    return viaduct
        .executeAsync(executionInput, schemaId)
        .thenApply(result -> HttpResponse.ok(result.toSpecification()));
  }

View full file on GitHub

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

Builder best practices

  • Declare schema IDs and their scope sets in scopes.yaml, and read them with SchemaScopeInfo.fromResources instead of restating them in code.
  • Keep the settings plugin’s module package configuration aligned with resolver packages (com.example.starwars...).
  • Avoid conditional logic in the builder; serve each scoped schema on its own route in the controller instead.

For the complete ViaductBuilder and ExecutionInput API reference, see the Viaduct API developer reference.