Skip to main content

samber/do vs uber/fx

Verdict: pick uber/fx if you're building a large, modular application that benefits from its batteries-included lifecycle framework and are comfortable with runtime reflection and its dependency footprint (dig, zap, multierr). Pick samber/do if you want compile-time-checked dependency resolution through generics, built-in health checks, and zero external dependencies, without giving up lifecycle management.

At a glance

samber/douber/fx
Resolution mechanismGo 1.18+ genericsReflection (via uber/dig)
Type safetyCompile-timeRuntime
External dependenciesNonedig, zap, multierr
Lifecycle hooksHookBeforeRegistration, HookAfterInvocation, etc.fx.Lifecycle (OnStart/OnStop)
Health checksBuilt-in Healthchecker interface, parallel with timeoutsNot built-in — implement your own
Graceful shutdownBuilt-in, dependency-aware, reverse orderVia fx.Lifecycle OnStop hooks
Modules / scopesScope tree with visibility rulesfx.Module
Struct-tag injectionOptional, opt-in (do.InvokeStruct)fx.In / fx.Out (common pattern)
Debugging toolsScope tree, dependency graph, Web UIfx.WithLogger, DOT graph export
Learning curveLow — mirrors constructor injection you'd write by handModerate — own vocabulary (fx.Provide, fx.Invoke, fx.Module, annotations)

API style

uber/fx wraps uber/dig and adds an application container (fx.New) that owns startup and shutdown. Providers are functions registered with fx.Provide, wired together by reflecting on their argument and return types:

app := fx.New(
fx.Provide(NewDatabase),
fx.Provide(NewUserService),
fx.Invoke(func(s *UserService) {}),
)
app.Run()

samber/do uses an explicit injector value and generic type parameters instead of an implicit global app:

injector := do.New()

do.Provide(injector, NewDatabase)
do.Provide(injector, NewUserService)

service := do.MustInvoke[*UserService](injector)

Both APIs use ordinary constructor functions. The difference is when type mismatches surface: with fx, a missing or ambiguous dependency fails at app.Run() time; with do, most of the same mistakes are Go compiler errors because Invoke[T] and Provide[T] are parameterized by the concrete type.

Type safety and performance

fx resolves the dependency graph via reflection on every fx.New() call, walking provider signatures to build the graph. samber/do builds the same kind of graph, but service identity and resolution go through generic type parameters resolved at compile time — there's no reflect.Type walk on the hot path for Invoke/Provide. For most applications the difference in raw invocation speed doesn't dominate startup time, but the compile-time checking catches wiring mistakes before code ships instead of at runtime app.Run().

Lifecycle and health checks

This is where the libraries diverge most in scope. fx.Lifecycle gives you OnStart(ctx) / OnStop(ctx) hooks per component, run in dependency order — good for HTTP servers, background workers, and connection pools. It does not include a health-check abstraction; teams typically build their own on top of OnStart.

samber/do ships both: lifecycle hooks similar in spirit to fx.Lifecycle, plus a first-class Healthchecker interface with configurable parallelism and timeouts, and a Shutdowner interface for graceful, dependency-aware shutdown out of the box.

Testing

Both libraries support swapping real dependencies for test doubles. fx uses fxtest.New and can override providers with fx.Replace or fx.Decorate. samber/do uses Clone() to snapshot an injector's registrations and do.Override to swap a specific service — no separate test-only package required.

Choose uber/fx if:

  • You're already invested in the Uber Go ecosystem (zap, dig) and want a full application framework, not just a container.
  • You need declarative module composition (fx.Module) across many teams contributing to the same binary.
  • Reflection-based wiring and its associated dependencies are an acceptable tradeoff for your team.

Choose samber/do if:

  • You want compile-time type checking on service resolution without adopting a full application framework.
  • You need built-in health checks and graceful shutdown without wiring your own on top of lifecycle hooks.
  • Zero non-stdlib dependencies matters (binary size, supply-chain surface, vendoring simplicity).

Already using uber/fx (or its underlying uber/dig) and considering a switch? See the migration guide from Uber Dig — the same constructor-based patterns apply to fx providers.