Good practices when using dependency injection

Dependency Injection (DI) is the most common way to achieve decoupling and separation of concerns, also promoting testability and readability. Let’s analyze some related design patterns that I’ve spotted in some projects.

Luís Soares
Mar 15 · 8 min read

📝 I used Kotlin in the code samples, but all the patterns apply regardless of the language or runtime.

You don’t need a DI framework

I use frameworks and libraries when it pays off. In what concerns DI, I recommend doing it manually. Check how clear and effective vanilla DI is:

“Dependency Injection” is a 25-dollar term for a 5-cent concept. [… it] means giving an object its instance variables. Really. That’s it. Dependency Injection Demystified

DI is a design concern, not a tool. The way to achieve it is to learn its concepts. If you’re using a library (e.g. Guice, Dagger, Koin), at least try to understand its underlying magic. This also applies to all-in-one frameworks such as Spring Boot.

Use constructor injection

There are a few types of DI:

The one I recommend is always constructor injection because it makes it hard to build a component without some of its dependencies. It makes each component's dependencies evident in tests and in implementation. Here’s an example of a component in a test class where you can clearly see what it depends on:

📝 Use named arguments if the language supports them. That significantly increases readability.

If the dependency is optional (uncommon), you can still use constructor injection and set the dependency to some default value:

In the rare case that you need a dynamic dependency — one that changes during runtime (e.g. a feature flag that’s obtained from the cloud), you can pass a function to fetch it. For example:

Protect dependencies

Making dependencies private ensures no one, other than their client, will interact with them. Immutable (i.e. final) dependencies guarantees no one will change them later. Non-nullable dependencies documents and enforces them as mandatory. Here’s an example:

📝 If the language doesn’t support enforced non-nullable types, make sure you null-check the dependencies in the constructor. This ensures invalid objects (i.e. with missing dependencies) aren’t created.

Centralize wiring

Wiring makes me think about an integrating circuit with all the components connected through colored wires. In software, wiring is the sum of all the DIs that happen within your app. Wiring is what assembles all the parts of your app together.

Image by Manoel Lemos via Flickr Copyright-free

In a typical software architecture (e.g. onion architecture), the wiring is done outside all layers. It’s the infrastructure part of your app and must therefore be properly isolated so it can be easily managed. How does it look like in code? Here’s an example:

This setup is a good example of self-documenting code because it centrally describes your app’s web of components: the dependencies, their clients, and the degrees of nesting. Because of that, it can and should be used as a tool to identify some design smells. Here are a few examples:

  • ⚠️ Too many dependencies — a component needing more than two dependencies is suspicious; more than four should raise an alarm. Check if you have an over-busy component. This also makes testing so much easier and focused.
  • ⚠️ Multiple creations of the same dependency — horizontal dependencies like monitors and loggers are usually shared so they should be created/configured once and passed multiple times.
  • ⚠️ Too much nesting — having a high degree of nesting might reveal that you are injecting components that are too tiny. You need to ask yourself whether they are implementation details; maybe they don’t need to be exposed to the outside, particularly if they’re used only once.
  • ⚠️ Wide scope — beware if you’re passing a wider scope than it’s actually needed (e.g. the whole configuration rather than just the required values) since that obscures the real needs of the components, in tests and implementation. Also, be aware of “black hole” dependencies that might be doing too much (e.g. named like “Hub”, “Manager”, “Service”).
  • ⚠️ Not respecting the layers and dependency directions — the wiring may reveal some violations of the layers and the dependencies’ directions of your app (a way to enforce it is to write ArchUnit tests). For example, your domain should never reference a concrete adapter but rather an interface.

The wiring code is usually placed at your main function. To make your main thin (a good practice), you can also extract into a function that you invoke from there.

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.

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:

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:

This is another way to say that you should separate the building 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.

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.

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.

Image by Jp Valery via Unsplash Copyright-free

Create interfaces only when needed

In statically typed languages, creating interfaces for every component is a common behavior that you should question.

DI is the proper way to guarantee the dependency inversion principle, which 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

The inner you go in the circle, the more stable. The arrows above the circles show the valid dependencies’ directions (volatile parts can depend on stable parts, not the other way around)

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 depending on the domain. The bottom line is, if you’re not violating the dependency rule, you don’t need to create an interface. Another valid reason to create an interface is having multiple versions of the same component (e.g. as in feature toggling).

Applying patterns in DI promotes predictability hence supporting building new functionality but also helping maintainability. It also serves as code self-documentation.

A conclusion of these recommendations is that there should be a clear line separating dependency creation/injection from your app — wiring is just assembling details. Proper wiring allows you to assemble your app and plug and play different components. It provides a control panel where you can manage your app’s dependencies.

My clean architecture sample project applies the article’s recommendations.

Image by Spencer via Unsplash Copyright-free


Everything connected with Code & Tech!

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store