CODEX
Dependency Injection good practices
Let’s analyze some good patterns that I’ve spotted in some projects.
Before moving on, make sure you read the basics:
Use constructor injection
There are a few types of DI:
🔴 property/setter injection:
👎️ you may forget to inject it
👎 dependencies are mutable
👎 objects are invalid while you don't set their dependencies val canvas = Canvas()
canvas.console = CliConsole()🔴 interface/method injection:
similar to setter injection, but Canvas would inherit from
an interface with a console dependency setter🔴 parameter injection:
👎 clutters method calls
👎 the caller must have the dep. canvas.draw(Point(3, 4), CliConsole())✅ constructor injection:
val canvas = Canvas(CliConsole())I recommend using only constructor injection (in OO) as it makes each component's dependencies evident in tests and implementation. Also, it makes it impossible to build an invalid component — with a missing dependency. Here’s an example of a component in a test class where you can see what it depends on:
val registerClient = RegisterClient(
emailSender = mockk(),
clientRepository = object : ClientRepo() {
fun findById(id: Long) = Client(id)
},
logger = mockk(),
)📝 Use named arguments if the language supports them, as it significantly increases readability.
Alternatively, you can pass functions rather than objects. This is especially relevant in the functional programming (FP) paradigm. In fact, for a full FP approach, you could drop the class and just resort to parameter injection.
class UpgradeUser(
private val userRepository: UserRepository,
private val logger: Logger,
private val getRetries: () -> Int,
) {
operator fun invoke() {
val retries: Int = getRetries()
// …
}
}Protect dependencies
Receiving dependencies in the constructor ensures you're forced to pass them. Making them private ensures no one else will interact with them. Immutable (i.e. final) dependencies guarantee no one will change them later (automatically guaranteeing failure atomicity). Constructor injection and immutability guarantee there’s a single place where you need to validate dependencies and there’s never an invalid object, even if only for a while.
After construction, a service should be immutable; its behavior shouldn’t be changed by calling any of its methods. Object Design Style Guide
In addition, making dependencies non-nullable ensures always a valid state (i.e. prevents null pointer issues) and helps to self-document the code. Languages like Kotlin and TypeScript support it natively but in others, make sure you null-check the dependencies in the constructor. This fail-fast mechanism ensures that invalid components (i.e. with missing dependencies) can’t exist.
Centralize wiring
Wiring is what assembles all the parts of your app. Put it in a single isolated place, usually near the app’s entry point. This promotes self-documenting code because it centrally describes your app’s web of dependencies. What does wiring look like in code? Here’s an example:
// Wiring section of the app;
// it's supposed to be near the app's 'main' function (boot point). It's known as a composition root or a hand-written container.
val app = RecordingsApp(
accessControl = AccessControl(),
recordingRepo = RecordingRepo(
database = Database.connect(
url = System.getenv("DB_URL")
),
logger = MyLogger(
level = Level.WARN,
),
),
recordingUploader = RecordingUploader(
apiBaseUrl = System.getenv("API_URL"),
),
clock = Clock.systemUTC(),
monitor = NewRelicMonitor(),
)In practice, you end up with a centralized place where you create all the dependencies, wire them together into your app, and boot the app itself.
Centralize loading of configuration values
Never load environment variables, config files, command-line arguments, or similar, inside your app’s components. That makes testing very hard and it hides those dependencies. Configuration values are also dependencies. That said, load them in the same place you set up the wiring — near your main function. It will make them evident and easily configurable in automated tests.
The result of calling a method on an object should be determined by its own implementation logic, and optionally by the behavior of one of its constructor arguments, or the method arguments provided to it; and nothing more. Advanced Web Application Architecture
// ⏺ in implementation
class ImageLoader(
private val timeout: Int,
private val baseUrl: String,
) {
// …
}
// ⏺ in wiring
val imageLoader = ImageLoader(
timeout = System.getenv("IMAGE_TIMEOUT"),
baseUrl = System.getenv("IMAGE_BASE_URL"),
)
// ⏺ in testing
val imageLoader = ImageLoader(
timeout = 2000,
baseUrl = "http://localhost:7171/mock-images-api",
)Consider lazy loading
Sometimes you want to run your app locally but don’t feel you need to set up all the env vars. If you’re not using all the parts, why should the app fail to start? Lazy loading is the answer. It’s also useful if some dependency is expensive to build or has some undesired side-effect. The downside of lazy loading is that, if something's misconfigured, the app only fails in run time rather than in boot time. This may be dangerous, so use it wisely. Here’s how:
// 1️⃣ inline initialization
// dep. is created once in boot time
val repo = MongoDBUserRepository(
KMongo.createClient(System.getenv("MONGODB_HOST"))
.getDatabase("demo")
)
// 2️⃣ with a getter
// dep. is created only if/when used
val repo get() MongoDBUserRepository(
KMongo.createClient(System.getenv("MONGODB_HOST"))
.getDatabase("demo")
)
// 3️⃣ lazy loading
// dep. is created only if used. it's only created once
val repo by lazy {
MongoDBUserRepository(
KMongo.createClient(System.getenv("MONGODB_HOST"))
.getDatabase("demo")
)
}Create interfaces only when needed
The dependency inversion principle states that:
[…] stable software architectures are those that avoid depending on volatile concretions, and that favor the use of stable abstract interfaces. Clean Architecture, Chapter 11

