Design Patterns Dependency Injection

Dependency Injection (DI) supplies an object with the other objects it needs from the outside. The object never builds its own helpers. Swapping, testing, and reusing parts becomes easy because the connections live in one place.

The Real-World Analogy

A desk lamp does not grow its own bulb. Someone screws in the bulb, and the lamp works with a warm bulb, a bright bulb, or an energy-saving bulb. The lamp depends on a socket standard and accepts whatever bulb arrives.

Problem Without DI

class OrderService {
    private EmailSender sender = new EmailSender();   // built inside
    void confirm(Order o) { sender.send(o); }
}

OrderService ───creates──▶ EmailSender
    Locked to email. A test sends real emails. SMS support needs edits.

Solution With DI

              ┌───────────────┐
 Assembler ──▶│ OrderService  │
 (outside)    └───────┬───────┘
                      │ uses
                      ▼
              ┌───────────────┐
              │ «interface»   │
              │ Notifier      │
              └───────▲───────┘
            ┌─────────┴────────┐
       EmailNotifier      SmsNotifier      FakeNotifier (tests)

The assembler picks the real notifier and hands it over. OrderService only knows the Notifier interface.

Three Ways to Inject

Constructor Injection

Dependencies arrive through the constructor. The object becomes ready to use the moment it exists. Most teams prefer this style.

Setter Injection

Dependencies arrive through setter methods after creation. The style suits optional helpers.

Interface Injection

The dependency offers an injection method that the client implements. Daily work rarely uses it.

Code Example

interface Notifier { void send(String message); }

class EmailNotifier implements Notifier {
    public void send(String message) { print("Email: " + message); }
}

class SmsNotifier implements Notifier {
    public void send(String message) { print("SMS: " + message); }
}

class OrderService {
    private final Notifier notifier;

    OrderService(Notifier notifier) {         // constructor injection
        this.notifier = notifier;
    }

    void confirm(String orderId) {
        notifier.send("Order " + orderId + " confirmed");
    }
}

// the assembler decides
OrderService service = new OrderService(new SmsNotifier());
service.confirm("A-101");

// a test supplies a fake
class FakeNotifier implements Notifier {
    String last;
    public void send(String message) { last = message; }
}

DI Containers

Large applications contain hundreds of connections. A DI container reads a configuration, builds objects, and wires them automatically. Spring for Java, ASP.NET Core for C#, and Angular for TypeScript all ship with containers.

Configuration:  Notifier → SmsNotifier
                     │
                     ▼
              ┌──────────────┐   builds   ┌──────────────┐
 Request ───▶ │  Container   │ ─────────▶ │ OrderService │
              └──────────────┘  injects   └──────────────┘
                                 SmsNotifier

Dependency Injection Versus Inversion

Dependency Inversion is the principle: depend on abstractions. Injection, the technique, supplies those abstractions at runtime. Developers use the technique to honor the principle.

Benefits in Daily Work

SituationGain From DI
Unit testsFake helpers replace slow or risky ones
Feature switchesOne line selects a new implementation
Team projectsDevelopers work against interfaces in parallel
ConfigurationDevelopment and production use different parts

Advantages and Disadvantages

AdvantagesDisadvantages
Low couplingMore setup code
Easy testingContainers hide object creation
Flexible configurationConstructors with many parameters signal design trouble

Step-by-Step Walkthrough

Follow the order example. The assembler creates an SmsNotifier and passes it into the OrderService constructor. OrderService stores the reference in a final field. Calling confirm() sends the message through whichever notifier arrived.

A test builds the service with a FakeNotifier and checks the last message after the call. No real message leaves the computer, and the test finishes in milliseconds.

Where to Assemble Objects

Wiring belongs near the entry point of the application, often called the composition root. Every other class receives its helpers and never builds them. A single place for wiring makes changes simple to find.

Key Takeaways

  • DI hands dependencies to an object instead of letting it create them.
  • Constructor injection suits required parts, and setter injection suits optional parts.
  • Containers automate wiring in large systems.
  • DI delivers the Dependency Inversion principle in practice.