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
ExecutionInputto resolve queries and mutations.
High-level flow¶
- Schema registration (IDs, SDL discovery, and scope sets).
- Module registration (generated types, resolvers, and package conventions).
- Runtime construction via
ViaductBuilder. - Execution: the controller creates an
ExecutionInputwith operation text and variables, selects a schema ID, and callsviaduct.execute(...)(Kotlin) orviaduct.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()
}
@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();
}
}
providesStarWarsSchemasselects the two scoped schemas the controller routes between,publicSchemaandpublicSchemaWithExtras, from whatscopes.yamldeclared.withTenantModuleInjectorFactorylets 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())
}
@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()));
}
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 withSchemaScopeInfo.fromResourcesinstead 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
ViaductBuilderandExecutionInputAPI reference, see the Viaduct API developer reference.