Day 128 — Dependency Injection (Manual)¶
Month 5 · Week 3 · ⬅ Day 127 · Day 129 ➡ · Journal index
🎯 Learning Objective¶
Wire an object graph by hand using constructor injection — no framework, no globals — so every dependency is explicit, swappable, and testable.
📚 Topics¶
- Constructor injection; "receive dependencies, don't reach for them"
- The composition root owns construction order and lifecycles
- Injecting interfaces (Clock, Mailer) to create test seams
- Why package-level globals /
init()singletons are an anti-pattern
📖 Reading / Sources¶
- Go blog — Dependency injection (
google/wirerationale) - Mat Ryer — How I write HTTP services (DI in
main) - Effective Go — Interfaces & methods
-
testing— fakes over mocks
📝 Notes¶
- Dependency injection in Go is just passing arguments. A constructor takes its collaborators as parameters; it never
news them or reaches for a global → [[dependency-injection]]. - The constructor's parameter list is the dependency contract. If you can't construct the type without supplying X, X can't be silently forgotten → [[constructor-injection]].
- Inject interfaces, not concrete types, for anything that touches the outside world (clock, DB, mailer). That's the seam a test fills with a fake — no mocking framework needed → [[test-seam]].
- The composition root (
main, or anewServer()it calls) is the single place that picks concrete types and assembles the graph. Everything below it is wired by parameters → [[composition-root]]. - Prefer fakes over mocks: a small hand-written type that implements the interface is clearer and less brittle than a generated mock with expectation DSLs → [[fakes]].
- Anti-pattern: a package-level
var DB *sql.DBset ininit(). It hides the dependency, couples every caller to one instance, defeatst.Parallel(), and makes ordering bugs at startup. Pass it in instead → [[global-state]]. - DI ≠ a DI container. Manual wiring scales surprisingly far; reach for codegen (wire) only when the graph gets tedious — that's tomorrow.
💻 Code Examples¶
// Inject an interface so time is controllable in tests.
type Clock interface{ Now() time.Time }
type Service struct {
clock Clock
greeter Greeter
}
// Constructor injection: the signature is the dependency list.
func NewService(clock Clock, greeter Greeter) *Service {
return &Service{clock: clock, greeter: greeter}
}
func main() {
prod := NewService(realClock{}, politeGreeter{}) // production wiring
test := NewService(fixedClock{t: someTime}, politeGreeter{}) // deterministic
_ = prod
_ = test
}
Full runnable model (stdlib only):
examples/month-05/manualdi/main.go· Run:go run ./examples/month-05/manualdi
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
| Service depends on an injected port; fake proves delegation | ✅ | exercises/month-05/week-3/ports |
🐛 Mistakes Made¶
- Reached for
time.Now()directly inside a method, then couldn't test the timestamp deterministically. Injected aClock. - Stored a dependency in a package global "to avoid threading it through"; two parallel tests then stomped on each other. Threaded it through constructors instead.
❓ Open Questions¶
- When the graph has ~30 nodes, is hand-wiring still worth it, or does wire pay for itself? (Find out tomorrow.)
🧠 Active Recall (answer without looking)¶
- Q: What is "dependency injection" in plain Go terms, and what's the main payoff?
A
Passing a type's collaborators in (usually via its constructor) instead of having it create or reach for them. Payoff: explicit dependencies, controlled lifecycles, and a test seam — you pass a fake implementation of the injected interface.- Q: Why is a package-level
var DB *sql.DBinitialized ininit()an anti-pattern?
A
It hides the dependency (not visible in any signature), couples every caller to one shared instance, breaks parallel tests, and creates fragile startup ordering. Inject it through constructors instead.🪶 Feynman Reflection¶
A type shouldn't go shopping for its tools — they should be handed to it at the door. The constructor is that door: whatever the type needs to do its job arrives as a parameter. main is the workshop where I decide which real tools to hand out; a test is a different workshop where I hand out pretend tools that record what happened. The type behaves identically either way because it only knows the tool's shape (its interface).
🕳️ Knowledge Gaps¶
- Managing lifecycles for injected resources (who calls
Close()and in what order) as the graph grows.
✅ Summary¶
I can wire a dependency graph by hand with constructor injection, inject interfaces to create test seams, keep all concrete choices in one composition root, and articulate why global singletons are a trap.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 129: automate the wiring with
google/wire(compile-time DI codegen).
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: feat(examples): manual constructor-injection wiring (day 128)