Dependency Injection and Inversion of Control without religion 🔌
Dependency Injection is often described as a liturgy involving containers, annotations, reflection, and other creative ways to complicate what could have been a constructor with two parameters. It is usually better to start from the more important idea: before DI, there is Inversion of Control.
Inversion of Control: who owns the flow? 🧠
IoC means your application code does not keep total control of the execution flow. Instead of calling every dependency directly and deciding every step internally, part of the control is delegated to a framework, runtime, callback, or orchestrator.
Daily examples:
- a web server calls your handler
- a test runner executes your tests
- an event consumer receives messages from a broker
In brutally short form: don’t call us, we’ll call you.
Dependency Injection: make collaborations explicit 🪛
Dependency Injection is a practical way to apply IoC to component dependencies. Instead of constructing them inside the component, you receive them from outside.
Immediate benefits:
- more visible dependencies
- simpler tests
- easier substitution of infrastructure and collaborators
- less coupling to concrete details
The point is not abstraction for its own sake. The point is to avoid a domain component building its own database, logger, HTTP client, and maybe its own existential crisis.
Constructor injection: the healthy default 🏗️
The clearest and most boring form of DI is often the best one: constructor injection.
|
|
Dependencies are explicit, readable, and easy to replace in tests. No magic. No container shrine.
Small interfaces, not decorative interfaces ✂️
DI works well when contracts are small and coherent. If you define a fifteen-method interface just so you can say you used dependency injection, you are moving the problem, not solving it.
Good rules:
- define interfaces where there is real variability
- keep them close to the caller or the domain using them
- do not introduce them as architecture folklore
In Go this matters even more: small local interfaces usually work better than transplanted enterprise hierarchies.
Service Locator: the well-dressed cousin of toxic global state 🚫
Service Locator is often sold as elegant. In practice, it is very often global state with better tailoring.
Typical problems:
- real dependencies are not visible in the signature
- understanding what a component uses requires tracing registrations and bootstrap code
- tests become more fragile because they depend on global setup
If I need to read half the application bootstrap to know what a service depends on, I am not looking at good design. I am doing cave exploration.
DI containers: useful, but not free 🧰
A container can make sense in large or highly modular applications, especially when wiring becomes repetitive. But it is not free:
- it hides the dependency graph
- it can move errors from compile time to runtime
- it makes startup behavior more opaque
If your app has ten explicit dependencies and a readable main, you may already have what you need. A container is not automatically maturity. Sometimes it is just industrial decoration.
When DI is actually useful 🎯
DI brings value when:
- you want to isolate the domain from infrastructure
- you have replaceable collaborators like repositories, clocks, gateways, or notifiers
- you want fast and focused tests
- some dependencies change much more often than others
It is not required for every function, struct, or package. If there is no real variability, a concrete dependency may be perfectly fine.
Signs you are overdoing it 🚨
- interfaces created before there is even a plausible second implementation
- constructors assembling half the system to return a thin wrapper
- containers everywhere but unclear responsibilities
- mock and boilerplate costs greater than the original problem
In short 🧾
Inversion of Control is the concept. Dependency Injection is a useful technique for applying it to dependencies. The right criterion is not ideological purity, but reduced coupling and a lower cost of change.
The natural companion pieces are the guides on software design principles, S.O.L.I.D., and design anti-patterns.