Skip to content

Dependency Injection Pattern — Loosely Coupled Dependencies

DodaTech Updated 2026-06-24 4 min read

What You'll Learn

You will learn how Dependency Injection (DI) inverts the responsibility of creating dependencies, making your classes testable, configurable, and decoupled from their concrete collaborators. You will understand constructor injection, setter injection, and the role of DI containers.

Why It Matters

A UserService that directly instantiates a MySQLDatabase is impossible to unit test without a running MySQL instance. By injecting the database dependency, you can swap in a mock or in-memory database during tests. DI is the foundation of most modern application frameworks. The key insight is that a class should not create its own dependencies — they should be provided from the outside, so the class focuses on behaviour, not construction.

Real-World Use

Spring Framework, Guice, and Dagger are entire ecosystems built around DI. DodaTech's module system auto-wires service dependencies at startup, reading bindings from a configuration file so modules never import concrete implementations directly. When testing, the same configuration file can substitute mock implementations, enabling fast, isolated unit tests.

The Pattern

Client depends on an abstraction. Injector (or DI container) creates the concrete dependency and passes it to the client, usually through constructor injection, setter injection, or interface injection.

from abc import ABC, abstractmethod

class Database(ABC):
    @abstractmethod
    def save(self, data: dict) -> None:
        pass

    @abstractmethod
    def find(self, key: str) -> dict:
        pass

class InMemoryDatabase(Database):
    def __init__(self):
        self._store = {}

    def save(self, data: dict) -> None:
        self._store[data["id"]] = data

    def find(self, key: str) -> dict:
        return self._store.get(key, {})

class Logger(ABC):
    @abstractmethod
    def log(self, message: str) -> None:
        pass

class ConsoleLogger(Logger):
    def log(self, message: str) -> None:
        print(f"[LOG] {message}")

class UserService:
    def __init__(self, db: Database, logger: Logger):
        self._db = db
        self._logger = logger

    def create_user(self, user_id: str, name: str):
        self._db.save({"id": user_id, "name": name})
        self._logger.log(f"User {user_id} created")

    def get_user(self, user_id: str) -> dict:
        return self._db.find(user_id)

class Injector:
    @staticmethod
    def create_user_service() -> UserService:
        db = InMemoryDatabase()
        logger = ConsoleLogger()
        return UserService(db, logger)
service = Injector.create_user_service()
service.create_user("1", "Alice")
user = service.get_user("1")
print(f"Found: {user}")
[LOG] User 1 created
Found: {'id': '1', 'name': 'Alice'}

Structure

classDiagram
    class Client {
        -dependency: Abstraction
        +Client(dependency: Abstraction)
    }
    class Abstraction {
        <>
        +operation()
    }
    class ConcreteImplementation {
        +operation()
    }
    class Injector {
        +create(): Client
    }
    Client --> Abstraction : depends on
    ConcreteImplementation ..|> Abstraction
    Injector --> ConcreteImplementation : creates
    Injector --> Client : injects

Real-World Usage

  • Spring Framework@Autowired annotation injects beans by type or name; the IoC container manages the full dependency graph.
  • Angular — component constructors receive injected services; the injector hierarchy supports scoped and Singleton providers.
  • FastAPI / Flask — request-scoped dependencies are injected into route handlers.
  • Dagger — compile-time DI for Android and Java with zero runtime Reflection overhead.
  • Singleton is often used with DI — injected dependencies are frequently singletons within a scope.
  • Factory Method is an alternative to DI for creating dependencies.
  • Strategy is commonly injected into clients.
  • Service Locator is an alternative pattern for dependency lookup (often considered a DI anti-pattern).

Pros and Cons

Pros Cons
Decouples interface from implementation Increases configuration complexity
Greatly improves testability DI container can hide the dependency graph
Follows the Dependency Inversion Principle Over-injection leads to unclear boundaries
Centralised wiring reduces duplication Compile-time verification is harder with runtime DI
Encourages small, focused interfaces Can introduce performance overhead with Reflection-based containers

The code shows constructor injection — UserService receives its Database and Logger dependencies through its constructor. The Injector class acts as a simple DI container, creating the concrete implementations and wiring them together. For testing, you could create a MockLogger and pass it to UserService without changing any service code.

Practice Questions

  1. Implement a simple DI container that resolves constructor dependencies recursively.
  2. Compare constructor injection with setter injection — what are the trade-offs for mandatory vs optional dependencies?
  3. How would you implement a DI container with scoped lifetimes (transient, Singleton, request-scope)?
  4. What issues arise when you have circular dependencies in a DI graph, and how would you resolve them?

Challenge

Implement a DI container that supports both transient and Singleton lifetimes. Transient dependencies create a new instance every time; Singleton dependencies reuse the same instance for the lifetime of the container.

Real-World Task

Run DodaTech's dependency graph visualiser on your application. Identify classes that directly instantiate their dependencies (using new or constructors). Refactor at least one to use constructor injection and measure the improvement in testability.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro