Skip to content

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

📝 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 a newServer() 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.DB set in init(). It hides the dependency, couples every caller to one instance, defeats t.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 a Clock.
  • 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)

  1. Q: What is "dependency injection" in plain Go terms, and what's the main payoff?
APassing 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.
  1. Q: Why is a package-level var DB *sql.DB initialized in init() an anti-pattern?
AIt 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)