Concepts / SOLID and design principles / Dependency inversion and injection

Dependency inversion and injection

Depend on interfaces and receive collaborators from outside.

01The intuition

The problem it solves

A homework reminder service decides who to remind, then saves the reminders. The first version builds its own database connection inside the method.

import sqlite3

class ReminderService:
    def remind_all(self, due_date) -> int:
        conn = sqlite3.connect("school.db")
        rows = conn.execute("select id from students where due = ?", (due_date,))
        count = 0
        for (sid,) in rows:
            conn.execute("insert into reminders values (?)", (sid,))
            count += 1
        return count

To test remind_all you need a real SQLite file with the right tables. To move to Postgres you edit the service. To run it in a demo with no database, you cannot. The high-level rule ("remind every student with homework due") is welded to a low-level detail ("SQLite, these table names").

Dependency inversion says the high-level policy should depend on an abstraction, and the low-level detail should depend on that same abstraction. Dependency injection is the mechanical half: the concrete object is created outside and handed in.

Build it one piece at a time

Step 1: write the abstraction from the service's point of view. Not "a database", but the two things the service actually needs.

from abc import ABC, abstractmethod

class StudentRepo(ABC):
    @abstractmethod
    def ids_with_homework_due(self, due_date) -> list[int]: ...
    @abstractmethod
    def record_reminder(self, student_id: int) -> None: ...

Step 2: make the detail implement it. SQLite code moves into a class that depends on the abstraction, which is the inversion: the arrow now points from the detail to the interface.

class SqliteStudentRepo(StudentRepo):
    def __init__(self, path: str): self._conn = sqlite3.connect(path)
    def ids_with_homework_due(self, due_date) -> list[int]:
        rows = self._conn.execute("select id from students where due = ?", (due_date,))
        return [sid for (sid,) in rows]
    def record_reminder(self, student_id: int) -> None:
        self._conn.execute("insert into reminders values (?)", (student_id,))

Step 3: inject the dependency. The service receives a repo in its constructor and never names SQLite.

class ReminderService:
    def __init__(self, repo: StudentRepo):
        self._repo = repo

    def remind_all(self, due_date) -> int:
        ids = self._repo.ids_with_homework_due(due_date)
        for sid in ids:
            self._repo.record_reminder(sid)
        return len(ids)

Step 4: wire it at the edge, and fake it in tests. One place in the program, usually main, chooses the real implementation. A test chooses a list-backed fake and runs in a millisecond.

class FakeRepo(StudentRepo):
    def __init__(self, ids): self.ids, self.reminded = ids, []
    def ids_with_homework_due(self, due_date): return list(self.ids)
    def record_reminder(self, student_id): self.reminded.append(student_id)

def test_reminds_everyone_due():
    repo = FakeRepo([1, 2, 3])
    assert ReminderService(repo).remind_all("2026-11-03") == 3
    assert repo.reminded == [1, 2, 3]

service = ReminderService(SqliteStudentRepo("school.db"))   # in main
StepReminderService depends onTest needs a databaseSwitching to Postgres edits
Startsqlite3 and table namesyesthe service
1 and 2still sqlite3 inside the methodyesthe service
3 and 4StudentRepo onlynoone new repo class and main

Why it works

Before, the dependency arrow ran from policy to detail, so the detail's every quirk leaked upward. After, both point at an interface that the policy defined in its own words. The invariant is that the high-level module can be compiled, read and tested with no low-level module present. That is what makes the fake possible and what makes swapping implementations a matter of the wiring line.

Where it breaks: an abstraction shaped like the detail. A Database interface with execute_sql(query) is SQLite with extra steps; the fake would need to understand SQL. Write the interface in the vocabulary of the caller. Also avoid a service that builds its own dependencies "for convenience" with a default argument that touches the network; defaults hide the coupling.

The trade-off you must say out loud

Injection moves construction to the edge of the program, and a large application accumulates a long wiring section or reaches for a container framework. Say that you inject the things you need to fake or swap, repositories, clocks, notifiers, random sources, and construct plain values inline. The follow-up is often "where does the clock come from", and the answer is the same: a Clock interface injected, with a fixed fake in tests, which also fixes the singleton temptation.

Where it shows up in the 100 Google problems

02Used in these problems