SOLID Principles · The Five Principles

Dependency Inversion Principle

Make high-level policy depend on abstractions, not on concrete details, and inject those details from outside.

  • 4 min read
  • 10 practice questions
  • 2 graded problems
On this page
  1. Description
  2. The principle
  3. How to apply it, step by step
  4. Where you'll see it
  5. Which tool for which job?
  6. Common mistakes
  7. Check yourself
  8. Key takeaways
  9. Quick revise
  10. Practice

Description

The Dependency Inversion Principle says business rules shouldn't depend on databases, email servers, clocks or other details - both should depend on an abstraction that the business code owns. It's what makes core logic testable in milliseconds and lets infrastructure change without touching it. This skill covers the principle, dependency injection as the usual way to apply it, and how to avoid wrapping everything in interfaces.

Here's the kind of code it's about:

Java

class OrderService {
    private final MySqlOrderDao dao = new MySqlOrderDao();       // concrete database
    private final SmtpMailer mailer = new SmtpMailer("smtp.acme.com");  // concrete email server

    void placeOrder(Order order) {
        dao.insert(order);
        mailer.send(order.customerEmail(), "Thanks for your order!");
    }
}

Python

class OrderService:
    def __init__(self):
        self._dao = MySqlOrderDao()                          # concrete database
        self._mailer = SmtpMailer("smtp.acme.com")           # concrete email server

    def place_order(self, order: Order) -> None:
        self._dao.insert(order)
        self._mailer.send(order.customer_email, "Thanks for your order!")

The most important code in the system - the business rule "place an order" - is welded to MySQL and one SMTP server. You can't test it without a database and a mail server, and switching to Postgres or an email API means editing OrderService.

We'll go from simple to more technical:

  1. The principle: abstractions owned by the high-level code.
  2. Applying it with constructor injection, step by step.
  3. Where you'll see it: DI containers, JDBC and DB-API, ports and adapters.
  4. When an interface isn't worth it, and how abstractions leak.

The principle

High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.

The high-level code declares what it needs as an interface it owns; the details implement it; something outside wires them together.

Java

interface OrderRepository { void save(Order order); }
interface Notifier { void notify(String to, String message); }

class OrderService {
    private final OrderRepository orders;
    private final Notifier notifier;

    OrderService(OrderRepository orders, Notifier notifier) {   // details are injected
        this.orders = orders;
        this.notifier = notifier;
    }

    void placeOrder(Order order) {
        orders.save(order);
        notifier.notify(order.customerEmail(), "Thanks for your order!");
    }
}

// Production: new OrderService(new PostgresOrderRepository(ds), new EmailApiNotifier(key));
// Tests:      new OrderService(new InMemoryOrderRepository(), new RecordingNotifier());

Python

from typing import Protocol


class OrderRepository(Protocol):
    def save(self, order: Order) -> None: ...


class Notifier(Protocol):
    def notify(self, to: str, message: str) -> None: ...


class OrderService:
    def __init__(self, orders: OrderRepository, notifier: Notifier):   # details are injected
        self._orders = orders
        self._notifier = notifier

    def place_order(self, order: Order) -> None:
        self._orders.save(order)
        self._notifier.notify(order.customer_email, "Thanks for your order!")


# Production: OrderService(PostgresOrderRepository(pool), EmailApiNotifier(key))
# Tests:      OrderService(InMemoryOrderRepository(), RecordingNotifier())

How to apply it, step by step

  1. Identify the policy (business rules) and the details (databases, HTTP clients, clocks, file systems).
  2. Let the policy define small interfaces describing what it needs - in its own terms.
  3. Make the details implement those interfaces.
  4. Pass the details in through the constructor (dependency injection), instead of creating them inside.
  5. Wire everything in one place at startup - by hand or with a framework like Spring.

