---
title: 'Dependency Injection'
source: 'https://academia.sh/en/courses/architecture-components-and-data-layer/dependency-injection'
course: 'Architecture Components and the Data Layer'
language: en
updated: '2026-10-21T05:02:36+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Explains constructor injection, the container, the module and the scope'
    - 'Measures that in the production chain built by the container, with memory and disk empty, the answer comes from the network'
    - 'Shows that the same consumer reads different values from the same origin in different scopes'
    - 'Classifies that when fake data is injected into the memory tier in a test, the same read comes from memory'
---

# Dependency Injection

In dependency injection, a class does not build the objects it needs itself but receives them from outside. The lesson measures which tier answers a read in the production chain built by a container, and how the answer changes when the chain changes in a test.

In the previous lessons we built the tiers of the chain directly. In a real app a structure takes over this job: dependency injection. A class does not create the objects it needs; it receives them from outside, in its constructor. The structure that builds and hands out the objects is called a container. The question is again the same: **where does the answer to this read come from?**

The measuring tool is the core of the previous lessons; we repeat it unchanged.

```kotlin
// Origin.kt — classifies which tier a read's answer came from
class Tier(val origin: String, private val entries: Map<String, String>) {
    init {
        require(origin in listOf("memory", "disk", "network")) { "invalid tier: $origin" }
    }

    fun lookup(key: String): String? = entries[key]
}

class Read(val label: String, val key: String, chain: List<Tier>, fallback: String) {
    private val hit: Tier? = chain.firstOrNull { it.lookup(key) != null }
    val value: String = hit?.lookup(key) ?: fallback
    val origin: String = hit?.origin ?: "default"

    fun show() {
        println("$label: key=$key | value=$value -> $origin")
    }
}

fun countOrigins(reads: List<Read>) {
    for (name in listOf("memory", "disk", "network", "default")) {
        println("$name: ${reads.count { it.origin == name }}")
    }
}
```

## The Chain Handed In Through the Constructor

In constructor injection, a class asks for its dependency as a parameter. A module describes which object is built and how; a scope determines how long that object is shared. Injection libraries process this description at build time or at run time; we show only the constructor parameter and the result of the description.

```kotlin
// Platform code: CartRepository and module
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Inject
import javax.inject.Singleton

class CartRepository @Inject constructor(
    private val remote: CartApi,
    private val local: CartDao,
)

@Module
@InstallIn(SingletonComponent::class)
object DataModule {
    @Provides
    @Singleton
    fun provideCartApi(): CartApi = CartApi.create()
}
```

The consumer does not build the `CartApi` and `CartDao` objects itself; the module decides which one arrives. This piece is platform code, and here we make no claim about output, timing or file paths. The model builds this rule of the platform; the model is not the platform itself.

Two questions from earlier courses need to be kept apart here. M12/K02 asks where a change reaches, and M12/K03 asks who can change a result. This lesson describes who builds which object, but that is not what we measure: the category is not the owner that supplies the object but the tier that answers the read. Even though the container built the chain, what we look at is not the builder; it is which tier the answer came from.

In the model the container is a chain. `Container.read` hands the chain to a read. `preload` loads the requested keys from the network response into memory ahead of time. `productionContainer` builds the production chain: it builds memory from the return value of `preload`, leaves disk empty and fills the network. `testContainer` puts fake data in the memory tier. `CartScreenModel` is the consumer and receives the container in its constructor; it does not know which chain arrived.

```kotlin
// Container.kt — the container builds the chain, the consumer receives it in its constructor
class Container(private val chain: List<Tier>) {
    fun read(label: String, key: String, fallback: String): Read =
        Read(label, key, chain, fallback)
}

fun preload(network: Map<String, String>, keys: List<String>): Map<String, String> =
    keys.filter { it in network }.associateWith { network.getValue(it) }

fun productionContainer(network: Map<String, String>): Container =
    Container(listOf(Tier("memory", preload(network, listOf("campaign.code"))), Tier("disk", mapOf()), Tier("network", network)))

fun testContainer(fake: Map<String, String>, network: Map<String, String>): Container =
    Container(listOf(Tier("memory", fake), Tier("disk", mapOf()), Tier("network", network)))

class CartScreenModel(private val container: Container) {
    fun readAll(): List<Read> = listOf(
        container.read("title", "screen.title", "empty"),
        container.read("product-price", "product.price", "0"),
        container.read("stock", "product.stock", "0"),
        container.read("campaign", "campaign.code", "none"),
    )
}
```

