Dependency Injection (DI) in Go often sparks debates that feel disproportionate to the actual needs of most web APIs. Discussions quickly escalate to containers, lifecycles, scopes, and dynamic resolution—despite the fact that many services have a simple, static dependency graph resolved once at startup.
This article argues a straightforward position:
For a typical web API, compile-time dependency resolution is enough.
From there, we’ll look at why runtime DI frameworks tend to be overkill, why static approaches like wire already cover most needs, and why a field-tag-based generator can simplify things even further.
What most web APIs actually need
If we strip away abstractions, a large percentage of Go web APIs share the same characteristics:
- Stateless request handling
- Dependencies resolved once at startup
-
A mostly tree-shaped dependency graph
- config → database → repository → service → handler
No runtime rebinding of implementations
In this environment, DI is not about flexibility at runtime. It’s about wiring correctness and maintainability.
Runtime DI frameworks: powerful, but usually unnecessary
Frameworks such as fx / dig or generics-based containers like do shine when your application behaves like a framework itself:
- modules register themselves dynamically
- lifecycle hooks (start/stop) matter
- plugins or optional components are loaded at runtime
For typical web APIs, however, these features often come with downsides:
- additional APIs to learn and remember
- runtime error surfaces for what is fundamentally static wiring
- harder-to-follow control flow during startup
When dependencies are fixed and known ahead of time, runtime DI solves a problem you don’t really have.
Static wiring is already enough
If dependencies are static, resolving them at compile time or generation time is a natural fit.
This is where tools like wire enter the picture. They provide:
- compile-time dependency resolution
- no runtime container
- plain Go code as output
For a normal web API, this model is already sufficient. You get safety, predictability, and zero runtime DI overhead.
So far, the conclusion is simple:
For most web APIs, static DI is enough.
Where wire starts to feel heavy
Wire’s design emphasizes explicitness:
- provider functions
- provider sets
- injector functions per root
This explicitness can be valuable in some organizational contexts. But in day-to-day API development, it often turns into maintenance overhead:
- provider sets need constant updates as the graph evolves
- DI-specific files grow alongside business code
- the wiring itself becomes something developers have to reason about
The static model is right—but the amount of ceremony is not always justified.
A lighter static model: field-tag-based generation
If your goal is still static wiring and generated constructors, but with less DI-specific code, a simpler model works well:
- declare what you want in a container struct
- let a generator resolve the graph
- emit plain Go constructors
Conceptually:
type Container struct {
Handler *UserHandler `inject:""`
}
The generated code is just Go:
func NewContainer() (*Container, error) {
cfg := NewConfig()
db, err := NewDatabase(cfg)
if err != nil {
return nil, err
}
svc := NewUserService(db)
h := NewUserHandler(svc)
return &Container{Handler: h}, nil
}
No runtime container. No provider sets. No reflection.
This is the idea behind injector, a field-tag-only DI code generator:
https://github.com/mickamy/injector
The link is intentionally placed here, after the argument is established, rather than as an upfront pitch.
Is there still a place for wire?
Wire can still make sense when:
- provider sets are treated as a public composition API
- explicit wiring boundaries are a deliberate design goal
- DI graphs are reviewed and curated as first-class artifacts
In other words, wire’s strengths are organizational and process-driven.
If your project doesn’t need that level of explicitness, wire becomes harder to justify.
Practical takeaway
-
Runtime DI (fx/dig, do)
- Powerful, but often overkill for standard web APIs
-
Static DI (wire)
- Sufficient for most APIs, but verbose
-
Static + minimal (injector)
- Same guarantees, less ceremony
For the common case—stateless web APIs with stable dependency graphs—static wiring is enough.
And if you can keep that wiring static and reduce the amount of DI-specific code you maintain, that’s an even better outcome.
Closing
Dependency Injection doesn’t need to be a framework-level decision for most Go web APIs. It’s a startup detail.
Resolve dependencies once. Generate plain Go code. Keep the wiring boring.
If wire already feels sufficient—but a bit heavy—the field-tag-based approach is a natural next step.
In that sense, injector is not a radical alternative.
It’s simply asking:
If static DI is enough… why not make it simpler?
Top comments (0)