Where you'll see it

  • Spring and other DI containers exist to do step 5 for you: constructor parameters typed as interfaces, implementations chosen by configuration. Python projects mostly wire by hand in one startup function (FastAPI's Depends is the web-framework version).
  • JDBC: your code talks to java.sql.Connection; each database vendor ships a driver that implements it. (Python's equivalent is the DB-API, PEP 249: psycopg, sqlite3 and mysqlclient all offer the same connect(), cursor() and execute().)
  • Hexagonal / ports-and-adapters architecture: the domain defines ports (interfaces), infrastructure provides adapters.

Which tool for which job?

The dependencyDo this
A database, message broker, HTTP API or file systeman interface (port) owned by the business code, implemented by an adapter
The current timeinject a Clock (Java) or a now callable (Python)
Random numbers or idsinject a generator so tests can fix them
Stable, side-effect-free helpers and value objectsuse them directly - no interface needed
Wiring it all togetherone composition root at startup (by hand, or Spring / a DI container)

Common mistakes

MistakeWhat happensDo this instead
new / constructing details inside business classesuntestable without the real infrastructureinject them through the constructor
An interface for every class, including pure helpersindirection with no second implementation or test needabstract only volatile details
Abstractions that leak details (executeSql, driver exceptions)the business code still depends on the databasespeak the domain's language; translate errors in the adapter
A service locator called inside methodshidden dependenciesconstructor parameters
The interface defined by the infrastructure sidedependencies still point at the detailsthe business side owns the interface

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. Why is new SmtpMailer() inside OrderService a DIP violation, and what's the fix?

Show answer

The high-level service depends directly on a low-level detail. Define a Notifier interface (owned by the service's side), implement it with an SMTP adapter, and inject it through the constructor.

2. How does DIP make code with timeouts or expiry dates easier to test?

Show answer

Treat the clock as a dependency: inject a Clock interface instead of calling Instant.now(), and tests can set or advance time deterministically.

3. Who should own the interface - the database layer or the business logic?

Show answer

The business logic (high-level module). The database adapter implements the interface the domain defined - that's the "inversion".

Key takeaways

  • Business rules depend on abstractions they own; details implement them.
  • Inject dependencies through constructors; wire them in one place.
  • Clocks, databases, HTTP clients and file systems are all details worth abstracting.
  • Don't wrap stable, dependency-free code in interfaces just because.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before practice or an interview, and open the full lesson for anything that doesn't ring a bell.

Pick the tool

The dependencyDo this
Database, broker, HTTP API, filesa port owned by the domain + an adapter
The current timeinject Clock / a now callable
Random values, idsinject a generator
Pure helpers, value objectsuse directly
Wiringone composition root at startup

Snippets worth knowing by heart

Java

interface OrderRepository { void save(Order order); }

final class OrderService {
    private final OrderRepository orders;
    private final Clock clock;
    OrderService(OrderRepository orders, Clock clock) { this.orders = orders; this.clock = clock; }
}

// main: new OrderService(new PostgresOrderRepository(ds), Clock.systemUTC());
// test: new OrderService(new InMemoryOrderRepository(), Clock.fixed(t, ZoneOffset.UTC));

Python

class OrderRepository(Protocol):
    def save(self, order: Order) -> None: ...

class OrderService:
    def __init__(self, orders: OrderRepository, now: Callable[[], datetime]):
        self._orders = orders
        self._now = now

# main: OrderService(PostgresOrderRepository(pool), lambda: datetime.now(UTC))
# test: OrderService(InMemoryOrderRepository(), lambda: FIXED_TIME)

The traps

  • Constructing details inside business classes.
  • Interfaces for everything - only volatile details need them.
  • Leaky ports - SQL strings, driver exceptions.
  • Service locators - hidden dependencies.

Staff-level points, one line each

  • DIP = who owns the abstraction; DI = how dependencies arrive; IoC = the framework calls you.
  • Constructor-injecting a concrete MySqlOrderDao is DI without DIP.
  • Ports and adapters: all source dependencies point at the domain - enforce with ArchUnit / import-linter.
  • Composition root: wire once at the entry point; pure DI or a container.
  • Captive dependency: a singleton built with request data served ana's report to ben; inject a provider.
  • Constructor injection exposes cycles at startup (Spring Boot refuses them by default since 2.6) - move a responsibility.
  • Clock.fixed / Clock.offset make time testable; Python mock.patch must target where the name is looked up.
  • Fakes pass the same contract tests as real adapters; mocks for unobservable side effects.
  • Translate infrastructure errors into domain errors in the adapter.

Say it in an interview

  • "The domain owns OrderRepository; the Postgres adapter implements it."
  • "Time is a dependency here - I inject a clock so the expiry test is exact."
  • "Request data goes in method arguments, not into a singleton's constructor."
  • "The adapter turns driver exceptions into RepositoryUnavailable."

Practice

This skill comes with 10 multiple-choice questions that tell you which misconception a wrong answer reveals and 2 graded problems. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Dependency Inversion Principle free

An Order Service Behind Ports

MediumProCoding problemJava · Python

OrderService holds the most important rules in the shop - but it builds its own Postgres repository and Stripe gateway, so it can't run anywhere except production (in the test environment they just throw).

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Dependency Inversion Principle free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime