Skip to main content
Clean Architecture 80 XP · 8 min

Dependency Inversion: Control Over Dependencies

With interfaces, architects make source-code dependencies point against the control flow, granting absolute power over module coupling.

Showing
Ad (728×90)

Why this matters

In a traditional call stack, control flows from high-level modules to low-level ones — and the source-code dependency follows the same direction. The high-level module imports the low-level one. That means if the database changes, the business rules change too. The business logic is at the mercy of the infrastructure.

Dependency inversion severs this coupling. By inserting an interface between the business logic and the infrastructure, the source-code dependency can be made to point against the control flow. The database module depends on the repository interface, not the other way around. Business rules own the interface; infrastructure implements it. This gives architects absolute control over the coupling graph — the database, UI, and framework become plugins the main function wires up at startup, and the core never imports any of them.

The problem

Business logic that directly imports a concrete database driver is coupled to MySQL. Changing the database, mocking it for tests, or running offline is impossible without modifying the business class.

Bad

import mysql.connector  # business logic directly imports DB driver

class MySQLDatabase:
    def __init__(self):
        self.conn = mysql.connector.connect(
            host="localhost", user="root", password="secret"
        )

    def get_user(self, user_id: int) -> dict:
        cur = self.conn.cursor(dictionary=True)
        cur.execute("SELECT * FROM users WHERE id = %s", (user_id,))
        return cur.fetchone()

class UserService:
    def __init__(self):
        self.db = MySQLDatabase()  # tightly coupled to MySQL

    def get_profile(self, user_id: int) -> dict:
        return self.db.get_user(user_id)
import { createPool } from "mysql2/promise"; // business code imports infrastructure

class MySQLDatabase {
  private pool = createPool({ host: "localhost", user: "root" });

  async getUser(userId: number): Promise<User> {
    const [rows] = await this.pool.query(
      "SELECT * FROM users WHERE id = ?", [userId]
    );
    return (rows as User[])[0];
  }
}

class UserService {
  private db = new MySQLDatabase(); // coupled: can't swap, can't test without MySQL

  async getProfile(userId: number): Promise<User> {
    return this.db.getUser(userId);
  }
}

The solution

The business class depends on a repository abstraction it owns. The MySQL implementation is a plugin wired in from outside — the service never imports it.

Good

from abc import ABC, abstractmethod

class UserRepository(ABC):  # interface owned by business logic
    @abstractmethod
    def get(self, user_id: int) -> dict: ...

class InMemoryUserRepository(UserRepository):  # test plugin
    def __init__(self, data: dict): self._data = data
    def get(self, user_id: int) -> dict: return self._data[user_id]

class UserService:
    def __init__(self, repo: UserRepository):  # depends on abstraction only
        self._repo = repo

    def get_profile(self, user_id: int) -> dict:
        return self._repo.get(user_id)

# Production: pass MySQLUserRepository(); tests: pass InMemory
svc = UserService(InMemoryUserRepository({1: {"name": "Alice"}}))
print(svc.get_profile(1))
interface UserRepository {  // interface owned by business logic
  getUser(userId: number): Promise<User>;
}

class InMemoryUserRepository implements UserRepository {
  constructor(private data: Record<number, User>) {}
  async getUser(id: number): Promise<User> { return this.data[id]; }
}

class UserService {
  constructor(private repo: UserRepository) {} // zero import of MySQL

  async getProfile(userId: number): Promise<User> {
    return this.repo.getUser(userId);
  }
}

// In tests: InMemory. In production: MySQLUserRepository. Service unchanged.
const svc = new UserService(new InMemoryUserRepository({ 1: { name: "Alice" } }));

Key takeaway

Business rules should never know the name of a database, framework, or UI library — interfaces let architects point all source-code dependencies toward the policies that matter most.

Done with this lesson?

Mark it complete to earn XP and track your progress.