Card 02/ 09

CodeDifficulty: Beginner2 min

Spotting a Dependency Inversion Violation

ReminderService creates its own MySQLDatabase, so a test needs a real MySQL server.

A dependency inversion violation shows up as business code that creates its own database connection, mail client or HTTP client. This card shows a ReminderService that does so, in Python, Java and TypeScript, and what happens when a test tries to run it. Each MySQLDatabase here stands in for a real driver and fails the way a driver does when no server is listening.

A ReminderService that creates its own database

py
class MySQLDatabase:
    def __init__(self, url):
        raise ConnectionError(f"no MySQL server at {url}")

    def find_email(self, user_id): ...


class ReminderService:
    def __init__(self):
        self.database = MySQLDatabase("db.internal:3306")

    def remind(self, user_id):
        email = self.database.find_email(user_id)
        return f"MAIL to {email}: reset your password"


def test_remind():
    service = ReminderService()
    assert service.remind(7) == "MAIL to [email protected]: reset your password"


try:
    test_remind()
except ConnectionError as error:
    print(f"test_remind failed: ConnectionError: {error}")
OutputRan with python:3.12-slim (Python 3.12.13)
test_remind failed: ConnectionError: no MySQL server at db.internal:3306
java
public class DipViolation {
    static void testRemind() {
        ReminderService service = new ReminderService();
        System.out.println(service.remind(7));
    }

    public static void main(String[] args) {
        try {
            testRemind();
        } catch (IllegalStateException error) {
            System.out.println("testRemind failed: " + error.getMessage());
        }
    }
}

class MySQLDatabase {
    MySQLDatabase(String url) {
        throw new IllegalStateException("no MySQL server at " + url);
    }

    String findEmail(int userId) { return null; }
}

class ReminderService {
    private final MySQLDatabase database = new MySQLDatabase("db.internal:3306");

    String remind(int userId) {
        return "MAIL to " + database.findEmail(userId) + ": reset your password";
    }
}
OutputRan with eclipse-temurin:21-jdk (Java 21.0.12)
testRemind failed: no MySQL server at db.internal:3306
ts
class MySQLDatabase {
  constructor(url: string) {
    throw new Error(`no MySQL server at ${url}`);
  }

  findEmail(userId: number): string {
    return "";
  }
}

class ReminderService {
  private readonly database = new MySQLDatabase("db.internal:3306");

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

function testRemind(): void {
  const service = new ReminderService();
  console.log(service.remind(7));
}

try {
  testRemind();
} catch (error) {
  console.log(`testRemind failed: ${(error as Error).message}`);
}
OutputRan with node:22-slim (Node 22.23.1), TypeScript 5.9.3
testRemind failed: no MySQL server at db.internal:3306

The test fails in every language, and not because the reminder logic is wrong. It fails because creating a ReminderService creates a MySQLDatabase, and there is no server at db.internal:3306 on the machine running the test. To test one line of business logic, someone has to start a database, create the tables and load a user.

Look at the highlighted line. The new or the constructor call inside the service is the violation: the policy chooses its own detail, by name, and nobody outside can change that choice. Other signs have the same cause. An import of a driver module at the top of a file of business rules, a connection string in a class that computes discounts, or a test suite that only runs on a machine with a database are all the same arrow pointing the wrong way.

The cost is not only in tests. Moving users to another store means editing ReminderService, a class whose rules did not change. Policy and detail are tied together, so a change to either reaches the other.