DEV Community

Tetsuro Mikami
Tetsuro Mikami

Posted on

Dependency Injection in Go: How Much Is Enough for Web APIs?

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:""`
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)