Dependency Injection in Go: Patterns & Best Practices

Published: (December 10, 2025 at 05:45 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

Introduction

Dependency injection (DI) is a fundamental design pattern that promotes clean, testable, and maintainable code in Go applications. By receiving their dependencies from external sources rather than creating them internally, components become decoupled, modular, and easier to work with.

Benefits of Dependency Injection

  • Improved Testability – Replace real implementations with mocks or test doubles, enabling fast, isolated unit tests without external services.
  • Better Maintainability – Dependencies are explicit in constructor functions, making it clear what a component requires.
  • Loose Coupling – Components depend on abstractions (interfaces) rather than concrete types, allowing implementations to change without affecting dependents.
  • Flexibility – Configure different implementations for development, testing, and production without altering business logic.

Constructor Injection (Idiomatic Go)

The most common way to implement DI in Go is through constructor functions, typically named NewXxx, that accept dependencies as parameters.

// Define an interface for the repository
type UserRepository interface {
    FindByID(id int) (*User, error)
    Save(user *User) error
}

// Service depends on the repository interface
type UserService struct {
    repo UserRepository
}

// Constructor injects the dependency
func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
}

// Methods use the injected dependency
func (s *UserService) GetUser(id int) (*User, error) {
    return s.repo.FindByID(id)
}

Multiple Dependencies

When a component requires several collaborators, list them all as constructor parameters:

type EmailService interface {
    Send(to, subject, body string) error
}

type Logger interface {
    Info(msg string)
    Error(msg string, err error)
}

type OrderService struct {
    repo       OrderRepository
    emailSvc   EmailService
    logger     Logger
    paymentSvc PaymentService
}

func NewOrderService(
    repo OrderRepository,
    emailSvc EmailService,
    logger Logger,
    paymentSvc PaymentService,
) *OrderService {
    return &OrderService{
        repo:       repo,
        emailSvc:   emailSvc,
        logger:     logger,
        paymentSvc: paymentSvc,
    }
}

Dependency Inversion Principle (DIP) & Interface Segregation

High‑level modules should depend on abstractions, not concrete implementations. In Go, this translates to defining small, focused interfaces.

// Good: Small, focused interface
type PaymentProcessor interface {
    ProcessPayment(amount float64) error
}

// Bad: Large interface with unnecessary methods
type PaymentService interface {
    ProcessPayment(amount float64) error
    RefundPayment(id string) error
    GetPaymentHistory(userID int) ([]Payment, error)
    UpdatePaymentMethod(userID int, method PaymentMethod) error
    // ... many more methods
}

A narrow interface follows the Interface Segregation Principle, keeping clients from depending on methods they don’t use.

Database Abstraction Example

// Database interface – high‑level abstraction
type DB interface {
    Query(ctx context.Context, query string, args ...interface{}) (Rows, error)
    Exec(ctx context.Context, query string, args ...interface{}) (Result, error)
    BeginTx(ctx context.Context) (Tx, error)
}

// Repository depends on the abstraction
type UserRepository struct {
    db DB
}

func NewUserRepository(db DB) *UserRepository {
    return &UserRepository{db: db}
}

func (r *UserRepository) FindByID(ctx context.Context, id int) (*User, error) {
    query := "SELECT id, name, email FROM users WHERE id = $1"
    rows, err := r.db.Query(ctx, query, id)
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    // ... parse rows
    return nil, nil
}

This pattern is especially useful for multi‑tenant databases or when swapping between different drivers.

Composition Root

The composition root is the single place (usually main) where the entire object graph is assembled:

func main() {
    // Initialize infrastructure dependencies
    db := initDatabase()
    logger := initLogger()

    // Initialize repositories
    userRepo := NewUserRepository(db)
    orderRepo := NewOrderRepository(db)

    // Initialize services with dependencies
    emailSvc := NewEmailService(logger)
    paymentSvc := NewPaymentService(logger)
    userSvc := NewUserService(userRepo, logger)
    orderSvc := NewOrderService(orderRepo, emailSvc, logger, paymentSvc)

    // Initialize HTTP handlers
    userHandler := NewUserHandler(userSvc)
    orderHandler := NewOrderHandler(orderSvc)

    // Wire up routes
    router := setupRouter(userHandler, orderHandler)

    // Start server
    log.Fatal(http.ListenAndServe(":8080", router))
}

This makes the dependency graph explicit and simplifies reasoning about the application’s structure, which is valuable for REST APIs and other layered systems.

DI Frameworks for Larger Projects

Wire (compile‑time)

Wire generates type‑safe code at compile time, eliminating runtime reflection overhead.

go install github.com/google/wire/cmd/wire@latest
// wire.go
//go:build wireinject
// +build wireinject

package main

import "github.com/google/wire"

func InitializeApp() (*App, error) {
    wire.Build(
        NewDB,
        NewUserRepository,
        NewUserService,
        NewUserHandler,
        NewApp,
    )
    return &App{}, nil
}

Dig (runtime)

Dig uses reflection to resolve dependencies at runtime, offering more flexibility at the cost of some overhead.

import "go.uber.org/dig"

func main() {
    container := dig.New()

    // Register providers
    container.Provide(NewDB)
    container.Provide(NewUserRepository)
    container.Provide(NewUserService)
    container.Provide(NewUserHandler)

    // Invoke a function that needs dependencies
    err := container.Invoke(func(handler *UserHandler) {
        // Use handler
    })
    if err != nil {
        log.Fatal(err)
    }
}

When to Use a Framework vs. Manual DI

  • Use a framework when:

    • The dependency graph is complex with many interdependent components.
    • Multiple implementations of the same interface must be selected based on configuration.
    • Automatic resolution would reduce boilerplate and error‑prone wiring.
    • You are building a large application where manual wiring becomes cumbersome.
  • Stick with manual DI when:

    • The application is small to medium‑size and the graph remains simple.
    • You prefer explicit wiring without additional dependencies.
Back to Blog

Related posts

Read more »

Why Design Patterns?

It’s important to understand that Design Patterns were never meant to be hacked‑together shortcuts to be applied in a haphazard, “one‑size‑fits‑all” manner to y...