Overview
The Micronaut Starter demonstrates how to integrate Viaduct with Micronaut’s powerful dependency injection (DI) framework. This tutorial is essential for building production-ready applications where resolvers need access to services, repositories, and other dependencies.What You’ll Learn
- Integrating Viaduct with Micronaut’s DI container
- Using dependency injection in GraphQL resolvers
- Separating development and production code with Gradle source sets
- Fast development mode with ViaductServer
- Production-ready configuration without the development server
Prerequisites
- Java JDK 21 installed
JAVA_HOMEenvironment variable set correctly orjavain your PATH- Understanding of dependency injection concepts
Project Structure
Key Concepts
Production vs Development
Production Build:- Includes only
src/main/kotlin(production code) - Does NOT include
viaduct-servedependency - Suitable for deployment with a full HTTP server
- Includes both
src/main/kotlinandsrc/dev/kotlin - Includes
viaduct-servedependency for GraphiQL - Fast iteration with automatic schema reloading
package com.example.viadapp.production
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import viaduct.service.BasicViaductFactory
import viaduct.service.TenantRegistrationInfo
import viaduct.service.api.Viaduct
@Factory
class ViaductConfiguration(
private val micronautTenantCodeInjector: MicronautTenantCodeInjector
) {
@Bean
fun providesViaduct(): Viaduct {
return BasicViaductFactory.create(
tenantRegistrationInfo = TenantRegistrationInfo(
tenantPackagePrefix = "com.example.viadapp",
tenantCodeInjector = micronautTenantCodeInjector
)
)
}
}
@Factory marks this class as a bean factory@Bean method provides the Viaduct instancetenantCodeInjector enables DI for resolver classesMicronautTenantCodeInjector@Singleton
class MicronautTenantCodeInjector(
private val beanContext: BeanContext
) : TenantCodeInjector {
override fun <T : Any> getInstance(clazz: KClass<T>): T {
return beanContext.getBean(clazz.java)
}
}
import jakarta.inject.Singleton
@Singleton
class GreetingService {
fun getGreeting(): String = "Hello from Micronaut!"
}
@Resolver
@Singleton
class GreetingResolver(
private val greetingService: GreetingService // Injected dependency
) : QueryResolvers.Greeting() {
override suspend fun resolve(ctx: Context): String {
return greetingService.getGreeting()
}
}
@Singletonimport viaduct.serve.ViaductServerConfiguration
import io.micronaut.context.ApplicationContext
@ViaductServerConfiguration
class MicronautViaductFactory : ViaductFactory {
override fun create(): Viaduct {
// Start only the Micronaut DI container (not the full HTTP server)
val context = ApplicationContext.run()
// Get the Viaduct instance from DI
return context.getBean(Viaduct::class.java)
}
}
@ViaductServerConfigurationApplicationContext (DI only, no HTTP server)Viaduct bean from the containerhttp://localhost:8080sourceSets {
// Development-only source set
create("dev") {
kotlin.srcDir("src/dev/kotlin")
compileClasspath += sourceSets.main.get().output
runtimeClasspath += sourceSets.main.get().output
}
}
// Dev configuration extends from main
val devImplementation by configurations.getting {
extendsFrom(configurations.implementation.get())
}
dependencies {
// Production dependencies
implementation("io.micronaut:micronaut-inject")
implementation("io.micronaut:micronaut-context")
// Development-only dependency
devImplementation("com.airbnb.viaduct:viaduct-serve")
}
// Serve task includes dev classes
tasks.named<JavaExec>("serve") {
classpath += sourceSets["dev"].output
classpath += sourceSets["dev"].runtimeClasspath
}
dev source set is separate from maindevImplementation dependencies are excluded from production buildsserve task explicitly includes dev classes and dependencieshttp://localhost:8080/graphiqlHow Dependency Injection Works
Resolver Instantiation Flow
- Query Execution: Viaduct needs a resolver instance
- DI Request: Calls
tenantCodeInjector.getInstance(ResolverClass::class) - Micronaut Resolution:
MicronautTenantCodeInjectorasksBeanContextfor the bean - Dependency Injection: Micronaut creates the resolver and injects its dependencies
- Caching: The instance is cached by Micronaut’s singleton scope
Injecting Complex Dependencies
You can inject any Micronaut bean into resolvers:Development Workflow
Fast Iteration Cycle
- Start development server:
./gradlew --continuous serve - Modify resolvers or schema
- Gradle auto-recompiles
- Refresh GraphiQL to test changes
Production Deployment
- Build production JAR:
./gradlew build - Deploy to your server
- Run with Micronaut HTTP server:
Why Use Micronaut with Viaduct?
- Dependency Injection: Resolvers can access databases, services, and external APIs
- Compile-Time DI: Micronaut performs DI at compile time (no reflection)
- Fast Startup: Minimal overhead compared to Spring
- Cloud-Native: Built for microservices and serverless
- Clean Separation: Development tools don’t bloat production artifacts
Next Steps
- Review the Star Wars Tutorial which also uses Micronaut DI
- Learn about Custom Context for request-scoped data
- Study Batch Resolvers with injected dependencies
- Read about Testing with Micronaut test support