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
dependencyinjectionpackage 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.Lifecyclew/OnStart/OnStopfor 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
}
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()
}
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 {}
}
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()
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 getwon’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.
Top comments (0)