“High-level modules should not depend on low-level modules. Both should depend on abstractions.”

Welcome to a core principle of software architecture: Dependency Inversion, the “D” in SOLID. In this article, we’ll explore what it means in practice, how to implement it in Go using interfaces and dependency injection, and why it’s essential for writing unit-testable code.

banner


🧠 What Is the Dependency Inversion Principle?

The Dependency Inversion Principle (DIP) flips the traditional dependency model:

  • ❌ Traditional: High-level business logic depends directly on low-level implementation (e.g., a database).

  • ✅ DIP: Both layers depend on abstractions — usually in the form of interfaces.

It’s about reversing the direction of dependency to reduce coupling and improve flexibility.


💡 A Quick Example: Tightly Coupled Code

1
2
3
4
5
6
7
8
type OrderService struct {
    db *sql.DB
}

func (o *OrderService) Save(order Order) error {
    _, err := o.db.Exec("INSERT INTO orders ...")
    return err
}

This code:

  • Tightly couples OrderService to a concrete *sql.DB

  • Is hard to test in isolation


✅ Refactor with Dependency Inversion

Let’s invert the dependency:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type OrderRepository interface {
    Save(order Order) error
}

type OrderService struct {
    repo OrderRepository
}

func (o *OrderService) Save(order Order) error {
    return o.repo.Save(order)
}

Now:

  • OrderService depends on an interface

  • OrderRepository can be backed by a real DB in prod or a mock in tests


🛠️ Enter Dependency Injection

We’ve inverted dependencies — now we need a way to supply them. Manual constructor injection in Go:

1
2
3
func NewOrderService(repo OrderRepository) *OrderService {
    return &OrderService{repo: repo}
}

At runtime, inject the actual implementation:

1
service := NewOrderService(NewPostgresOrderRepository(db))

🧪 Why This Rocks for Testing

DIP + interfaces = test-friendly code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type MockOrderRepository struct {
    SavedOrder Order
}

func (m *MockOrderRepository) Save(order Order) error {
    m.SavedOrder = order
    return nil
}

func TestOrderService_Save(t *testing.T) {
    mock := &MockOrderRepository{}
    service := NewOrderService(mock)

    order := Order{ID: 42}
    err := service.Save(order)

    require.NoError(t, err)
    require.Equal(t, 42, mock.SavedOrder.ID)
}

You’ve isolated business logic from infrastructure — the holy grail of testability.


🔄 Summary

  • DIP inverts traditional dependency direction: high-level modules depend on interfaces, not implementations.

  • Interfaces define contracts at the boundaries.

  • Dependency Injection supplies those interfaces at runtime.

  • Unit Testing becomes effortless when your logic isn’t tangled up in database, network, or file system concerns.


🧭 When to Use DIP in Go

  • ✅ When abstracting IO, storage, APIs, or 3rd party integrations

  • ✅ When writing business logic you want to test independently

  • ❌ Not needed for everything — Go prefers concrete, simple code unless you need indirection


🚀 Follow me on norbix.dev for more insights on Go, system design, and engineering wisdom.