Wire vs fx vs dig

Comparing DI Frameworks in Go

Wire generates code at compile time, fx manages dependencies at runtime, and dig analyzes dependency graphs.

The wiring problem in Go

You start a Go service. main.go has a handful of lines. You create a database connection, pass it to a repository, pass that to a service, pass that to an HTTP handler. It works. Then you add caching. Then you add metrics. Then you add a configuration struct that needs validation and environment variable overrides. Suddenly main.go is a tangle of constructors. You change one dependency and three other files break. You realize you are spending more time connecting parts than building logic.

This is the dependency injection problem. Dependency injection is just passing what a function needs instead of making it create the thing itself. In Go, you can do this manually. Tools like Wire, fx, and dig automate the process. They do not write your business logic. They manage the plumbing of connecting your types.

Go does not have a built-in dependency injection framework. The language prefers explicit code. If a function needs a database, you pass the database. This keeps dependencies visible and testable. When the graph of dependencies grows, manual wiring becomes tedious and error-prone. That is where these tools step in. They analyze your types and generate or execute the wiring code for you.

DI tools manage the graph. You manage the types.

What Wire, fx, and dig actually do

Dependency injection tools in Go fall into two categories: compile-time code generation and runtime composition. Wire generates code at compile time. fx builds the graph at runtime. dig is a library that analyzes dependency graphs and powers fx.

Think of a kitchen. The chef needs a knife, a pan, and ingredients. Manual wiring is the chef grabbing each item from the drawer. DI is a sous-chef who lays out everything before service starts. Wire is a sous-chef who writes a checklist for the next shift. fx is a sous-chef who also cleans up the kitchen when service ends.

Wire uses code generation. You define your dependency graph in a special file. You run go generate. Wire writes a new Go file with the exact constructor calls. The compiler sees the generated code. If the graph is broken, generation fails. You never run broken code. There is no reflection overhead. The generated code is just Go.

fx uses reflection at runtime. You provide constructor functions to fx. fx analyzes them and builds the graph in memory. It calls constructors in the correct order. fx also manages lifecycle. You can register shutdown hooks. fx calls them when the app stops. fx provides a CLI for debugging your graph. You can inspect the dependency tree before the app starts.

dig is the engine behind fx. It is a library for graph analysis. You can use dig standalone to analyze dependencies programmatically. Most users do not use dig directly. They use fx, which uses dig internally. dig is useful if you need custom DI behavior or want to build your own framework.

Go values simplicity. DI frameworks in other languages often hide complexity behind magic. Go tools try to be transparent. Wire generates code you can read. fx exposes the graph. This aligns with Go philosophy. You can always see what is happening.

Convention aside: gofmt is mandatory in Go. Wire generates code that passes gofmt. You trust the tool to format the output. Most editors run gofmt on save. You do not argue about indentation in generated code.

DI tools manage the graph. You manage the types.

Manual wiring: the baseline

Here is the manual way to wire a simple dependency graph. You construct each type and pass it to the next. This works for small projects. It breaks when the graph grows.

// main.go
func main() {
    // Load config from environment or file
    cfg := loadConfig()

    // Create database connection using config
    db, err := connectDB(cfg)
    // Check error immediately to fail fast on bad config
    if err != nil {
        log.Fatal(err)
    }

    // Pass db to repository constructor
    repo := NewRepo(db)

    // Pass repo to service constructor
    svc := NewService(repo)

    // Pass service to handler constructor
    handler := NewHandler(svc)

    // Start HTTP server with the fully wired handler
    http.ListenAndServe(":8080", handler)
}

Manual wiring is explicit. You can see every dependency. You can test each constructor in isolation. The downside is repetition. If you add a new dependency, you update main.go. If you change a constructor signature, you update main.go. If you have multiple entry points, you duplicate the wiring.

Convention aside: if err != nil is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. Constructors should return errors. If a dependency fails to initialize, the app should fail fast. Tools like Wire and fx handle errors automatically. If a constructor returns an error, the app fails to start.

Manual wiring works for small projects. It breaks when the graph grows.

How Wire generates code

Wire generates code at compile time. You define the graph once, and the tool writes the constructor calls for you. The generated code is plain Go. You can read it. You can debug it.

Here is a Wire setup. You create a wire.go file with a wire.Build call. You run go generate. Wire creates wire_gen.go with the implementation.

// wire.go
//go:build wireinject
// +build wireinject

// This file is excluded from normal builds.
// It only executes when you run `go generate`.
package main

import "github.com/google/wire"

