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:
fun sendWelcomeEmail(userService: UserService, emailService: EmailService): Boolean {
// ...
}
val result = container.call(::sendWelcomeEmail)Works with instance methods too:
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:
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:
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:
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
val logger = container.resolveOrNull<Logger>() // null if unresolvable
val hasLogger = container.has<Logger>() // true if resolvableNote: 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:
val logger by container.lazy<Logger>() // resolves on first useWhen 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:
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:
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:
// 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)
}