## The Production Chain

In production the container asks to preload only the campaign code into memory and leaves disk empty. The network response carries the title, the price and the stock; there is no campaign code, so `preload` returns an empty map and memory stays empty as well.

```kotlin
// Main.kt — in the production chain the answer comes from the network
fun main() {
    val network = mapOf(
        "screen.title" to "Cart",
        "product.price" to "120",
        "product.stock" to "7",
    )
    val model = CartScreenModel(productionContainer(network))
    val reads = model.readAll()
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
title: key=screen.title | value=Cart -> network
product-price: key=product.price | value=120 -> network
stock: key=product.stock | value=7 -> network
campaign: key=campaign.code | value=none -> default
memory: 0
disk: 0
network: 3
default: 1
```

Three reads came from the network; the campaign fell to the default because it was in no tier. Memory and disk were tiers that did not answer in this chain: because they were empty, the read passed them and reached the network.

**AA1.** The model builds the container as a plain chain and imitates the scope by rebuilding the chain on every call. On the platform the container builds the object graph and keeps singleton objects alive; the chain is only one slice of that graph.

## The Scope Changes, the Origin Does Not

Let the same consumer run in the scope of two users. Each scope is built with its own network response; the consumer class is the same in both.

```kotlin
// Main.kt — two scopes: same consumer, same origin, different value
fun main() {
    val scopes = mapOf(
        "user-a" to mapOf("product.price" to "120"),
        "user-b" to mapOf("product.price" to "95"),
    )
    for ((name, network) in scopes) {
        val model = CartScreenModel(productionContainer(network))
        println("scope: $name")
        model.readAll()[1].show()
    }
}
```

```text
scope: user-a
product-price: key=product.price | value=120 -> network
scope: user-b
product-price: key=product.price | value=95 -> network
```

In the two scopes the price came out as 120 and 95; the origin of both is the network. The consumer stayed the same, the value changed, the origin did not. The scope determines the value; the rule looks only at who gave the answer.

**AA2.** The model builds the scope as a network response map. On the platform a scope is the lifetime boundary of an object, and two consumers in the same scope share the same instance; the model has no object sharing.

## The Bounding Measurement

Testing is the real gain of injection: the chain changes while the consumer does not. In the test, fake price and stock are put in the memory tier. The network response stays the same as in production.

```kotlin
// Main.kt — in the test, fake data is injected into the memory tier
fun main() {
    val network = mapOf(
        "screen.title" to "Cart",
        "product.price" to "120",
        "product.stock" to "7",
    )
    val fake = mapOf("product.price" to "1", "product.stock" to "1")
    val model = CartScreenModel(testContainer(fake, network))
    val reads = model.readAll()
    reads.forEach { it.show() }
    countOrigins(reads)
}
```

```text
title: key=screen.title | value=Cart -> network
product-price: key=product.price | value=1 -> memory
stock: key=product.stock | value=1 -> memory
campaign: key=campaign.code | value=none -> default
memory: 2
disk: 0
network: 1
default: 1
```

Same consumer, same network response; but the price and stock now came from memory and their values became 1. The title is still from the network and the campaign still from the default. Once fake data entered the memory tier, the answer to the read was given from there. The rule classifies by who gave the answer, not by who built the tier.

**AA3.** The model builds fake data as a memory tier. On the platform a fake is a test implementation of an interface; the behavior of the fake is not measured in this lesson.

## Summary

- This lesson's origin: network. In the production chain built by the container, memory and disk were empty and the network was full; the title, price and stock came from the network.
- The lesson's measure is that the network answered after the tiers that did not answer. In production the result was network 3, default 1, memory 0, disk 0.
- In two scopes the same consumer read 120 and 95; both are network. The scope determines the value, not the origin.
- When fake data entered the memory tier in the test, the price and stock came from memory: memory 2, network 1, default 1.
- The category is not the owner that builds the object but the tier that gives the answer.

## Next Step

We have seen the tiers of the chain and the structure that builds them. The next topic of the course, The Repository Pattern, gathers this chain behind a single interface: the screen asks without knowing which tier it reads from, the repository manages the order, and the data layer is read with the same measure.
