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
| Step | ReminderService depends on | Test needs a database | Switching to Postgres edits |
|---|---|---|---|
| Start | sqlite3 and table names | yes | the service |
| 1 and 2 | still sqlite3 inside the method | yes | the service |
| 3 and 4 | StudentRepo only | no | one 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
- Design a cache manager with pluggable eviction and TTL: the cache depends on an
EvictionPolicyand aClockinterface; TTL tests set the clock by hand. - Design a thread-safe singleton and justify the alternative: the alternative is exactly this, one instance built in main and injected, which keeps tests independent.
- Design a URL shortener (class level): an
IdStrategyand aLinkRepositoryare injected, so the same service runs on a dictionary in tests and a database in production. - Design a notification service: channels, templates and the retry clock are passed in, which is how a test proves three retries without waiting.