Depend on Abstractions
The Dependency Inversion Principle says: high-level code — your business logic — should not depend on low-level details like a specific database or file format. Both should depend on an abstraction: in Go, an interface.
The "inversion" is the direction of the arrow. Instead of the service pointing at one concrete storage, the service points at an interface — and every storage points at that same interface from the other side.
Hard-Wired to the Details
This user service creates and uses a concrete DiskStore directly. It works — but the service is welded to one storage. A different store, a faster store, a fake store for tests: every one of them means editing the service itself.
The service welded to its storage
UserService builds its own DiskStore inside. High-level registration logic now depends on a low-level detail — the file on disk.
Output: Go program and captured output
package main
import "fmt"
type DiskStore struct{}
func (d DiskStore) Save(user string) {
fmt.Println("writing", user, "to file users.txt on disk")
}
// UserService builds its own storage — welded to the detail.
type UserService struct {
store DiskStore
}
func (s UserService) Register(user string) {
fmt.Println("registering user:", user)
s.store.Save(user)
}
func main() {
service := UserService{store: DiskStore{}}
service.Register("ana")
}
Invert the Dependency
The fix: the service asks for a Storage interface in its constructor and never learns which one it got. The disk store and the memory store both implement the interface.
Now the arrow points from both sides to the abstraction — and swapping the storage is a one-line change at the place where the service is created, not inside it.
The service accepts an interface
Same registration logic, two different storages. NewUserService(DiskStore{}) or NewUserService(MemoryStore{}) — the service code itself never changes.
Output: Go program and captured output
package main
import "fmt"
// The abstraction both sides depend on.
type Storage interface {
Save(user string)
}
type DiskStore struct{}
func (d DiskStore) Save(user string) {
fmt.Println("writing", user, "to file users.txt on disk")
}
type MemoryStore struct{}
func (m MemoryStore) Save(user string) {
fmt.Println("keeping", user, "in memory")
}
type UserService struct {
store Storage
}
func NewUserService(store Storage) UserService {
return UserService{store: store}
}
func (s UserService) Register(user string) {
fmt.Println("registering user:", user)
s.store.Save(user)
}
func main() {
onDisk := NewUserService(DiskStore{})
onDisk.Register("ana")
inMemory := NewUserService(MemoryStore{})
inMemory.Register("ben")
}
The payoff: testing without a disk
Because the service depends on the abstraction, a test can hand it a memory store and check the result instantly — no files, no cleanup. Dependency inversion is what makes code easy to unit-test.
Output: Go program and captured output
package main
import "fmt"
type Storage interface {
Save(user string)
}
// MemoryStore records what was saved — perfect for checks.
type MemoryStore struct {
users []string
}
func (m *MemoryStore) Save(user string) {
m.users = append(m.users, user)
}
type UserService struct {
store Storage
}
func NewUserService(store Storage) UserService {
return UserService{store: store}
}
func (s UserService) Register(user string) {
s.store.Save(user)
}
func main() {
store := &MemoryStore{}
service := NewUserService(store)
service.Register("ana")
service.Register("ben")
if len(store.users) == 2 {
fmt.Println("check passed: 2 users saved")
}
fmt.Println("saved users:", store.users)
}