Want your code to be easy to observe? Use dependency injection for observability concerns. Sounds dry. Hear me out.
The problem
Your code calls log.info(...) directly. In tests, you can't verify what was logged. In prod, if you want to change the logger, you're grepping the codebase. If you want to add tracing, you're editing every call site.
Same for metrics. Same for tracing. Same for error reporting.
The fix
Pass the observer in. Functions take a logger, metrics, or tracer as an argument (or constructor dependency). The function doesn't know what's behind it.
func HandleOrder(order Order, deps Deps) error {
deps.Metrics.Inc("order.received")
deps.Logger.Info("processing order", "id", order.ID)
// ...
}
Now:
- In tests, pass in a mock that captures all calls
- In prod, pass in the real logger
- Changing backends (Datadog → Prometheus) is one wire-up change
- Adding tracing is one new field in
Deps
Why most codebases don't do this
It feels verbose. 'Why do I have to thread a logger through every function?' Engineers hate boilerplate.
The alternative is a global singleton. Easy to use, impossible to test cleanly, nightmare to refactor.
The boilerplate is worth it. Especially for observability, where you will want to swap implementations later.
The subtle win
Dependency-injected observability forces you to think about what you're observing. When you have to explicitly pass the logger, you notice that a function is calling it 8 times. Is that too much? Is the logging doing real work? Would one structured log at the end of the function be better?
Functions with injected dependencies tend to have better observability because the developer had to look at it.
The starting point
You don't need to refactor everything at once. Pick one critical path — the checkout flow, the auth path. Refactor just that path to use dependency injection for observability. See if tests and debugging get easier.
If yes, expand. If no, you found something else is the bottleneck.
The bigger idea
Observability is code. Treat it with the same architectural discipline you treat the rest of your codebase. Dependency injection is one tool. There are others (context objects, middleware, decorators). Pick one, apply it consistently, and your future-you will be able to observe your code in ways that are impossible with ad-hoc log.info calls everywhere.
Written by Dr. Samson Tanimawo
BSc · MSc · MBA · PhD
Founder & CEO, Nova AI Ops. https://novaaiops.com
Top comments (0)