Kontainer
A lightweight dependency injection container for Kotlin. Constructor injection, lifecycle management, and validation at startup.
Why another DI container?
Dependency injection solves a real problem: it decouples your code from the concrete implementations it uses, making it testable and composable. But most DI containers are designed for long-lived application scopes. On a server handling hundreds of requests per second, you often need state that lives exactly as long as one request — the current user, collected metrics, an audit trail — and then disappears cleanly.
What is Kontainer?
Kontainer is a dependency injection container that uses a two-stage architecture: you define a blueprint, then create container instances from it. Services are injected via constructors.
val blueprint = kontainer {
singleton(UserRepository::class)
singleton(UserService::class)
dynamic(RequestContext::class)
}
val kontainer = blueprint.create()
val service = kontainer.get(UserService::class) That's it. Define services, create a container, get what you need.
The core idea: stateful, scoped containers
Most DI containers give you a single, long-lived container. Kontainer is different — it's built around the idea that you create a fresh container for every unit of work.
On the server side, every incoming HTTP request, CLI command, or scheduled job gets its own container instance, created from a shared blueprint. Singletons (database pools, configuration) are shared. But dynamic services are fresh per container — they can carry state that lives exactly as long as the request does, then gets garbage collected. Clean slate, every time.
This unlocks powerful patterns:
- Inject the current user everywhere — set it once when the request starts, every service that needs it gets it via constructor injection. No parameter threading.
- Per-request metrics and audit logs — collect database query counts, timings, and log entries throughout a request. Flush at the end. No manual cleanup.
- Request-scoped caching — cache expensive lookups for the duration of one request without worrying about stale data across requests.
Think of it as PHP's "shared nothing" model, but selective — you choose which services are global and which are per-request. This pattern is used extensively in Funktor's Insights module for production observability.
Read more about this in Service Lifecycle.
Why Kontainer?
Kontainer doesn't generate code at compile time or scan the classpath at runtime. Instead, it validates the entire dependency graph on first use and tells you clearly when something doesn't wire up.
- Pure Kotlin DSL — your DI config is code, not annotations or XML.
- Three lifecycles — Singleton (global), Dynamic (per-container), Prototype (per-injection).
- Validated on first use — missing dependencies and inconsistencies are caught with clear messages.
- Constructor injection — services declare what they need in their constructor.
What you get
Service Lifecycles
Singleton, Dynamic, and Prototype — each with clear, predictable behavior.
Constructor Injection
Declare dependencies in your constructor. Kontainer resolves them automatically.
Factory Methods
When constructors aren't enough, use factory lambdas with up to 10 injected parameters.
Lazy & List Injection
Lazy<T>, List<T>, Lookup<T> — inject one, many, or defer.
Modules
Group services into reusable modules. Libraries ship a module, apps include it.
Debug Tools
Code location tracking, container dumps, and debug info for every service.
Quick taste
A service with dependencies, resolved automatically:
class UserRepository {
fun findById(id: String) = "User #$id"
}
class UserService(private val repo: UserRepository) {
fun greet(id: String) = "Hello, ${repo.findById(id)}!"
}
val blueprint = kontainer {
singleton(UserRepository::class)
singleton(UserService::class)
}
val kontainer = blueprint.create()
println(kontainer.get(UserService::class).greet("42"))
// "Hello, User #42!"