Service Injection

Kontainer supports constructor injection, factory methods, lazy injection, and injecting multiple implementations of an interface.

Constructor injection

The simplest and most common pattern. Declare dependencies in your constructor:

class Counter {
    private var count = 0
    fun next() = ++count
}

class MyService(val counter: Counter)

val blueprint = kontainer {
    singleton(Counter::class)
    singleton(MyService::class)
}

val kontainer = blueprint.create()
val service = kontainer.get(MyService::class)

println(service.counter.next())  // 1
println(service.counter.next())  // 2

Factory method injection

When a service needs parameters that Kontainer doesn't know about, use a factory lambda. The lambda's parameters are injected; you handle the rest.

class MyService(private val counter: Counter, private val offset: Int) {
    fun next() = counter.next() + offset
}

val blueprint = kontainer {
    singleton(Counter::class)

    // Kontainer injects Counter; you provide the offset
    singleton(MyService::class) { counter: Counter ->
        MyService(counter, 100)
    }
}

Factories support up to 10 injected parameters.

Inject by super type

A service can be injected by any of its supertypes — interface or parent class:

interface CounterInterface {
    fun next(): Int
}

class Counter : CounterInterface {
    private var count = 0
    override fun next() = ++count
}

// MyService depends on the interface, not the concrete class
class MyService(val counter: CounterInterface)

val blueprint = kontainer {
    singleton(Counter::class)
    singleton(MyService::class)
}

If two services implement the same interface, injection becomes ambiguous and Kontainer reports an error. Use List<T> injection instead (see below).

Nullable injection

Mark a dependency as nullable to make it optional. If the service isn't registered, null is injected instead of throwing:

class SomeOptionalService

// Will receive null if SomeOptionalService is not registered
class MyService(val optional: SomeOptionalService?)

val blueprint = kontainer {
    singleton(MyService::class)
    // SomeOptionalService is NOT registered
}

val kontainer = blueprint.create()
println(kontainer.get(MyService::class).optional)  // null

Inject all implementations

Inject all services that implement a given type as a List. This is powerful for building extensible, plugin-style architectures:

interface Repository {
    val name: String
}

class UserRepository : Repository {
    override val name = "users"
}

class OrderRepository : Repository {
    override val name = "orders"
}

// Injects ALL Repository implementations
class Database(val repos: List<Repository>)

val blueprint = kontainer {
    singleton(Database::class)
    singleton(UserRepository::class)
    singleton(OrderRepository::class)
}

val kontainer = blueprint.create()
kontainer.get(Database::class).repos.forEach {
    println(it.name)  // "users", "orders"
}

Lazy injection

Wrap a dependency in Lazy<T> to defer its creation until first use:

class ExpensiveService {
    init { println("ExpensiveService created!") }
    fun doWork() = "done"
}

class MyService(private val lazy: Lazy<ExpensiveService>) {
    fun run() = lazy.value.doWork()
}

val blueprint = kontainer {
    singleton(ExpensiveService::class)
    singleton(MyService::class)
}

val kontainer = blueprint.create()
val service = kontainer.get(MyService::class)
// ExpensiveService is NOT created yet

service.run()
// NOW ExpensiveService is created

For a more idiomatic Kotlin style, use by delegation to unwrap the lazy automatically:

class MyService(lazyService: Lazy<ExpensiveService>) {
    private val service by lazyService

    fun run() = service.doWork()
}

Breaking circular dependencies with Lazy

When two services need each other, make one of them lazy to break the cycle:

class ServiceOne(private val two: ServiceTwo) {
    val name = "one"
    fun greet() = "I know ${two.name}"
}

class ServiceTwo(private val one: Lazy<ServiceOne>) {
    val name = "two"
    fun greet() = "I know ${one.value.name}"
}

val blueprint = kontainer {
    singleton(ServiceOne::class)
    singleton(ServiceTwo::class)
}

val kontainer = blueprint.create()
println(kontainer.get(ServiceOne::class).greet())  // "I know two"
println(kontainer.get(ServiceTwo::class).greet())  // "I know one"

Lookup injection

For large sets of implementations where you only need specific ones at a time, use Lookup<T>. Unlike List<T>, a Lookup creates services on demand:

import io.peekandpoke.ultra.common.Lookup

class Database(val repos: Lookup<Repository>)

val blueprint = kontainer {
    singleton(Database::class)
    dynamic(UserRepository::class)
    dynamic(OrderRepository::class)
}

val kontainer = blueprint.create()
val db = kontainer.get(Database::class)

// Only UserRepository is created — OrderRepository stays untouched
val users = db.repos.get(UserRepository::class)
println(users.name)  // "users"

Injection patterns summary

Pattern Constructor type Behavior
Direct val x: Service Injected immediately
Nullable val x: Service? Null if not registered
List val x: List<Base> All implementations
Lookup val x: Lookup<Base> Lazy, on-demand access
Lazy val x: Lazy<Service> Created on first .value
Lazy List val x: Lazy<List<Base>> Deferred list of all impls