Skip to main content

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_HOME environment variable set correctly or java in your PATH
  • Understanding of dependency injection concepts

Project Structure

micronaut-starter/
├── src/
│   ├── main/kotlin/com/example/viadapp/production/
│   │   ├── ViaductConfiguration.kt         # Viaduct bean factory
│   │   └── MicronautTenantCodeInjector.kt  # DI bridge for resolvers
│   └── dev/kotlin/com/example/viadapp/dev/
│       └── MicronautViaductFactory.kt      # ViaductServer integration (dev only)
└── viadapp/                                 # Resolver module
    └── src/main/kotlin/com/example/viadapp/
        ├── HelloWorldResolver.kt
        └── HelloWorldTenantModule.kt

Key Concepts

Production vs Development

Production Build:
  • Includes only src/main/kotlin (production code)
  • Does NOT include viaduct-serve dependency
  • Suitable for deployment with a full HTTP server
Development Build:
  • Includes both src/main/kotlin and src/dev/kotlin
  • Includes viaduct-serve dependency for GraphiQL
  • Fast iteration with automatic schema reloading
1
Configure the Viaduct Bean Factory
2
The ViaductConfiguration.kt:22 creates a Viaduct instance as a Micronaut bean:
3
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
            )
        )
    }
}
4
Key Points:
5
  • @Factory marks this class as a bean factory
  • @Bean method provides the Viaduct instance
  • tenantCodeInjector enables DI for resolver classes
  • Micronaut automatically injects MicronautTenantCodeInjector
  • 6
    Implement the Tenant Code Injector
    7
    The MicronautTenantCodeInjector bridges Viaduct and Micronaut’s DI:
    8
    @Singleton
    class MicronautTenantCodeInjector(
        private val beanContext: BeanContext
    ) : TenantCodeInjector {
        override fun <T : Any> getInstance(clazz: KClass<T>): T {
            return beanContext.getBean(clazz.java)
        }
    }
    
    9
    This allows Viaduct to request instances from Micronaut’s container when creating resolvers.
    10
    Create Resolvers with Dependencies
    11
    Resolvers can now use constructor injection:
    12
    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()
        }
    }
    
    13
    Key Points:
    14
  • Both resolver and service are marked @Singleton
  • Dependencies are injected via constructor parameters
  • Micronaut manages the lifecycle of all beans
  • 15
    Set Up Development Mode
    16
    The MicronautViaductFactory.kt in src/dev/kotlin/ enables fast development:
    17
    import 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)
        }
    }
    
    18
    How It Works:
    19
  • ViaductServer discovers this provider via @ViaductServerConfiguration
  • Starts a minimal ApplicationContext (DI only, no HTTP server)
  • Retrieves the Viaduct bean from the container
  • Serves GraphQL requests with GraphiQL at http://localhost:8080
  • 20
    Configure Gradle Source Sets
    21
    The build.gradle.kts separates development and production code:
    22
    sourceSets {
        // 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
    }
    
    23
    Key Points:
    24
  • dev source set is separate from main
  • devImplementation dependencies are excluded from production builds
  • serve task explicitly includes dev classes and dependencies
  • 25
    Run in Development Mode
    26
    Start the development server with GraphiQL:
    27
    ./gradlew serve
    
    28
    This:
    29
  • Starts only the Micronaut DI container (not the full HTTP server)
  • Provides GraphiQL IDE at http://localhost:8080/graphiql
  • Faster startup than a full Micronaut HTTP server
  • 30
    Open your browser:
    31
    http://localhost:8080/graphiql
    
    32
    Run a query:
    33
    query {
      greeting
      author
    }
    
    34
    Enable Auto-Reload on Changes
    35
    Use Gradle’s continuous build mode:
    36
    ./gradlew --continuous serve
    
    37
    Now when you modify resolver code, Gradle automatically recompiles and restarts the server.
    38
    Build for Production
    39
    Create a production JAR that excludes development code:
    40
    ./gradlew build
    
    41
    The resulting artifact:
    42
  • Contains only src/main/kotlin code
  • Excludes viaduct-serve dependency
  • Excludes MicronautViaductFactory
  • Ready for deployment with a full Micronaut HTTP server
  • How Dependency Injection Works

    Resolver Instantiation Flow

    1. Query Execution: Viaduct needs a resolver instance
    2. DI Request: Calls tenantCodeInjector.getInstance(ResolverClass::class)
    3. Micronaut Resolution: MicronautTenantCodeInjector asks BeanContext for the bean
    4. Dependency Injection: Micronaut creates the resolver and injects its dependencies
    5. Caching: The instance is cached by Micronaut’s singleton scope

    Injecting Complex Dependencies

    You can inject any Micronaut bean into resolvers:
    @Singleton
    class UserRepository {
        suspend fun findById(id: String): User? { /* ... */ }
    }
    
    @Singleton
    class AuthService {
        fun getCurrentUserId(): String { /* ... */ }
    }
    
    @Resolver
    @Singleton
    class CurrentUserResolver(
        private val userRepository: UserRepository,
        private val authService: AuthService
    ) : QueryResolvers.CurrentUser() {
        override suspend fun resolve(ctx: Context): User? {
            val userId = authService.getCurrentUserId()
            return userRepository.findById(userId)
        }
    }
    

    Development Workflow

    Fast Iteration Cycle

    1. Start development server: ./gradlew --continuous serve
    2. Modify resolvers or schema
    3. Gradle auto-recompiles
    4. Refresh GraphiQL to test changes
    No manual restarts needed!

    Production Deployment

    1. Build production JAR: ./gradlew build
    2. Deploy to your server
    3. Run with Micronaut HTTP server:
    fun main(args: Array<String>) {
        Micronaut.run(Application::class.java, *args)
    }
    
    The Viaduct bean is available for injection into HTTP controllers.

    Why Use Micronaut with Viaduct?

    1. Dependency Injection: Resolvers can access databases, services, and external APIs
    2. Compile-Time DI: Micronaut performs DI at compile time (no reflection)
    3. Fast Startup: Minimal overhead compared to Spring
    4. Cloud-Native: Built for microservices and serverless
    5. Clean Separation: Development tools don’t bloat production artifacts

    Next Steps

    Build docs developers (and LLMs) love