Go Dependency Injection in Practice: Build Modular Application Architecture with Uber Fx Using 5 Core Patterns
Go Dependency Injection: Why Are You Still Manually Wiring Dependencies in 2026?
Still creating dozens of dependencies manually in main()? Changing one constructor requires modifying ten call sites? Replacing dependencies for testing means changing lots of code? Dependency Injection (DI) is the core pattern for solving these problems. In 2026, Uber Fx has become the most mature DI framework in the Go ecosystem, used at scale by Uber, Lyft, and others. Fx uses three core concepts — Provider + Invoker + Lifecycle — to achieve declarative dependency assembly → automatic dependency graph construction → graceful startup/shutdown management.
This article walks through 5 core patterns, covering the full pipeline from module definition → dependency injection → lifecycle management → optional dependencies → test integration.
Core Concepts
| Concept | Description |
|---|---|
| Provider | Constructor function that provides dependencies |
| Invoker | Function that consumes dependencies |
| Module | Logical grouping of Providers and Invokers |
| Lifecycle | Application startup/shutdown lifecycle hooks |
| Value Group | Collection of multiple dependencies of the same type |
| Named | Named dependencies to distinguish different instances of the same type |
| Optional | Optional dependency that doesn't error when missing |
| Decorate | Decorator to replace or wrap existing dependencies |
Problem Analysis: 5 Challenges in Go Dependency Management
- Manual wiring: Dozens of NewXxx() calls in main(), order-sensitive
- Circular dependencies: A depends on B, B depends on A, compilation error
- Lifecycle management: Graceful shutdown of database connections and HTTP servers
- Testing difficulty: Replacing dependencies requires modifying lots of code
- Module reuse: Different services sharing the same infrastructure modules
Step-by-Step: 5 Core Uber Fx Patterns
Pattern 1: Fx Application Basics and Module Definition
// cmd/server/main.go
package main
import (
"go.uber.org/fx"
"github.com/example/myapp/internal/config"
"github.com/example/myapp/internal/database"
"github.com/example/myapp/internal/httpserver"
"github.com/example/myapp/internal/repository"
"github.com/example/myapp/internal/service"
)
func main() {
app := fx.New(
fx.Provide(
config.NewConfig,
database.NewPostgresDB,
repository.NewUserRepository,
repository.NewOrderRepository,
service.NewUserService,
service.NewOrderService,
),
fx.Invoke(
httpserver.RegisterRoutes,
),
)
app.Run()
}
Pattern 2: Provider Dependency Injection and Constructor Pattern
// internal/database/postgres.go
package database
import (
"context"
"fmt"
"time"
"github.com/example/myapp/internal/config"
"github.com/jackc/pgx/v5/pgxpool"
"go.uber.org/fx"
)
func NewPostgresDB(lc fx.Lifecycle, cfg *config.Config) (*pgxpool.Pool, error) {
dsn := fmt.Sprintf(
"postgres://%s:%s@%s:%d/%s?sslmode=%s",
cfg.Database.User, cfg.Database.Password,
cfg.Database.Host, cfg.Database.Port,
cfg.Database.DBName, cfg.Database.SSLMode,
)
poolCfg, err := pgxpool.ParseConfig(dsn)
if err != nil {
return nil, fmt.Errorf("failed to parse database config: %w", err)
}
poolCfg.MaxConns = 25
poolCfg.MinConns = 5
poolCfg.MaxConnIdleTime = 5 * time.Minute
pool, err := pgxpool.NewWithConfig(context.Background(), poolCfg)
if err != nil {
return nil, fmt.Errorf("failed to create connection pool: %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
}
Pattern 3: Lifecycle Management and Graceful Shutdown
// internal/httpserver/server.go
package httpserver
import (
"context"
"fmt"
"net/http"
"time"
"go.uber.org/fx"
)
func NewHTTPServer(lc fx.Lifecycle, mux *http.ServeMux, cfg *config.Config) *http.Server {
srv := &http.Server{
Addr: fmt.Sprintf(":%d", cfg.Server.Port),
Handler: mux,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
go srv.ListenAndServe()
return nil
},
OnStop: func(ctx context.Context) error {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
return srv.Shutdown(shutdownCtx)
},
})
return srv
}
Pattern 4: Value Group and Named Dependencies
// internal/middleware/module.go
package middleware
import (
"net/http"
"go.uber.org/fx"
)
type Middleware func(http.Handler) http.Handler
var Module = fx.Module("middleware",
fx.Provide(
fx.Annotate(NewLoggingMiddleware, fx.ResultTags(`group:"middlewares"`)),
fx.Annotate(NewAuthMiddleware, fx.ResultTags(`group:"middlewares"`)),
fx.Annotate(NewCORSMiddleware, fx.ResultTags(`group:"middlewares"`)),
fx.Annotate(NewRateLimitMiddleware, fx.ResultTags(`group:"middlewares"`)),
),
fx.Invoke(ChainMiddlewares),
)
func ChainMiddlewares(mux *http.ServeMux, middlewares []Middleware) {
var handler http.Handler = mux
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
mux.Handle("/", handler)
}
Pattern 5: Fx Test Integration
// internal/service/user_test.go
package service_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"go.uber.org/fx/fxtest"
"github.com/example/myapp/internal/repository"
"github.com/example/myapp/internal/service"
)
type MockUserRepository struct {
mock.Mock
}
func (m *MockUserRepository) GetByID(ctx context.Context, id int64) (*repository.User, error) {
args := m.Called(ctx, id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*repository.User), args.Error(1)
}
func TestUserService_GetUser(t *testing.T) {
mockRepo := new(MockUserRepository)
mockRepo.On("GetByID", mock.Anything, int64(1)).Return(&repository.User{
ID: 1, Username: "testuser", Email: "test@example.com",
}, nil)
svc := service.NewUserService(mockRepo)
user, err := svc.GetUser(context.Background(), 1)
assert.NoError(t, err)
assert.Equal(t, "testuser", user.Username)
}
Pitfall Guide
Pitfall 1: Circular Dependencies
// ❌ Wrong: A depends on B, B depends on A
fx.Provide(NewA, NewB) // circular dependency
// ✅ Correct: Extract shared dependency to C
fx.Provide(NewC, NewA, NewB) // A needs C, B needs C
Pitfall 2: Forgetting to Register Provider
// ❌ Wrong: Using unregistered dependency
fx.Invoke(func(svc *service.OrderService) { ... })
// ✅ Correct: Ensure all dependencies are registered
fx.Provide(service.NewOrderService),
fx.Invoke(func(svc *service.OrderService) { ... }),
Pitfall 3: Blocking Lifecycle Hook
// ❌ Wrong: Blocking in OnStart
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
srv.ListenAndServe() // blocks!
return nil
},
})
// ✅ Correct: Start in goroutine
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
go srv.ListenAndServe()
return nil
},
})
Pitfall 4: Value Group Type Mismatch
// ❌ Wrong: Inconsistent group element types
fx.Annotate(NewLogMiddleware, fx.ResultTags(`group:"middlewares"`)) // Middleware
fx.Annotate(NewAuthHandler, fx.ResultTags(`group:"middlewares"`)) // http.Handler
// ✅ Correct: Ensure consistent group element types
fx.Annotate(NewLogMiddleware, fx.ResultTags(`group:"middlewares"`)) // Middleware
fx.Annotate(NewAuthMiddleware, fx.ResultTags(`group:"middlewares"`)) // Middleware
Pitfall 5: Not Using fxtest in Tests
// ❌ Wrong: Manual Fx app for testing
app := fx.New(...)
// ✅ Correct: Use fxtest for automatic verification
app := fxtest.New(t, ...)
app.RequireStart()
defer app.RequireStop()
Error Troubleshooting
| # | Error Message | Cause | Solution |
|---|---|---|---|
| 1 | missing type: *service.UserService |
Provider not registered | Add fx.Provide |
| 2 | cycle detected |
Circular dependency | Extract shared dependency |
| 3 | cannot provide function returns |
Constructor signature error | Ensure dependency type is returned |
| 4 | OnStart hook timed out |
Startup timeout | Check if hook is blocking |
| 5 | already providing type |
Duplicate Provider registration | Use fx.Named to distinguish |
| 6 | no value found for group |
Empty Value Group | Check Group tags and Providers |
| 7 | optional dependency not found |
Missing optional dependency | Use fx.Annotate with Optional |
| 8 | invalid Annotate usage |
Annotation usage error | Check fx.Annotate parameters |
| 9 | context cancelled |
Operation during shutdown | Check OnStop Hook |
| 10 | failed to build dependency graph |
Dependency graph build failure | Check all Providers and Invokes |
Advanced Optimization
- Modular organization: Use fx.Module to group Providers and Invokers by business domain
- Decorator pattern: Use fx.Decorate to replace test environment dependencies
- Conditional injection: Use fx.Annotate with fx.OptionalTags for conditional dependencies
- Dependency replacement: Use fx.Replace to swap specific dependencies in tests
- Custom logging: Use fx.WithLogger to customize Fx log output format
Comparison
| Dimension | Uber Fx | Wire | Dig | google/wire |
|---|---|---|---|---|
| Injection method | Runtime | Compile-time | Runtime | Compile-time |
| Type safety | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| Lifecycle | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐ | ⭐⭐ |
| Value Group | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| Test friendliness | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| Debug info | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| Learning curve | Medium | High | Low | High |
Summary: Uber Fx evolves Go dependency injection from "manual wiring" to "declarative auto-injection". Provider → Invoker → Lifecycle → Value Group → Test integration — all five in one, achieving automatic dependency resolution, lifecycle management, and graceful shutdown. Core principles: constructors are Providers, dependencies are parameters, lifecycle is Hooks.
Recommended Online Tools
- JSON Formatter: /en/json/format
- Hash Calculator: /en/encode/hash
- cURL to Code: /en/dev/curl-to-code
Try these browser-local tools — no sign-up required →