Chapter 9: Dependency Injection
Dependency Injection
Section titled “Dependency Injection”Dependency Injection (DI) makes code testable and loosely coupled by passing dependencies instead of creating them internally.
Dependency Injection is a fundamental design principle that dramatically improves code quality. At its core, DI is simple: instead of creating dependencies inside a struct or function, you pass them in from outside. This inversion of control unlocks testability, flexibility, and better code organization.
Go’s approach to DI is refreshingly straightforward compared to frameworks in other languages. No XML configuration, no annotations, no magic. Just interfaces, constructors, and explicit dependency passing. This simplicity makes DI accessible while maintaining all its benefits.
This chapter covers constructor injection, interface segregation, functional options for optional dependencies, and patterns for wiring applications together. You’ll learn when DI helps, when it’s overkill, and how to strike the right balance.
The Problem Without DI
Section titled “The Problem Without DI”Understanding Tight Coupling
Section titled “Understanding Tight Coupling”Without DI, code is tightly coupled and hard to test. Tight coupling means a type directly creates or accesses its dependencies rather than receiving them. This seems convenient at first - you just call NewDatabase() whenever you need a database. But it creates serious problems.
Testing Becomes Impossible: Want to test a service that connects to a real database? You need a running database for every test. Tests become slow, brittle, and environment-dependent. You can’t test locally without infrastructure.
Inflexibility: Switching implementations requires editing every place that creates the dependency. Want to replace PostgreSQL with MySQL? Find every NewPostgresDB() call and change it. Miss one, and the application breaks.
Hidden Dependencies: Looking at a struct’s fields doesn’t tell you what it depends on. Dependencies are created inside methods, making them invisible. This makes code hard to understand and reason about.
Circular Dependencies: When types create their own dependencies, circular dependency problems emerge. Type A creates type B, which creates type C, which creates type A. These cycles are difficult to detect and resolve.
Example
Section titled “Example”Constructor Injection
Section titled “Constructor Injection”How It Works
Section titled “How It Works”Constructor injection solves tight coupling by passing dependencies when creating an object. Instead of NewService() creating its dependencies internally, NewService(deps) accepts them as parameters. The constructor becomes explicit about what the type needs.
In Go, constructor injection uses “New” functions that accept dependencies and return configured instances. These functions make dependencies visible and enforceable - you literally can’t create the object without providing what it needs.
The pattern has three parts:
- Define Interface - specify what capabilities you need, not concrete types
- Constructor Function - accept dependencies implementing those interfaces
- Store Dependencies - keep them in struct fields for method use
This inversion of control - dependencies flow in rather than being created inside - is what makes code testable and flexible.
Why Constructor Injection
Section titled “Why Constructor Injection”Explicit Dependencies: Every dependency is visible in the constructor signature. Want to know what a service needs? Look at NewService(db Database, cache Cache). No surprises.
Compile-Time Safety: Forgot to pass a dependency? The code won’t compile. This catches errors immediately rather than at runtime.
Easy Testing: Pass mock implementations in tests. Production uses real implementations. Same code, different dependencies. No conditional logic, no build tags, just dependency injection.
Immutable After Construction: Dependencies are set once at creation and never change. This eliminates a whole class of bugs related to dependencies being swapped mid-execution.
Implementation
Section titled “Implementation”Pass dependencies through the constructor:
Interface Segregation
Section titled “Interface Segregation”The Principle
Section titled “The Principle”Interface Segregation is the “I” in SOLID principles: clients shouldn’t depend on interfaces they don’t use. In practice, this means defining small, focused interfaces rather than large, monolithic ones.
A big interface like Repository with 20 methods forces implementers to implement all 20, even if they only need 3. It also forces clients to depend on 17 methods they don’t use. Small interfaces prevent this - define UserFinder, UserSaver, and UserDeleter separately.
Go’s interface composition makes this natural. Define small interfaces, compose them into larger ones when needed. A service that only reads users depends on UserFinder. A service that reads and writes depends on an interface composed of UserFinder and UserSaver.
Why Interface Segregation Matters
Section titled “Why Interface Segregation Matters”Minimal Dependencies: Services depend only on what they actually use. A read-only service shouldn’t depend on write methods. This reduces coupling and makes intent clear.
Easier Mocking: Testing a service that depends on UserFinder (1 method) is trivial - implement one method. Testing a service depending on FullRepository (20 methods) means implementing 20 methods even if only 1 is used.
Flexibility: Small interfaces can be implemented by multiple types. UserFinder might be implemented by PostgresRepo, CachedRepo, or MockRepo. Each provides finding in different ways. A large interface locks you into specific implementations.
Clear Contracts: Small interfaces document exactly what capabilities a type needs. NewService(finder UserFinder) clearly states “this service finds users.” Compare to NewService(repo Repository) - what does it actually do with that repository?
Implementation
Section titled “Implementation”Define small, focused interfaces:
Functional Options for Optional Dependencies
Section titled “Functional Options for Optional Dependencies”Wire: Compile-Time DI
Section titled “Wire: Compile-Time DI”Google’s Wire generates dependency injection code at compile time:
//go:build wireinject
package main
import "github.com/google/wire"
func InitializeApp() *App { wire.Build( NewDatabase, NewUserRepository, NewUserService, NewApp, ) return nil}Wire generates wire_gen.go with the actual initialization code.
Manual Wiring
Section titled “Manual Wiring”For simpler projects, wire dependencies manually:
Key Takeaways
Section titled “Key Takeaways”- Constructor injection - pass dependencies via constructors
- Depend on interfaces - not concrete types
- Interface segregation - small, focused interfaces
- Functional options - for optional dependencies
- Wire manually - simple projects don’t need frameworks
- Use Wire - for complex dependency graphs
Exercise
Section titled “Exercise”Testable HTTP Client
Create an HTTP client wrapper that accepts a Doer interface (matching http.Client.Do). This makes it testable without real HTTP calls.
Next up: Chapter 10: Testing Strategies