The term Dependency Injection (DI) is used to describe many things. Sometimes it refers to the usage of a DI container. Others use it to refer to dependency inversion—the D in SOLID principles.
But most commonly: it’s just passing around arguments. Like pieces of a puzzle. Or a lego factory.
Avid readers of this publication have already come across the term Dependency Injection Hell in a previous article. It refers to the code smell of creating bloated God Classes without much finesse.
During coaching I see some teams use DI containers because “the framework came with it”. Without a clear use case for why you are using it, it can often lead to more complex code.
Continue reading and let’s go into a bit more detail on some common patterns that will help you and your team move faster and with less confusion.
Raw ROI - How good can it get?
Full disclosure: You can do everything without one. Or everything with one.
The Dependency Inversion principle serves one, ultimate purpose: modularity.
However, DI containers (DIC) serve two different ones:
Singletons via Scoped (shared) services
Configurability
Overall, we want to end up with clear, simple modules while leaving the nitty gritty details of composition and instantiation to the application that uses it. We want less lines of code. Less lines of configs. And ideally no magic outside of our composition root.
To reconfigure your dependency, they have to be modular.
For code to be modular it has it be designed to be easy to change.
Ease of change is a high standard for any engineering or architecture effort. A well-recommended scenario is testing its behavior rather the implementation by separating checks on what it does for the user from how it was implemented.
Modularity is a topic best left for another time. If you’re curious though, here’s a podcast with me discussing these details with Jason Gorman, a renown TDD and modularity expert:
Pieces of the puzzle
As with ORMs, they can be used for one thing and forgotten.
Or they can permeate the entire ecosystem of your project, giving you headaches for years to come.
When you apply Dependency Inversion to the limit, you end up with one level of code — level zero — that has cannot be inverted. This is commonly a bootstrap or startup file.
We will refer to this as the Composition Root. Because it is the root level at which we compose the dependency graph.
Over the decades I have become more aware and sensitive to a few properties of good and bad DI container frameworks, along with inversion habits that I deem are worth your inspection:
Magic — Does the DIC come with magical auto-wiring or annotations? This may make your code more confusing to work with. A pure DIC should not permeate into your business and module-level code— not even with annotations.
Verbose Configs — We’re aiming to write less code. When the code-writing goes from your language of choice to verbose XML or JSON configs it’s masquerading a bad product at its very inception.
Lambda support — Sometimes all you need is a factory function. It pains me to see complex AbstractFactoryBuilderFactoryAbstractionComponents being created whose sole purpose it is to be buddies with the DI framework and inject the universe into a little array sorter. Your DI container should understand factory functions and inject the result without much effort.
Complex caching — Blessing and a curse. Large projects have large DI graphs. I have seen projects crumble under the weight of booting up a large DIC framework on each request only to crumble when it has to serve a single resized image. In PHP and Ruby ecosystems, if your app slows down gradually, the root cause is often a badly managed DI graph.
Multiple Composition Roots — Sometimes it is necessary to keep parts of your dependency graph logically separate. This is common in modular monoliths. .NET has logical separation between its projects and assemblies. Typescript projects may have server-side and client-side roots.
Practical Rules
There are two sets of rules that I’ve picked up from many different sources. Some are from the Clean Code and Clean Architecture series by Robert C. Martin, some are from mega-blogs like martinfowler.com and refactoring advice.
I will refer to instances as objects here for practicality’s sake. Your DIC will likely use a name like bean, component or service.
Age Rule
Long-lived objects should not depend on short-lived objects.
The composition root needs to outlive all objects it creates and manages
Short-lived objects may depend on long-lived objects only if they are within the same composition root-hierarchy
In web frameworks this is quite a common problem.
...
public constructor(private OrderRepository: Repository<Order>) {...}
public show(req: Request, orderId: number) {
const order = this.OrderRepository.find(OrderId.fromNumber(orderId));
// passing on code here will require objects that depend on this particular order. How is that dependency scoped properly?
...
}
Composition Rule
Your code will unavoidable have application, domain and infrastructure concerns along with some glue. Infrastructure and glue are considered low-level concerns for this rule. Application and domain are high-level.
High-level objects should not depend on low-level objects directly.
Low-level objects should not depend on high-level objects directly.
When cooperation is needed, a high- or low-level abstraction shall be created to wire them together.
An example of this in FinTech: a TransactionProcessor
in banking (high-level) should not depend on the type of logger it uses (low-level). At the same time, critical paths will require complex logic to not leak personal data in the log files that are transaction-specific.
The High-to-low level coupling can be achieved by using a Logger
interface. This is infrastructure.
The Low-to-high level coupling can be achieved by using a TransactionFormatter
. This is part of the domain.
Examples
Below is a nonsense example of representing a house with ways to interact with it. Particularly in its physical separation. It’s a bad model all models are. Let me know how you’d do it differently!
☒ BAD: This is lazy DI
class House {
public constructor(private I: stove,
private dont: sink,
private know: bedroom,
private which: hallway,
private room: fridge,
private it: roof,
private was: entrance) {}
public openDoor(): void {}
public cook(): void {}
}
☑️ GOOD: Focus concerns on per-need basis
class House {
public constructor(private mainDoor: entrance,
private cookingStation: stove)
}
☑️ BETTER: Compose the building
class House {
public constructor(private foyer: entrance & hallway),
private kitchen: ICookingStation)
}
DI is about the imperative side, not the configs
☒ Inversion is not about this side
public constructor(private I: stove,
private dont: sink,
private know: bedroom,
private which: hallway,
private room: fridge,
private it: roof,
private was: entrance) {}
Nor is it about the auto-wiring magic and configs.
☑️ It’s to decouple from constructors
const house = this.houseBuilder.compose([...]);
Can you imagine passing the constructor from the previous example everywhere throughout your code? Granted, it’s a code smell. But if it’s present you have to deal with it before you can fix it. This coupling to constructor parameters in combination the original issue of scoping is what DIC’s aim to solve.
And don’t get me started on code reuse through by abstract class abuse where constructor are being shadowed or overriden!
Conclusion
It’s a complex topic, there’s sadly no free lunch in these parts of software. Your team is likely getting advice on this left, right and centre from different framework flavours. And that’s okay.
Hopefully this was a practical glimpse into the underlying principles and given you some breathing room.
Relax, you’re on the right path ✌️
What do you disagree with in the article. I’d love to hear your thoughts.
What drives you nuts about Dependency Injection frameworks?