Card 04/ 09

WalkthroughDifficulty: Intermediate3 min

Refactoring to Dependency Inversion, Step by Step

Define the interface on the service's side, implement it, then inject it.

Inverting a dependency means introducing an interface that the high-level class owns, implementing it in the low-level code, and injecting the implementation. This card takes the ReminderService that created its own database through those steps, and settles the question people get wrong most often: where the interface belongs.

Inverting ReminderService's dependency on MySQL

Write down what the policy needs

Read ReminderService and list what it asks the database for. It needs one thing: the email address for a user id. It does not need connections, tables or SQL.

Define the interface beside the policy

Create UserSource with one method, email_for(user_id), in the same module or package as ReminderService. It is named after the service's need, not after MySQL, and it lives with the code that owns it.

Implement it in the detail

Write MySQLUserSource, which implements UserSource by running the query. All the SQL moves here. The MySQL package now depends on the reminder package, because it implements the reminder package's interface.

Inject the implementation

Change ReminderService to take a UserSource in its constructor and delete the line that created the database. The application's start-up code creates MySQLUserSource and passes it in.

Where each class lives after the refactoring

The final code below wires the real implementation in. The query it prints is a stand-in for one sent to a database.

ReminderService with an injected UserSource

py
from typing import Protocol


class UserSource(Protocol):
    def email_for(self, user_id: int) -> str: ...


class ReminderService:
    def __init__(self, users: UserSource):
        self.users = users

    def remind(self, user_id: int) -> str:
        return f"MAIL to {self.users.email_for(user_id)}: reset your password"


class MySQLUserSource:
    def email_for(self, user_id: int) -> str:
        print(f"SELECT email FROM users WHERE id = {user_id}")
        return "[email protected]"


if __name__ == "__main__":
    service = ReminderService(MySQLUserSource())
    print(service.remind(7))
OutputRan with python:3.12-slim (Python 3.12.13)
SELECT email FROM users WHERE id = 7
MAIL to [email protected]: reset your password
java
public class DipInverted {
    public static void main(String[] args) {
        ReminderService service = new ReminderService(new MySQLUserSource());
        System.out.println(service.remind(7));
    }
}

interface UserSource {
    String emailFor(int userId);
}

class ReminderService {
    private final UserSource users;

    ReminderService(UserSource users) {
        this.users = users;
    }

    String remind(int userId) {
        return "MAIL to " + users.emailFor(userId) + ": reset your password";
    }
}

class MySQLUserSource implements UserSource {
    public String emailFor(int userId) {
        System.out.println("SELECT email FROM users WHERE id = " + userId);
        return "[email protected]";
    }
}
OutputRan with eclipse-temurin:21-jdk (Java 21.0.12)
SELECT email FROM users WHERE id = 7
MAIL to [email protected]: reset your password
ts
interface UserSource {
  emailFor(userId: number): string;
}

class ReminderService {
  constructor(private readonly users: UserSource) {}

  remind(userId: number): string {
    return `MAIL to ${this.users.emailFor(userId)}: reset your password`;
  }
}

class MySQLUserSource implements UserSource {
  emailFor(userId: number): string {
    console.log(`SELECT email FROM users WHERE id = ${userId}`);
    return "[email protected]";
  }
}

const service = new ReminderService(new MySQLUserSource());
console.log(service.remind(7));
OutputRan with node:22-slim (Node 22.23.1), TypeScript 5.9.3
SELECT email FROM users WHERE id = 7
MAIL to [email protected]: reset your password

The second step is where the refactoring usually goes wrong. Putting UserSource in the MySQL package, next to its implementation, makes the service import from the MySQL package again: the arrow is back where it started. Putting it in a shared interfaces module owned by nobody invites everyone to add methods to it. The interface belongs to the policy, because the policy is the code that decides what it must contain.