In other words, the inner parts of your app should not directly depend on the outer parts, which are more volatile; for example, the domain should never depend on a concrete database repository. The other way around is fine; for example, a web handler can depend directly on the domain. The bottom line is that, if you’re not violating the dependency rule, you don’t need to create an interface.
Only introduce an interface for objects that actually communicate with something outside your application. This might save you a couple of interfaces. Advanced Web Application Architecture
Another valid reason to create an interface is to have multiple versions of the same component (e.g. for feature toggling).
📝 As an alternative to interfaces, consider passing a function if your language supports it. This goes in line with the idea of injecting the minimum needed to accomplish the goal.
Consider wiring for feature toggling
Rather than having ifs spread across your business logic, what if you injected different versions of dependencies according to feature flags (e.g. from env vars) as if they were plugins? Feature flags can be part of the wiring infrastructure. Think about a customizable PC where you can easily swap each of its parts without breaking its general intent.
// ⏺ injecting a different feature based on an FF
val deleteUser = when (System.getenv("DELETE_FF")) {
"true" -> DeleteUserNew(repo)
else -> DeleteUser(repo)
}
App(deleteUser).start()
// ⏺ injecting an inocuous component version if an FF is off
// you could also decide based on the env (e.g. staging only)
val cleanupJob = when (System.getenv("CLEANUP_JOB_ACTIVE_FF")) {
"true" -> FastCleanupJob(clock)
else -> object : CleanupJob() {
override fun run() {
// does nothing
}
}
}
App(cleanupJob).start()An obvious benefit is that the branching decision happens early, which increases safety (the old code is not even wired/running). This power is only unleashed if you have your business logic split per use case, which I recommend anyway since it makes testing so much easier and respects the SRP.
This technique provides pleasant management of feature flags, especially when toggling them but also when it’s time to delete the old code. It also provides a way to inject dry-run (side-effects-free) versions of components. You could even deploy multiple versions of the same app.
Encapsulate the creation of dependencies
I like to see dependencies as pluggable parts ready to be used. Using the customizable PC metaphor, you can imagine these parts (e.g. camera, graphics card) in a box, at your disposal, and ready to be assembled. In software, the dependencies (e.g. use cases, repositories, gateways) can also be in a single place, like a pile of building blocks. I usually put them in objects that I can pass around. In the example below, you can see two possible configurations I can boot up my app with:
// ⏺ Config.kt
abstract class Config {
open val listUsers = ListUsers(repo)
open val createUser = CreateUser(repo)
open val deleteUser = DeleteUser(repo)
abstract val repo: UserRepository
}
object ConfigWithMongoDb : Config() {
override val repo =
MongoDBUserRepository(
KMongo.createClient(System.getenv("MONGODB_HOST"))
.getDatabase("cdemo")
)
}
object ConfigWithMySql : Config() {
override val repo =
MySqlUserRepository(
Database.connect(
url = System.getenv("MYSQL_URL"),
driver = "com.mysql.cj.jdbc.Driver"),
)
}
// ⏺ Main.kt
fun main() {
ToDoApp(ConfigWithMongoDb).start()
}This is another way to say that you can separate the creation of dependencies from their wiring. Encapsulating dependencies enables having different configurations depending on your needs. For example, you could have a configuration for local development. You could also launch a frontend app that depends on server APIs with a configuration that relies solely on fakes. Be aware that this pattern doesn't make sense on simple apps.
