Go · Dependency Injection

Go Dependency Injection
with uber/fx

Register the constructors — fx does the wiring
— building the graph from types, lifecycle included

fx.Provide fx.Invoke fx.Module fxtest
The graph fx builds for you types only
config.New() ├─ database.New(cfg) │ ├─ NewMysqlArticleRepository(db) │ └─ NewMysqlAuthorRepository(db) │ └─ NewArticleUsecase(repo, authorRepo, timeout) │ └─ NewArticleHandler(echo, usecase) └─ NewEcho(cfg) └─ registerHooks(lifecycle, echo, cfg)
Registration order is irrelevant. fx reads only parameter and return types.
Greeting. A full tour of uber/fx today. Point at the tree on the right — that is what fx builds on its own; all we do is register constructors. Land that framing before anything else.
problemwhy DI

main() is turning into an assembly manual

  • You call every constructor in order, by hand
  • You line up parameter order yourself
  • Every new service means editing main()
  • More dependencies, and this grows fast
// 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)
the real cost Knowledge of the assembly order gets scattered across the code. That knowledge is already inside the types.
Open with "you know this code, right?" to build rapport. Getting the order wrong is a compile error, sure — but the real problem is that assembly knowledge takes up permanent residence in main.
solutionuber/fx

fx decides the order from types alone

What we do

Just register constructor functions. Order is not our concern.

fx.Provide(
    config.New,
    database.New,
    NewArticleUsecase,
)
What fx does

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
in one line 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
"It reads types through reflection" is the core idea. Frame it as a trade — you give up some compile-time safety and hand assembly knowledge to the types. That framing pays off on slide 29.
agendaagenda

What we cover today

ChTopicKey methods
01fx basics — register, run, lifecycleProvide Invoke Supply Lifecycle
02Scaling patterns — tools for a growing appModule Decorate Annotate Private
03Testing strategyfxtest Replace Populate
04Wrap-up — when to use it, when not todecision 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.

Four chapters. If time runs short, trim 03 (testing) and stay on 01 and 02. The group/Private material in 02 is something people meet late in practice, so it can slide.
01

fx Basics

Register, run, and hand off the lifecycle. These four — Provide · Invoke · Supply · Lifecycle — are enough to run a real app.

This chapter is the root of everything today. The Provide (lazy) versus Invoke (eager) distinction keeps coming back, so do not rush it.
mapcheat sheet

fx methods at a glance ① Basics

MethodGroupRole
fx.NewbasicsCreates the app container — collects Options into a graph
fx.ProvidebasicsLazy registration, keyed by return type
fx.InvokebasicsEager execution. The entry point for side effects
fx.SupplybasicsRegisters a ready-made value, no constructor
fx.LifecyclelifecycleOnStart / OnStop hooks
80% of today These five are enough to run a real app. The scaling and testing tools in the next chapters only matter once the app grows.
Do not read the table out loud. Say "just these five for now" and move on. Each one gets filled in with an example later.
mapcheat sheet

fx methods at a glance ② Scaling · Testing

MethodGroupRoleSince
fx.ModulescalingGrouping by domainv1.17+
fx.DecoratescalingWrapping a dependency (logging · caching · metrics)v1.18+
fx.AnnotatescalingAttaches name / group / As to a constructor
fx.In / fx.OutscalingBundles params or results into a struct for tag matching
fx.PrivatescalingEncapsulates dependencies inside a Modulev1.20+
fxtest.NewtestingA test-only app
fx.ReplacetestingSwaps an existing Provide for a mock
fx.PopulatetestingExtracts an internal instance into a variable
The three with version numbers (Module, Decorate, Private) are a common source of "why doesn't this work?" in the field. Add a line telling people to check their go.mod version.
structurefx.Option

Almost everything returns an fx.Option

app := 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
)
① Things that return an Option

Provide · Invoke · Supply · Module · Decorate · Replace · Populate
→ they go in as arguments to fx.New

② Things that return an Annotation

Annotate · ResultTags · ParamTags
→ they are used inside Provide · Supply

tell these two apart and the fx API turns simple all at once. You stop wondering where a thing is supposed to go.
The single most confusing point when you first read the fx docs. Once Option versus Annotation is clear, "where do I put this?" stops being a question.
basicsfx.Provide

Provide only registers — it defers execution

app := 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 }),
)
what lazy means A registered constructor runs only once something asks for its type. If nothing asks, it never runs at all.
hence order-free Listing NewUserService first changes nothing. fx sorts by type.
"I wrote a pile of Provides and nothing runs" is the classic beginner question. It leads straight into the next slide.
basicsfx.Invoke

Without Invoke, nothing happens at all

fx.Provide — lazy

Only puts it on the graph. No side effects.

Most NewXxx constructors

fx.Invoke — eager

Always runs at startup. The types it asks for become the starting point of graph assembly.

Booting the server · registering routes · registering hooks

