5 of 5

Advanced

Thread safety, callable injection, custom resolvers, and architectural patterns.

Thread safety

The container is safe for concurrent resolution from multiple threads:

  • Singletons are created exactly once. If multiple threads resolve the same singleton concurrently, one thread executes the factory while the others wait.
  • Scoped instances are created once per scope, even under contention.
  • Factories execute independently on each thread with no shared mutable state.
  • Circular dependency detection is per-thread and does not produce false positives under concurrency.

Registration (factory, singleton, scoped, register) should happen during a single-threaded setup phase before concurrent resolution begins.

Callable injection

Invoke any function with its dependencies resolved from the container:

kotlin
fun sendWelcomeEmail(userService: UserService, emailService: EmailService): Boolean {
    // ...
}

val result = container.call(::sendWelcomeEmail)

Works with instance methods too:

kotlin
val controller = OrderController()
container.call(controller::processOrder)

Default values

When the container cannot resolve a parameter — typically a primitive like String or Int — it checks whether the parameter has a Kotlin default value. If it does, the container skips that parameter and lets Kotlin use the default. If it doesn't, resolution fails with UnresolvableDependencyException.

In constructors

Default values let you mix auto-resolved services with configuration that has sensible defaults:

kotlin
class NotificationService(
    val emailClient: EmailClient,       // auto-resolved
    val maxRetries: Int = 3,            // default used
    val from: String = "noreply@app",   // default used
    val logger: Logger,                 // auto-resolved
)

val container = Container()
val service = container.resolve<NotificationService>()
// emailClient and logger resolved from the container
// maxRetries = 3, from = "noreply@app"

In functions

The same behavior applies when using callable injection:

kotlin
fun sendReport(analytics: AnalyticsService, format: String = "pdf"): ByteArray {
    // ...
}

container.call(::sendReport)  // analytics resolved, format = "pdf"

Resolution priority

If a type is registered in the container, the registration always wins — even when the parameter has a default value. This lets you override defaults when needed:

kotlin
class ApiClient(
    val baseUrl: String = "https://api.example.com",
    val httpClient: HttpClient,
)

val container = Container()

// Without registration: baseUrl uses the default
container.resolve<ApiClient>().baseUrl  // "https://api.example.com"

// With registration: container value takes precedence
container.factory<String> { "https://staging.example.com" }
container.resolve<ApiClient>().baseUrl  // "https://staging.example.com"

Convenience extensions

Optional resolution

kotlin
val logger = container.resolveOrNull<Logger>()   // null if unresolvable
val hasLogger = container.has<Logger>()           // true if resolvable

Note: both trigger a full resolution — singleton and factory instances will be created as a side effect on success.

Lazy resolution

Defer resolution until first access:

kotlin
val logger by container.lazy<Logger>()  // resolves on first use

When used with a Scope, do not store the Lazy beyond that scope's lifetime.

Custom auto-resolver

Replace the default reflection-based auto-resolution with your own strategy:

kotlin
class MyAutoResolver : AutoResolver {
    override fun <T : Any> resolve(type: Class<T>, resolver: Resolver): T {
        // your resolution logic
    }
}

val container = Container(MyAutoResolver())

Interface segregation

The container is split into focused interfaces. Use them to restrict what each part of your code can do:

kotlin
interface Registrar   // register(), factory(), singleton(), scoped()
interface Resolver    // resolve()
interface Caller      // call()
interface Container : Registrar, Resolver, Caller  // child()
interface Scope : Container, AutoCloseable          // close()

Give application code only what it needs:

kotlin
// Setup — full access
fun bootstrap(): Container {
    val container = Container()
    container.register(AuthServiceProvider(), PaymentServiceProvider())
    return container
}

// Routes — can only resolve, not register
fun userRoutes(resolver: Resolver) {
    val service = resolver.resolve<UserService>()
}

// Middleware — can only call functions
fun runMiddleware(caller: Caller) {
    caller.call(::authenticate)
}