DEV Community

Jack Apolo
Jack Apolo

Posted on

Go DI Libraries Comparison: go-infrastructure vs. Fx

Choosing a DI library in Go replaced how you wire services, manage lifecycle, and keep code testable. In this post, I will compare go-infrastructure and Uber's Fx to make a confident choice.

Why these two?

  • Fx is Uber’s DI framework built on dig. It wires dependencies at runtime via reflection and gives you a powerful application lifecycle (hooks for start/stop, modules, invoke). Great when boot order and shutdown choreography matter. (pkg.go.dev)
  • go-infrastructure is “batteries included”: DI plus logging, config hot-reload, events, error handling, filesystem/server helpers—all designed to work together out of the box. (GitHub)

Side-by-side snapshot

Dimension go-infrastructure Fx (Uber)
DI style Typed API; explicit registration & generic resolution Reflection-based (via dig) w/ fx.Provide/fx.Invoke
Lifecycle Minimal; use provided modules/utilities First-class fx.Lifecycle with OnStart/OnStop hooks
Out-of-the-box modules Yes: logs, file config reload, events, errors, disk/server No defaults; you compose your own
Learning curve Low—ship something fast Medium—concepts (Provide/Invoke/Lifecycle) to learn
Maturity Actively evolving Battle-tested across Uber services

Real pros & cons

go-infrastructure

Pros

  • Batteries included: plug-and-play logging, hot-reloading config from JSON, typed events, consolidated error handling. Less glue in main.go. (GitHub)
  • Straightforward DI: register constructors, resolve with generics—easy to follow and easy to test. See container, resolver, and dependencyinjection package in the repo. (GitHub)

Cons

  • No central lifecycle like Fx's; if you need ordered start/stop across many subsystems, you'll do a bit more by hand.
  • Smaller ecosystem; you'll write a module yourself when integrations get exotic.

Fx

Pros

  • Lifecycle done right: fx.Lifecycle w/ OnStart/OnStop for clean boot/shutdown ordering—critical in big services. (pkg.go.dev)
  • Composable via fx.Provide/fx.Invoke, value groups, and modules—scales with team size and dependency graphs. (pkg.go.dev)
  • Widely used (and maintained) in production at Uber. (go.uber.org)

Cons

  • Uses reflection (via dig): tremendous flexibility, but a little more indirection during boot, and sometimes “magical” error messages. (GitHub)
  • A bit more boilerplate for simple services.

Code you can skim

Minimal service injection

go-infrastructure

import (
    di "github.com/janmbaco/go-infrastructure/dependencyinjection" // use /v2 if your module is v2
    logsIoc "github.com/janmbaco/go-infrastructure/logs/ioc"       // ...and /v2 here too
)

type UserService struct{ /* ... */ }
func NewUserService() *UserService { return &UserService{} }

func main() {
    c := di.NewBuilder().
        AddModule(logsIoc.NewLogsModule()).
        Register(func(r di.Register) {
            r.AsSingleton(new(*UserService), NewUserService, nil)
        }).
        MustBuild()

    svc := di.Resolve[*UserService](c.Resolver())
    _ = svc
}
Enter fullscreen mode Exit fullscreen mode

The repo exhibits this pattern (builder → modules → Resolve). (GitHub)

Fx

import "go.uber.org/fx"

type UserService struct{ /* ... */ }

func NewUserService() *UserService { return &UserService{} }

func main() {
    fx.New(
        fx.Provide(NewUserService),
        fx.Invoke(func(s *UserService) {
            // use s
        }),
    ).Run()
}
Enter fullscreen mode Exit fullscreen mode

Typical Provide/Invoke/Run() flow. (pkg.go.dev)


Config that hot-reloads

go-infrastructure

import (
    di "github.com/janmbaco/go-infrastructure/dependencyinjection" // use /v2 if applicable
    cfgIoc "github.com/janmbaco/go-infrastructure/configuration/fileconfig/ioc"
    cfgRes "github.com/janmbaco/go-infrastructure/configuration/fileconfig/ioc/resolver"
)

type Config struct{ Port int `json:"port"` }

func main() {
    c := di.NewBuilder().AddModule(cfgIoc.NewConfigurationModule()).MustBuild()
    h := cfgRes.GetFileConfigHandler(c.Resolver(), "config.json", &Config{Port: 8080})
    _ = h.GetConfig().(*Config) // updates when the file changes
    select {}
}
Enter fullscreen mode Exit fullscreen mode

Hot-reload from JSON is part of the provided modules. (GitHub)

Fx

You’ll bring your own config loader/watcher and wire its start/stop with fx.Lifecycle:

fx.New(
    fx.Provide(NewConfigWatcher),
    fx.Invoke(func(lc fx.Lifecycle, w *ConfigWatcher) {
        lc.Append(fx.Hook{
            OnStart: func(ctx context.Context) error { return w.Start() },
            OnStop:  func(ctx context.Context) error { return w.Stop() },
        })
    }),
).Run()
Enter fullscreen mode Exit fullscreen mode

Fx has first-class lifecycle hooks. (pkg.go.dev)

Import-path note (SemVer v2+): when you ship v2 of your module, imports must specify /v2 (example: github.com/janmbaco/go-infrastructure/v2/...). Otherwise, go get won’t resolve the right major. (GitHub)


Verdict: Which one should you pick?

  • Use go-infrastructure when you need to ship fast with logging, hot-reloading config, events, and DI already wired. For normal HTTP services or microservices where you don't need intricate boot choreography, it cuts boilerplate and gets you to "working app" quicker. This is my default to get a service live. (GitHub)
  • Use Fx when you have complicated startup/shutdown requirements, many subsystems which have to start in order, or a large team that benefits from strict lifecycle semantics and modular composition. This is my pick for large multi-module systems. (pkg.go.dev)

If you're on the fence: prototype a small slice in both. If lifecycle hooks drive your design, Fx will feel natural; if your priority is getting robust plumbing with minimal ceremony, go-infrastructure will feel lighter.


Further reading

Top comments (0)