rule of thumb Side effect → Invoke. Returns a value → Provide.
// An app with Provides but no Invoke
→ not a single constructor runs. The app starts quietly and exits quietly.
Good spot for a show of hands — "20 Provides, no Invoke, how many constructors run?" The answer is zero.
basicsfx.Supply

Values with nothing to build go through Supply

type 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"
    }),
)
when to use it Config structs and constants — values with no construction logic. Wrapping them in Provide just adds noise like func() *Config { return cfg }.
the difference, in one line Provide registers a constructor function; Supply registers an already-built value.
Supply shows up especially often in tests — build the struct by hand instead of reading a real config file.
in practicebefore → after

Manual wiring, moved over to fx

// 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
)
what changed The assembly order is gone. What remains is a list of what exists; "in what order" is something fx reads out of the types.
Powerful when placed side by side with the manual code on slide 02. Jumping back to 02 for a moment is fine.
how it worksgraph

The graph comes out of the signatures

config.New() → *Config
NewEcho() → *echo.Echo
database.New(*Config) → *sql.DB
ArticleRepo(*sql.DB)
AuthorRepo(*sql.DB)
ArticleUsecase(repo, authorRepo, timeout)
ArticleHandler(echo, usecase)
registerHookslifecycle, echo, cfg

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.

The point is that we never drew this picture. Nowhere in the code did we write this order down, and fx produced it anyway.
lifecyclefx.Lifecycle

Bind startup and shutdown as a pair

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
        },
    })
}
where it belongs Resources whose setup and teardown come in pairs — booting and stopping a server, opening and closing a DB connection. Register it with fx.Invoke(registerHooks).
Stress that graceful shutdown comes for free. OnStop hooks run in reverse registration order.
carefultiming

When exactly does the server come up?

1
fx.New() / fxtest.New()
The body of each 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)
2
app.Start(ctx)
The registered OnStart hooks run — the server actually boots here
3
app.Stop(ctx)
The registered OnStop hooks run — graceful shutdown
why Lifecycle has two stages So hooks can be registered early through Invoke while the hook body waits until Start.
The most commonly confused point in real work. "I put it in Invoke, why did it run already?" and "why hasn't it started yet?" are both answered by this table.
02

Scaling Patterns

Once an app grows, the Provide list runs to dozens of lines. Tools to group (Module), wrap (Decorate), distinguish (name/group), and hide (Private).

From here on these are things you do not need today but will certainly meet someday. Tell people not to memorize — just remember that these exist.
scalingfx.Module · v1.17+

Group things by domain

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),
)
careful 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.
"Module = encapsulation" is an easy misreading, and it is wrong. That misreading is the starting point of slide 22, so plant it here on purpose.
scalingfx.Decorate · v1.18+

Wrap it without touching the original

// 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}
    }),
)
the key Take the original as a parameter and return the same type, and fx swaps that node in the graph. UserService receives the wrapper without a single line changed.
Used for cross-cutting concerns — logging, caching, metrics. For some audiences, "think Java AOP" lands quickly.
scalingfx.Annotate + name:

Two values of the same type — read / write DB

① Register: name the constructors
fx.Provide(
    fx.Annotate(NewReadDB,
        fx.ResultTags(`name:"readDB"`)),
    fx.Annotate(NewWriteDB,
        fx.ResultTags(`name:"writeDB"`)),
    NewDBService,
)
② Receive: match with an fx.In struct
type 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,
    }
}
what fx.In is An embedded marker that lets you receive several dependencies as a single parameter struct. Each field can point at a specific instance through a tag. (Use fx.Out to bundle return values.)
Start from the fact that fx has no way to tell identical types apart. Explaining it as "we are just adding name tags" is intuitive.
scalinggroup:

Receive implementations of one interface as a set

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}
}
why this is good Adding a new implementation leaves the receiving code untouched. The standard move for collecting plugins, handlers, and external clients.
"To add one more Notifier, what do you edit?" — one Provide line. The value is that the receiving side stays put.
comparename vs group

Point at one, or take them all?

PatternUse forReceiving sideAs implementations grow
name:"X"Identifying one type individuallysingle fieldreceiving code changes too
group:"Y"Collecting one type or interfaceslice fieldreceiving code stays put
when name: fits A few instances with different roles, like read/write DBs. You need to point at each one precisely.
when group: fits A set of implementations treated identically, like Notifiers or Handlers. Naming them one by one hurts on every extension.
Remember this one slide and you will not get confused in practice: point at one → name, take them all → group.
scalingfx.Private · v1.20+

Hide a Module's internal dependencies

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
the problem it solves Stops other Modules from accidentally sharing infrastructure dependencies like DB handles and external API clients.
scope *internalDB can only be injected into newModuleService, inside the same Module.
The answer to "Module is not encapsulation," planted back on slide 17. In practice it prevents accidents where a DB connection pool gets shared unintentionally.
03

Testing Strategy

How to boot an fx-wired app inside a test, how to slot mocks in, and how to pull assembled instances back out.

Where the practical payoff of a DI framework shows up. There is one assembly point, so swapping things out is easy.
testingfxtest.New