// InitApp defines the dependency graph for the application.
// Wire analyzes the return type and the provided constructors
// to generate the wiring code in wire_gen.go.
func InitApp(cfg Config) (*App, error) {
    wire.Build(
        NewDB,
        NewRepo,
        NewService,
        NewApp,
    )
    return nil, nil
}

Wire reads InitApp. It sees the return type is *App. It looks at wire.Build. It traces NewApp needs *Service. It traces NewService needs *Repo. It traces NewRepo needs *DB. It traces NewDB needs Config. It writes wire_gen.go with all the calls. The compiler sees wire_gen.go. If you break the graph, go generate fails.

Wire catches errors early. If you have a cyclic dependency, Wire rejects the code with wire: cycle detected. If you miss a constructor, Wire complains with wire: inject: no provider found for .... You fix the graph before you run the app.

Convention aside: Receiver naming in Go is usually one or two letters matching the type. (r *Repo) Get(...) not (self *Repo). Wire does not care about receiver names. It cares about types. Keep your receivers idiomatic.

Wire writes code. fx runs code. Both solve the same problem differently.

How fx manages lifecycle

fx builds the graph at runtime and manages the lifecycle of your application. It handles startup and graceful shutdown automatically. You provide providers. fx resolves dependencies and calls constructors.

Here is an fx setup. You create an fx.App with providers and invokers. fx resolves the graph and runs the app.

// main.go
func main() {
    // fx.New creates the application container.
    // It resolves dependencies at startup using reflection.
    app := fx.New(
        fx.Provide(
            loadConfig,
            connectDB,
            NewRepo,
            NewService,
            NewHandler,
        ),
        fx.Invoke(startServer),
    )

    // Run starts the app and blocks until a signal is received.
    // It calls shutdown hooks in reverse order of creation.
    app.Run()
}

fx uses reflection to analyze your providers. It builds the graph in memory. It calls constructors in the correct order. If the graph is broken, fx panics at startup with fx: could not provide .... You can catch this and fail gracefully.

fx provides lifecycle management. You can register shutdown hooks. fx calls them when the app stops. This is useful for closing database connections, flushing metrics, or draining HTTP servers. fx handles the order. It calls hooks in reverse order of creation.

fx also provides a CLI. You can run fx to inspect your graph. You can see the dependency tree. You can debug wiring issues without running the app. This is a strong feature of fx.

Convention aside: context.Context always goes as the first parameter, conventionally named ctx. Functions that take a context should respect cancellation and deadlines. DI tools do not inject context automatically. You usually pass context to the entry point or the handler. Keep context flowing through your graph manually.

fx gives you lifecycle management. Wire gives you code you can read. Pick based on what you value.

Pitfalls and errors

DI tools help, but they introduce their own pitfalls. Cyclic dependencies are the most common. A needs B, B needs A. The graph has a cycle. Wire catches this at generation time. fx catches this at startup. Both reject the code.

Wire reports cycles with wire: cycle detected. fx reports cycles with fx: cycle detected. You must break the cycle. Refactor your types. Extract shared dependencies. Use interfaces to decouple.

Missing providers are another issue. You forget to provide a constructor. Wire complains with wire: inject: no provider found for .... fx complains with fx: could not provide .... You add the missing provider.

Over-engineering is a real risk. DI tools add abstraction. Abstraction costs readability. If you have three types, manual wiring is clearer. DI adds indirection. You lose the ability to see the graph in main.go. You must trust the tool.

Convention aside: context.Context should flow through the graph. DI tools do not inject context. You pass context to the root or the handler. Functions that take context should respect cancellation. If a goroutine waits on a channel that never gets closed, you have a leak. Always have a cancellation path.

A broken graph should fail at startup, not in production. Trust the tool to catch cycles.

When to use Wire, fx, or dig

Use manual construction when your dependency graph is small and stable. The simplest code is the best code. You can see every dependency. You can test each constructor in isolation. No tool overhead.

Use Wire when you want compile-time safety and zero runtime overhead. You get generated code that you can inspect and debug. Wire catches errors before you run the app. The generated code is plain Go. You trust the compiler.

Use fx when you need lifecycle management, shutdown hooks, or a CLI for debugging your graph. It handles startup and teardown for you. fx provides fxtest for testing. You can provide mock dependencies in tests. fx is ideal for complex services with many moving parts.

Use dig when you need a library to analyze dependency graphs programmatically. It is the engine behind fx, but you can use it standalone for custom tooling. dig is useful if you need to build your own DI framework or analyze graphs at runtime.

Start simple. Add tools only when the wiring hurts.

Where to go next