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.