A test-only app container

app := fxtest.New(t,
    fx.Provide(NewLogger, NewMysqlUserRepo, NewUserService),
    fx.Invoke(func(svc *UserService) { /* ... */ }),
)
defer app.RequireStop()
app.RequireStart()
why not fx.New It reports failures through t, helps with cleanup, and folds fx logs into the test output.
the Require prefix RequireStart/RequireStop abort the test immediately on failure.
Revealing here that every example so far was fxtest makes the transition natural.
testingfx.Replace + fx.As

Swapping in a mock

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"
    }),
)
why 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.
The trap fx newcomers hit most often. Use Replace without As and you get "why isn't it being replaced?"
testingPopulate vs Invoke

Pulling assembled instances out

// 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)
Aspectfx.Invokefx.Populate
NatureRuns a functionFills a variable
What you passa function (closure)a pointer
Pick it whenyou'll call and assert afterwardspulling it out is the point
Populate is a convenience function implemented on top of Invoke. Once you know they are the same underneath and differ only in intent, the choice is easy.
04

Wrap-up

What to reach for when — and how much you should not hand over to fx.

Final chapter. Leave the decision table as a takeaway and spend the time on the closing message instead.
wrap-updecision guide

What you want → the method to reach for

What you wantMethodRemember
Register a dependency (lazy)fx.Providedoes not run yet
Run at startup (boot the server)fx.Invokeside effect → Invoke
Inject a ready-made value or configfx.Supplyconstants · config
Start/stop hooks (graceful shutdown)fx.LifecycleAppend inside Invoke
Group dependencies by domainfx.Modulesplit a bloated Provide
Add logging or caching to a dependencyfx.Decoratewrap, no edits
Identify one type individually (read/write)Annotate + name:single field
Inject one interface's set of implsgroup:slice on the receiver
Hide a Module's internalsfx.Privateisolate infra handles
Inject a mock in a testfx.Replacematch iface via fx.As
Pull an instance out in a testfx.PopulateInvoke if asserting
Do not read it down the list — tell people to take it away as a reference. Instead pick two items already covered and recap those.
closingtrade-off

You don't have to manage everything with fx

Where fx shines

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

Where hand-wiring is better

Plain value objects and utilities

Putting something with no construction cost and no lifecycle on the graph buys nothing and only adds indirection

the price you pay Because it is reflection-based, you give up some compile-time type safety. In exchange you get detailed runtime errors and real productivity — whether that trade is worth it is answered by the size of your app.
Important not to end on "DI frameworks make everything better." Always add that manual wiring is clearer in a small app.
checkclick to reveal

Check yourself ①

Because 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 · 10
Provide 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 11
It looks only at each constructor's parameter and return types. Registration order is irrelevant, and a dependency cycle is reported as an error at startup. → Slide 03 · 13
In two stages. The Invoke body runs immediately at fx.New(), but all it does there is register hooks; the OnStart body runs at app.Start(ctx). → Slide 15
fx.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 · 22
Wait about three seconds before opening each answer. Q4 draws the most wrong answers, so take that one especially slowly.
checkclick to reveal

Check yourself ②

fx.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 18
Tag each constructor with something like name:"readDB" using fx.Annotate + fx.ResultTags, then put the same tags on the fields of a receiving struct that embeds fx.In. → Slide 19
They will, but it is the wrong tool — you would edit the receiving code every time an implementation is added. Register with group:"notifiers" and receive a []Notifier slice; the receiving code stays put. → Slide 20 · 21
fx matches by type, and the type of &mockUserRepo{} is *mockUserRepo, not the interface. Wrap it in fx.Annotate(..., fx.As(new(UserRepository))) for the replacement to take. → Slide 25
They are the same underneath — Populate 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 26
Q9 is a trap people really do hit, so asking "anyone been burned by this?" usually gets a reaction.
endthank you

Today in summary

  • fx builds the dependency graph from nothing but constructors' parameter and return types — assembly order disappears from the code
  • Provide is lazy registration, Invoke is eager execution. Without Invoke, nothing happens
  • Lifecycle has two stages — New registers the hooks, Start runs their bodies
  • As the app grows: group with Module, wrap with Decorate, distinguish with name/group, hide with Private
  • Testing is fxtest + Replace (don't forget fx.As) + Populate
  • Don't manage everything with fx. It shines brightest on components that have a lifecycle
per-method examples github.com/kenshin579/tutorials-go
golang/third-party/fx
real-world use (Clean Architecture) github.com/kenshin579/tutorials-go
project-layout/go-clean-arch-v2
Take Q&A with the repository addresses left on screen. If time remains, go back to the map on slide 06 and touch on the methods you could not cover.
Speaker notes
← Article uber/fx · Go DI
01 / 32

Shortcuts

→ · Space · PgDn
Next slide
← · PgUp
Previous slide
Home · End
First · Last
O
Slide overview
N
Speaker notes
T
Light / dark theme
F
Fullscreen
Esc
Close