Register the constructors — fx does the wiring
— building the graph from types, lifecycle included
main() is turning into an assembly manualmain()// Manual DI: wiring by hand in main() cfg, _ := config.New() db, _ := database.New(cfg) authorRepo := author.NewMysqlAuthorRepository(db) articleRepo := article.NewMysqlArticleRepository(db) timeout := time.Duration(cfg.Context.Timeout) * time.Second usecase := article.NewArticleUsecase(articleRepo, authorRepo, timeout) e := NewEcho() article.NewArticleHandler(e, usecase) e.Start(cfg.Server.Address)
Just register constructor functions. Order is not our concern.
fx.Provide(
config.New,
database.New,
NewArticleUsecase,
)
Reads each constructor's parameter types and return types, builds the graph, and calls them in the right order.
New(cfg *config.Config) (*sql.DB, error) ↑ needs ↑ provides
database.New requires *config.Config, so config.New — which returns that type — runs first. Dependency cycles are reported as an error at startup.
go get go.uber.org/fx
| Ch | Topic | Key methods |
|---|---|---|
| 01 | fx basics — register, run, lifecycle | Provide Invoke Supply Lifecycle |
| 02 | Scaling patterns — tools for a growing app | Module Decorate Annotate Private |
| 03 | Testing strategy | fxtest Replace Populate |
| 04 | Wrap-up — when to use it, when not to | decision guide |
fx has a lot of methods and they blur together. We open the map on the next slide first, then fill it in chapter by chapter with real examples.
Register, run, and hand off the lifecycle.
These four — Provide · Invoke · Supply · Lifecycle — are enough to run a real app.
| Method | Group | Role |
|---|---|---|
fx.New | basics | Creates the app container — collects Options into a graph |
fx.Provide | basics | Lazy registration, keyed by return type |
fx.Invoke | basics | Eager execution. The entry point for side effects |
fx.Supply | basics | Registers a ready-made value, no constructor |
fx.Lifecycle | lifecycle | OnStart / OnStop hooks |
| Method | Group | Role | Since |
|---|---|---|---|
fx.Module | scaling | Grouping by domain | v1.17+ |
fx.Decorate | scaling | Wrapping a dependency (logging · caching · metrics) | v1.18+ |
fx.Annotate | scaling | Attaches name / group / As to a constructor | — |
fx.In / fx.Out | scaling | Bundles params or results into a struct for tag matching | — |
fx.Private | scaling | Encapsulates dependencies inside a Module | v1.20+ |
fxtest.New | testing | A test-only app | — |
fx.Replace | testing | Swaps an existing Provide for a mock | — |
fx.Populate | testing | Extracts an internal instance into a variable | — |
fx.Optionapp := fx.New( // ← collects Options into an app fx.Provide(...), // fx.Option fx.Supply(...), // fx.Option UserModule, // fx.Option (returned by fx.Module) fx.Decorate(...), // fx.Option fx.Invoke(...), // fx.Option )
Provide · Invoke · Supply · Module · Decorate · Replace · Populate
→ they go in as arguments to fx.New
Annotate · ResultTags · ParamTags
→ they are used inside Provide · Supply
Provide only registers — it defers executionapp := fxtest.New(t, // Provide: registers constructors only. Nothing runs yet. fx.Provide( NewLogger, // returns Logger NewMysqlUserRepo, // returns UserRepository (needs Logger) NewUserService, // returns *UserService (needs UserRepository) ), // Invoke: this is where the graph finally gets assembled fx.Invoke(func(s *UserService) { svc = s }), )
NewUserService first changes nothing. fx sorts by type.
Invoke, nothing happens at allfx.Provide — lazyOnly puts it on the graph. No side effects.
Most NewXxx constructors
fx.Invoke — eagerAlways runs at startup. The types it asks for become the starting point of graph assembly.
Booting the server · registering routes · registering hooks
// An app with Provides but no Invoke → not a single constructor runs. The app starts quietly and exits quietly.
Supplytype Config struct { DBHost string DBPort int } cfg := &Config{DBHost: "localhost", DBPort: 3306} app := fxtest.New(t, fx.Supply(cfg), // register the value itself, no constructor fx.Invoke(func(c *Config) { // c.DBHost == "localhost" }), )
Provide just adds noise like func() *Config { return cfg }.
Provide registers a constructor function; Supply registers an already-built value.
// cmd/main.go app := fx.New( fx.Provide( config.New, // *config.Config database.New, // *sql.DB (needs *config.Config) NewEcho, // *echo.Echo // an anonymous function is a constructor too — only the types matter func(cfg *config.Config) time.Duration { return time.Duration(cfg.Context.Timeout) * time.Second }, article.NewArticleHandler, // needs Echo, UseCase article.NewArticleUsecase, // needs Repo, AuthorRepo, Duration article.NewMysqlArticleRepository, // needs DB author.NewMysqlAuthorRepository, // needs DB ), fx.Invoke(registerHooks), // start the server )
Arrows are the direction of dependency. fx builds this graph with no declaration at all — parameter and return types are the whole input. If a cycle appears, it tells you at startup exactly where things are tangled.
func registerHooks(lifecycle fx.Lifecycle, e *echo.Echo, cfg *config.Config) { lifecycle.Append(fx.Hook{ OnStart: func(context.Context) error { fmt.Println("Starting server") go e.Start(cfg.Server.Address) return nil }, OnStop: func(context.Context) error { fmt.Println("Stopping server") return nil }, }) }
fx.Invoke(registerHooks).
fx.Invoke function runs right away. All it does here is register hooks on the Lifecycle. (fx.Populate is filled in at this point too)OnStart hooks run — the server actually boots hereOnStop hooks run — graceful shutdownStart.
Once an app grows, the Provide list runs to dozens of lines.
Tools to group (Module), wrap (Decorate), distinguish (name/group), and hide (Private).
var UserModule = fx.Module("user", fx.Provide( NewMysqlUserRepo, NewUserService, ), ) var OrderModule = fx.Module("order", fx.Provide( NewMysqlOrderRepo, NewOrderService, ), )
// In practice: each domain package exposes a Module // and main composes them app := fx.New( fx.Provide(config.New, database.New, NewEcho), article.Module, // article domain author.Module, // author domain payment.Module, // payment domain fx.Invoke(registerHooks), )
Module only groups things under a name. The Provides inside are still exposed to the global graph. Hiding them takes fx.Private, on slide 22.
// Logging decorator: wraps the existing UserRepository type loggingUserRepo struct { inner UserRepository logger Logger } func (r *loggingUserRepo) FindByID(id int) string { r.calls = append(r.calls, fmt.Sprintf("FindByID(%d)", id)) return r.inner.FindByID(id) // call the original } app := fxtest.New(t, fx.Provide(NewLogger, NewMysqlUserRepo, NewUserService), fx.Decorate(func(repo UserRepository, logger Logger) UserRepository { return &loggingUserRepo{inner: repo, logger: logger} }), )
UserService receives the wrapper without a single line changed.
fx.Provide( fx.Annotate(NewReadDB, fx.ResultTags(`name:"readDB"`)), fx.Annotate(NewWriteDB, fx.ResultTags(`name:"writeDB"`)), NewDBService, )
fx.In structtype DBParams struct { fx.In ReadDB *DBConnection `name:"readDB"` WriteDB *DBConnection `name:"writeDB"` } func NewDBService(p DBParams) *DBService { return &DBService{ readDB: p.ReadDB, writeDB: p.WriteDB, } }
fx.Out to bundle return values.)
fx.Provide( fx.Annotate(func() Notifier { return &EmailNotifier{} }, fx.ResultTags(`group:"notifiers"`)), fx.Annotate(func() Notifier { return &SlackNotifier{} }, fx.ResultTags(`group:"notifiers"`)), fx.Annotate(func() Notifier { return &SMSNotifier{} }, fx.ResultTags(`group:"notifiers"`)), NewNotifierService, )
type NotifierParams struct { fx.In Notifiers []Notifier `group:"notifiers"` } func NewNotifierService(p NotifierParams) *NotifierService { return &NotifierService{notifiers: p.Notifiers} }
| Pattern | Use for | Receiving side | As implementations grow |
|---|---|---|---|
name:"X" | Identifying one type individually | single field | receiving code changes too |
group:"Y" | Collecting one type or interface | slice field | receiving code stays put |
PrivateModule := fx.Module("private", fx.Provide( newInternalDB, fx.Private, // ← makes this whole Provide group // Module-internal only ), fx.Provide(newModuleService), // this one is exposed ) // What if something outside asks for *internalDB? leakApp := fx.New( PrivateModule, fx.Populate(&leaked), fx.NopLogger, ) // leakApp.Err() != nil → error while building the graph
*internalDB can only be injected into newModuleService, inside the same Module.
How to boot an fx-wired app inside a test, how to slot mocks in, and how to pull assembled instances back out.
app := fxtest.New(t, fx.Provide(NewLogger, NewMysqlUserRepo, NewUserService), fx.Invoke(func(svc *UserService) { /* ... */ }), ) defer app.RequireStop() app.RequireStart()
t, helps with cleanup, and folds fx logs into the test output.
RequireStart/RequireStop abort the test immediately on failure.
app := fxtest.New(t, fx.Provide(NewLogger, NewMysqlUserRepo, NewUserService), // replace the real UserRepository with a mock fx.Replace(fx.Annotate(&mockUserRepo{}, fx.As(new(UserRepository)))), fx.Invoke(func(svc *UserService) { result := svc.repo.FindByID(1) // result == "mock-user-1" }), )
fx.As is needed
fx matches by type. The type of &mockUserRepo{} is *mockUserRepo, not UserRepository.
You have to register it as the interface type with fx.As(new(UserRepository)) for it to replace the existing Provide.
// Option 1: capture with an Invoke closure var svc1 *UserService app1 := fxtest.New(t, fx.Provide(NewLogger, NewMysqlUserRepo, NewUserService), fx.Invoke(func(s *UserService) { svc1 = s }), )
// Option 2: extract directly with Populate var svc2 *UserService app2 := fxtest.New(t, fx.Provide(NewLogger, NewMysqlUserRepo, NewUserService), fx.Populate(&svc2), ) // several at once works too fx.Populate(&svc, &logger)
| Aspect | fx.Invoke | fx.Populate |
|---|---|---|
| Nature | Runs a function | Fills a variable |
| What you pass | a function (closure) | a pointer |
| Pick it when | you'll call and assert afterwards | pulling it out is the point |
What to reach for when — and how much you should not hand over to fx.
| What you want | Method | Remember |
|---|---|---|
| Register a dependency (lazy) | fx.Provide | does not run yet |
| Run at startup (boot the server) | fx.Invoke | side effect → Invoke |
| Inject a ready-made value or config | fx.Supply | constants · config |
| Start/stop hooks (graceful shutdown) | fx.Lifecycle | Append inside Invoke |
| Group dependencies by domain | fx.Module | split a bloated Provide |
| Add logging or caching to a dependency | fx.Decorate | wrap, no edits |
| Identify one type individually (read/write) | Annotate + name: | single field |
| Inject one interface's set of impls | group: | slice on the receiver |
| Hide a Module's internals | fx.Private | isolate infra handles |
| Inject a mock in a test | fx.Replace | match iface via fx.As |
| Pull an instance out in a test | fx.Populate | Invoke if asserting |
Components that need lifecycle management
DB connections · HTTP servers · external clients — things whose startup and teardown pair up and that are shared in many places
Plain value objects and utilities
Putting something with no construction cost and no lifecycle on the graph buys nothing and only adds indirection
Provide is lazy — it registers and defers execution. The graph is assembled only when something asks for a type, and fx.Invoke is that starting point. → Slide 09 · 10Provide registers a constructor function and fx wires it in by return type. Supply registers an already-built value as is — right for config structs and constants with no construction logic. → Slide 11Invoke body runs immediately at fx.New(), but all it does there is register hooks; the OnStart body runs at app.Start(ctx). → Slide 15fx.Private. Module only groups under a name — the Provides inside stay exposed to the global graph. Put fx.Private in that same Provide group to hide them. → Slide 17 · 22fx.Decorate. Take the original as a parameter and return a wrapped value of the same type, and fx swaps that node in the graph. Whatever injects it receives the wrapper with no code change. → Slide 18name:"readDB" using fx.Annotate + fx.ResultTags, then put the same tags on the fields of a receiving struct that embeds fx.In. → Slide 19group:"notifiers" and receive a []Notifier slice; the receiving code stays put. → Slide 20 · 21&mockUserRepo{} is *mockUserRepo, not the interface. Wrap it in fx.Annotate(..., fx.As(new(UserRepository))) for the replacement to take. → Slide 25Populate is a convenience function built on Invoke. The difference is intent: if capturing is the whole goal use Populate; if you will call and assert afterwards use Invoke. → Slide 26Provide is lazy registration, Invoke is eager execution. Without Invoke, nothing happensLifecycle has two stages — New registers the hooks, Start runs their bodiesModule, wrap with Decorate, distinguish with name/group, hide with Privatefxtest + Replace (don't forget fx.As) + Populate