Dependency Inversion
What is the dependency inversion principle (DIP)?
Dependency inversion is one of the SOLID principles. It has two parts:
- High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces, protocols, abstract base classes).
- Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
So you depend on “what” (contract/interface), not “how” (concrete class). The “inversion” is that both high-level and low-level code depend on the same abstraction, rather than high-level code depending directly on low-level code.
What is the difference between dependency injection and dependency inversion?
- Dependency inversion (DIP): A design rule: depend on abstractions, not concretions. High-level code should not import or instantiate low-level classes directly; it should depend on an interface/protocol.
- Dependency injection (DI): A technique for providing dependencies to a component from the outside (constructor, parameters, framework). It doesn’t say what those dependencies should be (abstract vs concrete).
DI is often used to apply DIP: you inject an abstraction (e.g. UserRepository protocol), and the caller provides the concrete implementation. So: DIP = “depend on abstractions”; DI = “get your dependencies from outside.”
How do you apply dependency inversion in Python?
- Protocols (typing): Define a
Protocolthat describes the interface (methods and signatures). High-level code type-hints and uses the protocol; low-level code implements it.
from typing import Protocol
class UserRepository(Protocol):
def find_by_id(self, id: str): ...
class UserService:
def __init__(self, repo: UserRepository):
self.repo = repo
- Abstract base classes (ABC): Use
abc.ABCand@abstractmethodto define the contract; concrete classes inherit and implement. High-level code depends on the ABC. - Inject the abstraction: The caller or DI container wires a concrete implementation that satisfies the protocol/ABC. The high-level module never imports the concrete class.
What are the benefits of dependency inversion?
- Flexibility: You can swap implementations (e.g. different DB, different LLM client) without changing high-level logic.
- Testability: You can pass mocks or fakes that implement the same interface.
- Stability: High-level policy doesn’t break when low-level details change, as long as the contract is respected.
- Clear boundaries: Abstractions define the “seams” where you can plug in different implementations.
How does this relate to LLM/agent code?
Apply DIP so your orchestration (chains, graphs, agents) depends on interfaces for the LLM, tools, and memory—e.g. “something that takes messages and returns a response” or “something that stores and retrieves context.” Then you can plug in different providers (OpenAI, local model, mock for tests) or different memory backends without changing the orchestration logic. Same idea for tools: depend on a “tool runner” abstraction, not a specific API.
Interview angle
- “State the dependency inversion principle.” - high-level modules should not depend on low-level modules; both should depend on abstractions. And crucially, the abstraction is owned by the high-level module, not the implementation.
- “Who owns the interface?” - the consumer. A repository Protocol belongs beside the service that uses it, not beside the SQLAlchemy implementation. That ownership is what inverts the dependency; putting the interface next to the implementation achieves nothing.
- “How does this show up in Python?” - a
Protocoldefined in the domain layer, implemented by an adapter in the infrastructure layer. Structural typing means the adapter needs no import of the Protocol at all, so the dependency arrow points exactly one way. - “What does it buy you?” - the domain becomes testable and portable: swap Postgres for an in-memory fake without touching business logic, and the domain layer imports nothing from infrastructure.