As Go services grow beyond a handful of packages, the main() function becomes a tangled mess of initialization: create the database pool, pass it to the user repository, pass that to the auth service, pass that to the HTTP handler, wire up the health check, start the metrics server. Each new dependency means threading another parameter through the chain. Uber’s Fx framework solves this by providing a dependency injection container that automatically wires providers to consumers based on their types.
The platform uses Fx to compose its backend services into a modular application. Each domain (auth, billing, search, admin) registers its dependencies as Fx providers, and the framework resolves the full dependency graph at startup. If a required dependency is missing or a circular dependency exists, Fx fails fast with a clear error message rather than panicking at runtime deep in a call stack.
This approach trades a small amount of explicit wiring for significant gains in modularity: adding a new service means writing a provider function and registering it with Fx, not modifying a dozen initialization sites. It also makes testing straightforward—swap a real provider for a mock by replacing one module in the Fx app definition.
An Fx provider is a function that takes its dependencies as parameters and returns the thing it constructs. Fx inspects the function signature to determine what it needs and what it produces. For example, func NewUserRepo(db *sql.DB) *UserRepo tells Fx: “I need a *sql.DB and I produce a *UserRepo.” Fx matches types to wire everything together.
Fx modules group related providers into a single unit. A billing module might bundle the Stripe client, the subscription repository, and the billing service. Modules can be included or excluded from the app definition, making it easy to run a subset of the system for testing or to build different deployment configurations from the same codebase.
Fx manages the full lifecycle of the application: it starts components in dependency order and stops them in reverse order. Components can register OnStart and OnStop hooks to handle initialization (like opening database connections or starting HTTP servers) and cleanup (like draining connections or flushing buffers). This eliminates ad-hoc shutdown coordination.
Unlike name-based DI containers (common in Java), Fx resolves dependencies purely by Go types. This means the compiler catches many wiring errors: if two providers produce the same type, Fx reports the conflict at startup. For cases where you need multiple instances of the same type (e.g., a read-only DB and a read-write DB), Fx provides named values via fx.Annotate.
The entry point creates an Fx app by composing modules. Each module contributes providers, lifecycle hooks, and invocations (functions that trigger side effects like starting an HTTP server). Fx builds the dependency graph, validates it, and then starts all components in the correct order.
func main() {
fx.New(
fx.Provide(newConfig),
fx.Provide(newLogger),
// Domain modules
DatabaseModule,
AuthModule,
BillingModule,
SearchModule,
// HTTP server (consumes handlers from modules)
HTTPModule,
// Start the server
fx.Invoke(startServer),
).Run()
}
var DatabaseModule = fx.Module("database",
fx.Provide(newDatabasePool),
fx.Provide(newMigrator),
)
var AuthModule = fx.Module("auth",
fx.Provide(newUserRepo),
fx.Provide(newAuthService),
fx.Provide(newAuthHandler),
)
Each provider is a standard Go constructor. Fx reads the parameter types to determine dependencies and the return types to know what the provider produces. Providers can return errors—if a constructor fails, Fx aborts startup with the error context.
func newDatabasePool(cfg *Config, lc fx.Lifecycle) (*pgxpool.Pool, error) {
pool, err := pgxpool.New(context.Background(), cfg.DatabaseURL)
if err != nil {
return nil, fmt.Errorf("connect to database: %w", err)
}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
return pool.Ping(ctx)
},
OnStop: func(ctx context.Context) error {
pool.Close()
return nil
},
})
return pool, nil
}
func newUserRepo(pool *pgxpool.Pool) *UserRepo {
return &UserRepo{pool: pool}
}
func newAuthService(repo *UserRepo, cfg *Config) *AuthService {
return &AuthService{
repo: repo,
jwtSecret: []byte(cfg.JWTSecret),
tokenTTL: cfg.TokenTTL,
}
}
HTTP handlers from different modules need to register their routes on a shared router. Fx’s fx.Annotate with group tags collects multiple handlers into a slice, which the HTTP module consumes to build the final router.
type RouteRegistrar interface {
RegisterRoutes(r chi.Router)
}
// In AuthModule
fx.Provide(
fx.Annotate(
newAuthHandler,
fx.As(new(RouteRegistrar)),
fx.ResultTags(`group:"routes"`),
),
)
// In BillingModule
fx.Provide(
fx.Annotate(
newBillingHandler,
fx.As(new(RouteRegistrar)),
fx.ResultTags(`group:"routes"`),
),
)
// HTTP module consumes all route registrars
type HTTPServerParams struct {
fx.In
Registrars []RouteRegistrar `group:"routes"`
Logger *zap.Logger
}
func newHTTPServer(p HTTPServerParams) *chi.Mux {
r := chi.NewRouter()
r.Use(middleware.Logger)
for _, reg := range p.Registrars {
reg.RegisterRoutes(r)
}
return r
}
In tests, you replace real modules with test doubles. Fx makes this straightforward: build a test app with the modules you want, swapping the database module for an in-memory version or the Stripe module for a mock. The type system ensures the test app’s dependency graph is still valid.
func TestAuthService(t *testing.T) {
var authSvc *AuthService
app := fxtest.New(t,
fx.Provide(testConfig),
fx.Provide(newLogger),
TestDatabaseModule, // uses testcontainers PostgreSQL
AuthModule,
fx.Populate(&authSvc),
)
app.RequireStart()
defer app.RequireStop()
token, err := authSvc.Login(ctx, "user@example.com", "password")
require.NoError(t, err)
require.NotEmpty(t, token)
}
Rather than a single global config struct, each module defines its own config type and a provider that extracts it from the environment or a config file. This keeps module boundaries clean and makes it obvious what configuration each module requires.
Fx’s fx.Decorate wraps existing providers with additional behavior. This is useful for adding instrumented database pools, traced HTTP clients, or cached repositories without modifying the original provider functions.
Feature flags or environment variables can control which modules are loaded. A worker process might skip the HTTP module entirely and only load queue consumers, while the API server skips the worker module. Both configurations share the same domain modules.
*http.Client instances) cause an Fx error. Use fx.Annotate with fx.ResultTags and fx.ParamTags to distinguish named instances.fx.Options to compose smaller groups first and test them individually before assembling the full app.