SKILL.md
$27
fx is built on top of dig and shares the same reflection-based container engine. The DI primitives (Provide, Invoke, In/Out structs, named values, value groups) are identical — fx.In/fx.Out are re-exports of dig.In/dig.Out.
What fx adds on top:
Concern
dig
fx
DI container
✅ dig.New()
✅ (embedded)
Lifecycle hooks
❌
✅ fx.Lifecycle OnStart/OnStop
Module system
❌
✅ fx.Module with scoped decorators
Signal-aware run loop
❌
✅ app.Run() blocks on SIGINT/SIGTERM
Structured event logging
❌
✅ fx.WithLogger / fxevent
Startup/shutdown timeout
❌
✅ fx.StartTimeout / fx.StopTimeout
Choose fx for long-running services (HTTP servers, workers, daemons) — lifecycle and signal handling are mandatory there, and modules make large service graphs manageable.
Choose raw dig when you need wiring without a framework: CLI tools, libraries that expose a container to callers, test harnesses, or embedding DI into an existing app that manages its own lifecycle. See samber/cc-skills-golang@golang-uber-dig skill.
The Application
import "go.uber.org/fx"
app := fx.New(
fx.Provide(NewLogger, NewDatabase, NewServer),
fx.Invoke(RegisterRoutes),
)
app.Run() // blocks until SIGINT/SIGTERM, then runs OnStop hooks
Boot stages: fx.New validates types (constructors do not run); app.Start(ctx) runs each fx.Invoke and fires OnStart hooks in topological order; main blocks on app.Done(); app.Stop(ctx) fires OnStop hooks in reverse order. Default timeout is 15 seconds — override with fx.StartTimeout / fx.StopTimeout.
Provide and Invoke
fx.New(
fx.Provide(NewLogger, NewDatabase, NewServer), // lazy
fx.Invoke(RegisterRoutes, StartMetricsExporter), // always run during Start
)
fx.Provide registers constructors; fx.Invoke is the trigger — without an Invoke (directly or transitively) referencing a type, its constructor never runs.
Lifecycle Hooks
Inject fx.Lifecycle and append hooks. Constructors should return quickly; long-running work belongs in OnStart.
func NewHTTPServer(lc fx.Lifecycle, log *zap.Logger, cfg *Config) *http.Server {
srv := &http.Server{Addr: cfg.Addr}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
ln, err := net.Listen("tcp", srv.Addr)
if err != nil { return err }
go srv.Serve(ln) // blocking work in a goroutine
return nil
},
OnStop: func(ctx context.Context) error {
return srv.Shutdown(ctx)
},
})
return srv
}
Both callbacks receive a context bounded by StartTimeout/StopTimeout — respect cancellation. OnStart must return quickly — spawn a goroutine for blocking work; otherwise startup hangs and dependent hooks never fire.
fx.StartHook / fx.StopHook / fx.StartStopHook adapt simpler signatures (no context, no error, or both):
lc.Append(fx.StartStopHook(srv.Start, srv.Stop)) // matched pair
Parameter and Result Objects
fx re-exports dig's dig.In / dig.Out as fx.In / fx.Out. Use them when a constructor has 4+ dependencies, or when you need name/group/optional tags.
type ServerParams struct {
fx.In
Logger *zap.Logger
DB *sql.DB
Cache *redis.Client `optional:"true"`
Routes []http.Handler `group:"routes"`
}
func NewServer(p ServerParams) *Server { /* ... */ }
fx.Annotate
fx.Annotate wraps a constructor to add tags or interface bindings without a fx.Out struct. Prefer it for ergonomic name/group/As bindings:
fx.Provide(
fx.Annotate(NewPrimaryDB, fx.ResultTags(`name:"primary"`)),
fx.Annotate(NewPostgresDB, fx.As(new(Database))), // expose interface
fx.Annotate(NewUserHandler,
fx.As(new(http.Handler)),
fx.ResultTags(`group:"routes"`),
),
)
Value Groups
Many constructors, one consumer slice — typical for routes, health checks, metrics collectors:
type RouteResult struct {
fx.Out
Handler http.Handler `group:"routes"`
}
type ServerParams struct {
fx.In
Routes []http.Handler `group:"routes"`
}
Append ,flatten (group:"routes,flatten") to unwrap a slice instead of nesting it. Order is not guaranteed — provide an explicit ordered slice when sequence matters.
fx.Module
fx.Module groups providers, invokes, and decorators under a name. Modules scope decorators to themselves and their children — a logger renamed in fx.Module("db", ...) only appears renamed for code inside that module.
var DatabaseModule = fx.Module("database",
fx.Provide(NewConnection, NewUserRepository),
fx.Decorate(func(log *zap.Logger) *zap.Logger {
return log.Named("db")
}),
)
func main() {
fx.New(
fx.Provide(NewConfig, NewLogger),
DatabaseModule,
HTTPModule,
).Run()
}
Treat each module as a small library that can be lifted into another app — its public surface is the types it Provides.
For fx.Supply/fx.Replace/fx.Decorate, optional deps, custom logging, manual lifecycle, and Quick Reference, see advanced.md.
Best Practices
- Keep
main()thin — providers, modules, and a singleRun(). Push real work into modules so each can be tested in isolation.
- Use lifecycle hooks instead of
init()or goroutines launched from constructors — Start/Stop ordering depends on graph topology, butinit()goroutines do not, which leads to races and leaks.
- OnStart must return promptly — long work goes in a goroutine inside the hook. A blocking OnStart hangs the rest of the boot.
- Respect
ctx.Done()in hooks — a hook that ignores cancellation is reported as a timeout failure but its goroutine continues, leaking resources.
- Group by module, not by layer — a module owns the providers, lifecycle, and decorators for one concern (HTTP, DB, metrics).
- Use
fx.Annotatefor tags rather than wrapping a constructor in anfx.Outstruct — keeps the constructor reusable outside fx.
- Replace
fx.Providewithfx.Supplyfor pre-built values (config, command-line flags). Shorter, signals intent.
- Validate the graph in CI by booting under
fx.New(...).Err()— catches missing providers and cycles before deploy.
Common Mistakes
Mistake
Fix
Long-running work directly in OnStart
Spawn a goroutine inside OnStart; the hook itself must return quickly so dependent hooks can run.
fx.Provide something that should be fx.Supply
Pre-built values (config, secrets) belong in fx.Supply — clearer and avoids a no-op constructor.
Module decorator leaking to siblings
Decorate inside fx.Module(...) — decorators flow only to descendants. A top-level fx.Decorate is global.
Group order assumed
Groups are unordered. If order matters, provide an ordered slice from one constructor.
Constructors with side effects
Side effects belong in OnStart — constructors should be cheap and pure-ish, since they may run concurrently and lazily.
Forgotten fx.Invoke
Without an Invoke (or downstream consumer), constructors never run. Add at least one Invoke per app.
Testing
Use go.uber.org/fx/fxtest to integrate fx with *testing.T (failures call t.Fatal, RequireStop registers as t.Cleanup). fx.Populate(&target) pulls values out of the graph; fx.Replace swaps real dependencies for fakes. Full patterns in testing.md.
Further Reading
- advanced.md — Supply/Replace/Decorate, optional deps, custom event logging, manual lifecycle, full Quick Reference
- recipes.md — full HTTP service with database/metrics, background workers with graceful drain, multiple impls of the same interface, manual lifecycle for CLI embedding
- testing.md — fxtest patterns,
fx.Replace,fx.Populate, isolated lifecycle tests, CI graph validation
Cross-References
- → See
samber/cc-skills-golang@golang-uber-digskill for the underlying container,dig.In/dig.Out, and DI without lifecycle
- → See
samber/cc-skills-golang@golang-dependency-injectionskill for DI concepts and library comparison
- → See
samber/cc-skills-golang@golang-samber-doskill for a generics-based alternative without reflection
- → See
samber/cc-skills-golang@golang-google-wireskill for compile-time DI (no runtime container)
- → See
samber/cc-skills-golang@golang-structs-interfacesskill for interface design patterns
- → See
samber/cc-skills-golang@golang-contextskill for context propagation in OnStart/OnStop hooks
- → See
samber/cc-skills-golang@golang-testingskill for general testing patterns
If you encounter a bug or unexpected behavior in uber-go/fx, open an issue at https://github.com/uber-go/fx/issues.