Tools
Dependency Injection in Go: How Much Is Enough for Web APIs?
2025-12-24
0 views
admin
What most web APIs actually need ## Runtime DI frameworks: powerful, but usually unnecessary ## Static wiring is already enough ## Where wire starts to feel heavy ## A lighter static model: field-tag-based generation ## Is there still a place for wire? ## Practical takeaway ## Closing 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. If we strip away abstractions, a large percentage of Go web APIs share the same characteristics: A mostly tree-shaped dependency graph No runtime rebinding of implementations In this environment, DI is not about flexibility at runtime. It’s about wiring correctness and maintainability. Frameworks such as fx / dig or generics-based containers like do shine when your application behaves like a framework itself: For typical web APIs, however, these features often come with downsides: When dependencies are fixed and known ahead of time, runtime DI solves a problem you don’t really have. 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: 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. Wire’s design emphasizes explicitness: This explicitness can be valuable in some organizational contexts. But in day-to-day API development, it often turns into maintenance overhead: The static model is right—but the amount of ceremony is not always justified. If your goal is still static wiring and generated constructors, but with less DI-specific code, a simpler model works well: The generated code is just Go: 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. Wire can still make sense when: 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. Runtime DI (fx/dig, do) Static + minimal (injector) 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. 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. If static DI is enough… why not make it simpler? Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
type Container struct { Handler *UserHandler `inject:""`
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
type Container struct { Handler *UserHandler `inject:""`
} CODE_BLOCK:
type Container struct { Handler *UserHandler `inject:""`
} CODE_BLOCK:
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
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
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
} CODE_BLOCK:
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
} - Stateless request handling
- Dependencies resolved once at startup
- A mostly tree-shaped dependency graph config → database → repository → service → handler
- config → database → repository → service → handler
- No runtime rebinding of implementations - config → database → repository → service → handler - modules register themselves dynamically
- lifecycle hooks (start/stop) matter
- plugins or optional components are loaded at runtime - additional APIs to learn and remember
- runtime error surfaces for what is fundamentally static wiring
- harder-to-follow control flow during startup - compile-time dependency resolution
- no runtime container
- plain Go code as output - provider functions
- provider sets
- injector functions per root - 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 - declare what you want in a container struct
- let a generator resolve the graph
- emit plain Go constructors - 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 - Runtime DI (fx/dig, do) Powerful, but often overkill for standard web APIs
- Powerful, but often overkill for standard web APIs
- Static DI (wire) Sufficient for most APIs, but verbose
- Sufficient for most APIs, but verbose
- Static + minimal (injector) Same guarantees, less ceremony
- Same guarantees, less ceremony - Powerful, but often overkill for standard web APIs - Sufficient for most APIs, but verbose - Same guarantees, less ceremony
how-totutorialguidedev.toaidatabasegitgithub