Dependency Injection
Passing dependencies to a component from the outside instead of letting it create them internally.
Dependency Injection means passing dependencies (like repositories, API clients, or services) into a component rather than having the component create them itself. The component declares what it needs, and the caller provides concrete implementations.
In Go, this typically looks like a constructor that accepts interfaces:
type OrderRepository interface {
Save(order Order) error
FindByID(id string) (Order, error)
}
type PaymentGateway interface {
Charge(amount Money, card Card) error
}
func NewOrderService(
repo OrderRepository,
payments PaymentGateway,
) OrderService {
return OrderService{
repo: repo,
payments: payments,
}
}
The OrderService doesn't know if repo talks to PostgreSQL, Firestore, or an in-memory map. It works with the interface. The main function (or a setup function) decides which implementation to use and wires everything together.
This is the foundation of Clean Architecture. The Dependency Inversion Principle says that high-level modules shouldn't depend on low-level modules, but on abstractions. Dependency injection is how you put that principle into practice: you define interfaces in the application layer and inject implementations from the adapters layer.
The biggest payoff is testability. When your service accepts interfaces, you can pass a mock or an in-memory implementation in tests. You don't need to deal with network calls or flaky infrastructure. Your unit tests become fast and focused on business logic.
In Go, you don't need a framework for this. The simplest approach is to wire everything manually in main.go. It's explicit, easy to follow, and the compiler tells you immediately if something is missing. The wiring code can grow large in bigger projects, but it's straightforward to maintain.
Some teams use code generation or reflection-based tools to reduce boilerplate. Manual wiring is more verbose, but you can read the code and understand exactly what's happening. With reflection-based tools, debugging gets harder because the dependency graph is resolved at runtime.
The trade-off is boilerplate. You need constructors, interfaces, and explicit wiring code. For small projects, this can feel like overhead. As the codebase grows and more people contribute, it quickly pays off.
References
- How to implement Clean Architecture in Go (Golang) — Dedicated section on Dependency Injection showing how to wire adapters into application services through constructors. Demonstrates injecting repositories and gRPC clients in main.go as the simplest DI approach in Go.
- Microservices test architecture. Can you sleep well without end-to-end tests? — Notes how dependency injection wiring evolved as the project grew, and how injecting adapters through interfaces made the test architecture possible.
- Watermill 1.3 released, an open-source event-driven Go library — Shows using structs for dependency injection in Watermill event handlers, passing repositories and other dependencies through struct fields.
- Is Clean Architecture Overengineering? — Extensive discussion of dependency injection in the context of Clean Architecture. Covers manual wiring vs frameworks, why a complex dependency tree signals architectural problems, and the trade-offs of using DI libraries like Wire or fx.
- When you shouldn't use frameworks in Go — Discusses dependency injection frameworks vs manual wiring in Go. Covers experience with Google Wire, switching to manual injection, and why explicit wiring in main is preferable despite larger files.
- Unpopular opinions about Go — Recommends code generation over reflection for dependency injection. Suggests Wire for teams that need a DI tool, because the generated code is easy to debug compared to reflection-based alternatives.
- How to Create PRs That Get Merged The Same Day — Mentions how people sometimes blame dependency injection for application complexity, when the real issue is deeper architectural problems surfaced during code review.