koin icon indicating copy to clipboard operation
koin copied to clipboard

Question: Why is 'stacked parameters' behavior not documented?

Open omarsahl opened this issue 4 months ago • 2 comments

Considering the following code:

interface Greeter {
    fun greet()
}

class GreeterA(private val delegate: Greeter) : Greeter by delegate

class GreeterB(private val config: Config) : Greeter {
    override fun greet() = println("Hello, ${config.name}!")
}

class Config(val name: String)

fun main() {
    val koinApp = startKoin {
        printLogger(Level.DEBUG)
        modules(
            module {
                factoryOf(::GreeterA)
                factoryOf(::GreeterB) bind Greeter::class
            }
        )
    }

    val a = koinApp.koin.get<GreeterA>(
        parameters = { parametersOf(Config("Koin")) }
    )
    a.greet() // prints "Hello, Koin!"
}

If someone relied only on the documentation, they might expect this code not to work, but it actually does work, and prints the expected result.

From my (non-expert) understanding of Koin internals, this works because when Koin resolves GreeterA and needs an instance of GreeterB (which is bound to the Greeter interface), it:

  • First checks for parameters explicitly passed to GreeterB factory via the parameters lambda.
  • If none are found, it falls back to the current stacked parameters (I believe that's how it's called?), in this case, the parameters passed to GreeterA.
  • Finds the Config parameter there and uses it to resolve GreeterB.

This is a really cool behavior, but I couldn’t find any mention of it in the documentation.

So my question is: Is this considered an internal implementation detail (and therefore undocumented because maybe it’s not an encouraged usage), or is there another reason?

Thank you!

omarsahl avatar Aug 11 '25 10:08 omarsahl

This sounds really dangerous and unintended

Nek-12 avatar Sep 05 '25 05:09 Nek-12

I agree. The behaviour isn't well documented and has a confusing choice of ordering

    private fun <T> resolveFromContextOrNull(scope : Scope, instanceContext: ResolutionContext, lookupParent : Boolean = true): T? {
        return resolveFromInjectedParameters(instanceContext)
            ?: resolveFromRegistry(scope,instanceContext)
            ?: resolveFromStackedParameters(scope,instanceContext)
            ?: resolveFromScopeSource(scope,instanceContext)
            ?: resolveFromScopeArchetype(scope,instanceContext)
            ?: if (lookupParent) resolveFromParentScopes(scope,instanceContext) else null
            ?: resolveInExtensions(scope,instanceContext)
    }

I would expect Stacked Parameters (passed as parametersOf()) to be used before Registry is used on other dependencies. But I guess you then need to be more explicit when passing param down the dependency graph.

kassim avatar Sep 05 '25 15:09 kassim