intro to Uber FX & its usage patterns // Aleem Isiaka · Software Engineer GopherCon Africa 2026 • shortener.go ⎇ main Go 1.22 ready main 01 / 37 UTF-8 LF
"Go's package system is beautifully simple — until your app grows up and you're playing Jenga with constructors." // the package-graph stress every Go team eventually meets opening.md opening opening 02 / 37 read-only
The app we're building: a URL shortener. POST /shorten Accepts a long URL, returns a short slug. GET /{slug} Redirects to the original URL. // the dependency graph 01 Config addr, secrets → 02 · interface Store MemoryStore impl. → 03 Handlers ×2 Shorten, Redirect 04 *http.ServeMux routes the handlers → 05 *http.Server listens, shuts down README.md spec spec 03 / 37 6 components · just enough to hurt
real main.go. 01 The DI problem // why manual wiring breaks at scale 02 The landscape // Wire · Dig · Fx · do 03 Why Fx // six things you stop writing 04 Build along: 8 steps // the URL shortener, one concept at a time 05 Testing with fxtest // swap fakes, drive the lifecycle 06 Other APIs worth knowing // Supply, Error, timeouts, WithLogger 07 When NOT to use Fx // the four red flags 08 Summary & Q&A agenda.md 8 stops agenda 04 / 37 8 items
should worry you Four questions for the future you. What happens when NewShortenHandler needs a rate limiter? Every call site that constructs it has to know — the signature grows everywhere. What happens when you need a read replica and a write primary? They're both *sql.DB. How does the next caller pick which one is which? Who handles SIGTERM gracefully? You — by hand. With all the edge cases. Forever. How do you test ShortenHandler in isolation? You boot the full server, or hand-roll fakes for every layer below it. questions.md 4 issues §1 · problem 08 / 37 4 items
"Components should declare what they need. Something else should figure out how to build it." // inversion of control · DI is its most common implementation principle.md inversion of control §1 · problem 10 / 37 read-only
means three different things. Let’s be precise. 01 Dependency Injection · the pattern Components receive their dependencies as arguments instead of constructing them inside. already idiomatic Go ✓ 02 Inversion of Control · the principle Construction moves from the caller to something else. Could be main.go. Could be a framework. design principle 03 IoC Container · the implementation A library that owns the dependency graph at runtime. Resolves it. Manages lifecycle. ← Uber FX lives here → Most arguments about “DI in Go” are people defending (1) against critiques of (3). They’re talking past each other. di-layers.md terminology §1 · problem 11 / 37 3 layers · 1 talk
already doing it. It has a name. // the main.go you’ve already written cmd/server/main.go func main() { cfg := config.Load() logger := log.New() store := storage.New(cfg) h := handler.New(store, logger) srv := server.New(h, cfg) srv.Run() } // the pattern, named: Composition Root → One place where the graph is constructed. → Components stay framework-free. → Nothing magical. Read top to bottom. → ~90% of Go services do this and stop. → FX doesn’t replace the Composition Root. It scales it — for when the graph stops fitting in your head. composition-root.md the pattern, named §1 · problem 12 / 37 credit: Mark Seemann
four contenders Notable projects. PROJECT APPROACH MAINTAINED BY google/wire Code generation — emits wire_gen.go at build time. Explicit, zero runtime cost. Google uber-go/dig Reflection-based runtime DI container. Uber uber-go/fx Application framework built on top of Dig. Uber samber/do Generics-based, lightweight. Community landscape.md 4 projects §2 · landscape 14 / 37 4 rows
vs. Fx. Wire // a DI tool Code generation. Inspectable. Manual. + All wiring is generated Go — 100% inspectable + No runtime cost. No reflection. − More boilerplate; you manage provider sets manually → Better for libraries, or teams who distrust magic Fx // a DI framework Reflection. Less boilerplate. A real lifecycle. + Wiring at runtime — much less boilerplate + Built-in lifecycle wired into OS signals + First-class testing, modules, value groups → Better for full application composition // Building an application? Fx wins. Building a library? Use neither. wire-vs-fx.md tradeoff §2 · landscape 14 / 37 2 cols
Why Fx: six things you stop writing. → Automatic dependency resolution — declare providers, Fx builds the graph. → Application lifecycle — OnStart / OnStop hooks wired into signals. → Graceful shutdown in reverse order — DB closes after the server, automatically. → Modular composition — fx.Module lets teams own their own slice. → Structured graph logging — fx.WithLogger prints every wiring decision at startup. The answer to “but it’s magic.” → First-class testing with fxtest. // when to reach for this Past ~20K SLOC, 3+ teams, or 10+ services — manual wiring’s growth curve crosses FX’s. Below that, stay with the Composition Root. why-fx.md section 3 of 8 §3 · why fx 15 / 37 6 items
seconds. // THE API fx.Provide takes constructor functions. fx.Invoke takes side-effect functions that consume the graph. // THE TOY A worker pool. Zero shortener noise — just the API. primer/pool.go import "go.uber.org/fx" type Pool struct{ size int } func NewPool() *Pool { return &Pool{size: 4} } func main() { fx.New( fx.Provide(NewPool), fx.Invoke(func(p *Pool) { fmt.Println("ready", p.size) }), ).Run() } primer/pool.go ⎇ primer toy §3 · primer 1/3 16 / 37 5 lines of glue
free. // THE API Take an fx.Lifecycle in your constructor. Append OnStart / OnStop hooks. // THE MAGIC .Run() wires hooks to SIGINT/SIGTERM. Stops run in reverse. primer/pool.go func NewPool(lc fx.Lifecycle) *Pool { p := &Pool{size: 4, jobs: make(chan Job, 64)} lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { for i := 0; i < p.size; i++ { go p.worker() } return nil }, OnStop: func(ctx context.Context) error { close(p.jobs) return nil }, }) return p } → Zero signal-handling code. Same pattern shuts down the shortener cleanly in Step 02. primer/pool.go ⎇ primer + lifecycle §3 · primer 2/3 17 / 37 OnStart · OnStop
solves a real problem in the shortener. Watch main.go shrink as the app grows. // Steps 1–4: common patterns. Steps 5–8: advanced. part-04-build-along.md section 4 of 8 part four 19 / 37 8 steps
PROBLEM main.go is a list of constructor calls in a fragile order. // CONCEPT Register the constructors. Let Fx figure out the order. cmd/server/main.go func main() { fx.New( fx.Provide( config.Load, zap.NewProduction, storage.NewMemoryStore, handler.NewShortenHandler, handler.NewRedirectHandler, NewServer, ), fx.Invoke(func(*http.Server) {}), ).Run() } step-01.go ⎇ step-01 Go §4 · step 01 / 08 20 / 37 Provide builds · Invoke uses
and stop. // PROBLEM We called .Run() but nothing starts. SIGTERM goes nowhere. // CONCEPT Append start & stop hooks to the Lifecycle. Fx runs them in order — and reverse order. internal/server/server.go func NewServer(lc fx.Lifecycle, mux *http.ServeMux, cfg Config, log *zap.Logger) *http.Server { srv := &http.Server{Addr: cfg.Addr, Handler: mux} lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { go srv.ListenAndServe() return nil }, OnStop: func(ctx context.Context) error { return srv.Shutdown(ctx) }, }) return srv } → Zero signal-handling code. Stop hooks run in reverse order — DB closes after the server. step-02.go ⎇ step-02 Go §4 · step 02 / 08 21 / 37 OnStart · OnStop
// PROBLEM NewShortenHandler has 3 params. With a rate limiter and a cache, it'll have 6. // CONCEPT Embed fx.In in a params struct. Fx populates the fields. internal/handler/shorten.go type ShortenParams struct { fx.In Store Store Logger *zap.Logger Config Config } func NewShortenHandler(p ShortenParams) *ShortenHandler { return &ShortenHandler{ store: p.Store, logger: p.Logger, cfg: p.Config, } } → fx.Out does the same for multiple return values — one constructor produces many things. step-03.go ⎇ step-03 Go §4 · step 03 / 08 23 / 37 fx.In · fx.Out
// PROBLEM Handlers should depend on Store, not *MemoryStore. But Fx matches types exactly. // CONCEPT Annotate at the call site. fx.As stores the struct in the container as its interface. cmd/server/main.go type Store interface { Save(slug, url string) Lookup(slug string) (string, bool) } // Compile-time check: var _ Store = (*MemoryStore)(nil) fx.Provide( fx.Annotate( storage.NewMemoryStore, // returns *MemoryStore fx.As(new(Store)), // stored as Store ), ) → Read new(Store) as “the type Store” — it’s how Go passes a type to FX. step-04.go ⎇ step-04 Go §4 · step 04 / 08 25 / 37 fx.Annotate · fx.As
// the file everyone edits The router, before value groups. // THE SMELL Every new route means another constructor param and another mux.Handle line in the same file. // WHAT WE WANT A handler should ship itself — without anyone editing the mux. internal/server/mux.go func NewMux( shorten *handler.ShortenHandler, redirect *handler.RedirectHandler, stats *handler.StatsHandler, // added last week health *handler.HealthHandler, // added yesterday // ...and the next one, and the next one ) *http.ServeMux { mux := http.NewServeMux() mux.Handle("POST /shorten", shorten) mux.Handle("GET /{slug}", redirect) mux.Handle("GET /stats", stats) mux.Handle("GET /health", health) return mux } internal/server/mux.go ⎇ main before step 05 §4 · setup for step 05 26 / 37 N routes · N params
problem: main.go still knows about every component. // concept: each team ships a self- contained module. // storage/module.go storage/module.go var Module = fx.Module("storage", fx.Provide( fx.Annotate(NewMemoryStore, fx.As(new(Store))), ), ) // main.go — just composition cmd/server/main.go func main() { fx.New( fx.Provide( config.Load, zap.NewProduction, ), storage.Module, api.Module, ).Run() } → main.go is now a one-line composition. Teams ship features without touching it. step-07.go ⎇ step-07 Go §4 · step 07 / 08 29 / 37 scoped · ownership
api components to log with "module": "api" — without passing a different logger to each constructor. // CONCEPT Wrap a dependency within the scope of a module. Outside it, the original is untouched. api/module.go var Module = fx.Module("api", fx.Decorate(func(log *zap.Logger) *zap.Logger { return log.With(zap.String("module", "api")) }), // every provider below gets the decorated logger fx.Provide( NewShortenHandler, NewRedirectHandler, NewMux, NewServer, ), ) // what you’d see in the logs at runtime — scope made concrete: {"msg":"shortened url", "module":"api", ...} // inside api → decorated {"msg":"saved record", "module":"", ...} // in storage → original step-08.go ⎇ step-08 Go §4 · step 08 / 08 30 / 37 scoped, not global
meet Other APIs worth knowing. fx.Supply Inject a value that already exists. Parsed flags, a *sql.DB you built outside Fx, anything pre- constructed. No constructor needed. fx.Error Fail startup intentionally. Useful in tests to assert bad config refuses to boot. app.Err() goes non-nil. StartTimeout / StopTimeout Bound hook duration. How long Fx waits for slow OnStart / OnStop hooks before giving up. group:",flatten" Contribute a slice to a group. When your producer returns []Route and you want the whole slice flattened into the group at once. fx.WithLogger See the graph at startup. Logs every provider, every order, every failure. Life- saving when debugging "missing type" errors. also fx.Replace Replace a real component in a test, globally. Pairs beautifully with fxtest. more-apis.md section 6 of 8 §6 · more apis 33 / 37 6 tools
Step back when… 01 · libraries You're inside a reusable package. Consumers have their own DI story. Accept dependencies as plain constructor args. 02 · small programs It's a CLI, a script, a tiny lambda. Manual wiring is already trivial. Fx adds startup overhead and cognitive cost. 03 · team familiarity Your team is new to DI. Start with manual wiring or Wire — generated, readable. Graduate to Fx later. 04 · cold-start sensitive Startup time is on the SLA. Reflection at startup is fast — but not zero. If milliseconds matter, prefer Wire. red-flags.md 4 flags §7 · when not to 35 / 37 4 flags
DI is about declaring needs, not managing construction. 02 fx.Annotate is the bridge between plain Go and the Fx container — use it to keep your constructors framework-free. 03 fx.Module is the scaling primitive — teams own their own slice. 04 Not in libraries. Not in scripts. Everything in between — it earns its keep. // every step of this talk — on a branch $ git clone https://github.com/limistah/go-url-shortener Thanks. Questions? takeaways.md fin fin 37 / 37 • ready for q&a