CMD Guide
HomeOO & Low-Level DesignSOLID Principles

DIP in Practice Real-World Examples

The Dependency Inversion Principle (DIP) says high-level modules should not depend on low-level modules; both should depend on abstractions. That sounds tidy on a slide, but the interesting question is mechanical: once a class is written against an interface instead of a concrete type, who supplies the concrete instance, and when? Different answers to that single question produce the four patterns below — dependency injection, the service locator, IoC containers, and plugin architectures. They are not four unrelated tricks; they are four points on one spectrum of how a dependency reaches the code that uses it.

The one question that organizes everything

Every DIP technique is a different answer to: how does an object get the collaborators it needs without naming their concrete classes? Keep three axes in mind as you read, because they are what actually distinguish the patterns:

Hold those three axes; the closing comparison ties every pattern back to them.

diagram
diagram

1. Dependency Injection (DI)

Dependency injection is the most direct answer to the organizing question: the caller supplies the concrete dependency, and it arrives through the constructor — so it is visible in the type signature and bound when the object is created. The class never names a concrete class; it only ever sees the abstraction.

In a web application, a service often needs a data repository. Rather than have the service construct a specific repository, the repository is injected through the constructor:

// Abstraction
interface DataRepository {
    void save(String data);
}

// Low-level implementation
class MySQLRepository implements DataRepository {
    public void save(String data) {
        System.out.println("Saving to MySQL: " + data);
    }
}

// High-level service — depends only on the abstraction
class DataService {
    private final DataRepository repository;

    public DataService(DataRepository repository) {   // injected
        this.repository = repository;
    }

    public void process(String data) {
        repository.save(data);
    }
}

public class Main {
    public static void main(String[] args) {
        DataRepository repo = new MySQLRepository();   // caller chooses
        DataService service = new DataService(repo);
        service.process("Sample Data");
    }
}

Because the concrete choice lives in main (the composition root), swapping MySQL for Postgres or an in-memory fake for tests changes one line and touches no business logic. Crucially, you cannot construct a DataService without handing it a repository — the dependency is impossible to forget. Hold that property; it is exactly what the next pattern gives up.

2. Service Locator

A service locator answers the organizing question differently: instead of the caller passing dependencies in, a class reaches out to a central registry and asks for what it needs by name. The locator itself can be passed as an abstraction, so at a glance it still looks DIP-compliant:

interface Locator {
    PaymentService getPaymentService(String name);
}

class ServiceLocator implements Locator {
    private static final Map services = new HashMap<>();
    public static void register(String name, PaymentService s) { services.put(name, s); }
    public PaymentService getPaymentService(String name) { return services.get(name); }
}

interface PaymentService { void processPayment(double amount); }

class PayPalService implements PaymentService {
    public void processPayment(double amount) {
        System.out.println("PayPal: $" + amount);
    }
}

class Checkout {
    private final Locator locator;
    public Checkout(Locator locator) { this.locator = locator; }

    public void pay(String name, double amount) {
        PaymentService svc = locator.getPaymentService(name);  // hidden dependency
        svc.processPayment(amount);
    }
}

Where the service locator is contested

The pattern technically satisfies DIP's letter — Checkout depends on the Locator and PaymentService abstractions, not on PayPalService. But it works against DIP's spirit of explicit dependencies. Look back at the three axes: the dependency on PaymentService is no longer visible in Checkout's signature. You cannot tell what Checkout needs by reading its constructor — you have to read its method bodies to discover the hidden call to the locator. A caller can construct a Checkout that compiles fine and then fails at runtime because the requested service was never registered. That trades a compile-time guarantee for a runtime surprise.

It is worth being precise about who says what here, because this point is often misattributed. Martin Fowler, in Inversion of Control Containers and the Dependency Injection pattern (2004), treats the service locator and dependency injection as two legitimate options and is deliberately even-handed about choosing between them — he writes that “the choice between Service Locator and Dependency Injection is less important than the principle of separating service configuration from the use of services within an application.” Fowler does not brand the service locator an anti-pattern. The sharper “Service Locator is an anti-pattern” verdict comes from Mark Seemann (Dependency Injection in .NET, and his 2010 essay of that title), whose argument is precisely the explicitness one above: a locator hides the dependencies it dispenses, deferring failures to runtime and obscuring a type's true API. Both views agree on the underlying mechanics; they weigh the trade-off differently. Treat the service locator as a tool with a known cost — hidden dependencies — not as something a single famous author universally condemned.

3. Inversion of Control (IoC) Containers

An IoC container — Spring in Java, the built-in container in .NET — keeps DI's good property (dependencies in the constructor, visible and mandatory) but moves the wiring out of hand-written main code. You declare what each type needs; the container builds the object graph at startup:

@Service
public class OrderService {
    private final PaymentService paymentService;

    public OrderService(PaymentService paymentService) {  // container injects this
        this.paymentService = paymentService;
    }

    public void placeOrder(double amount) {
        paymentService.processPayment(amount);
    }
}

The key distinction from a service locator: OrderService still declares its dependency in the constructor, so its needs remain explicit and checkable. The container is the composition root doing the injection for you — it is not a registry that OrderService reaches into. A container is essentially “DI, automated”; a service locator is “DI, inverted back into a hidden lookup.” That is why containers are widely recommended and locators are contested even though both centralize configuration.

4. Plugin Architecture

A plugin architecture pushes the binding all the way to runtime. The core ships against an abstraction and knows nothing about the implementations; concrete plugins are discovered and loaded while the program runs:

interface AudioDecoder {
    void decode(String fileName);
}

class MP3Decoder implements AudioDecoder {
    public void decode(String fileName) {
        System.out.println("Decoding MP3: " + fileName);
    }
}

class MediaPlayer {
    private final AudioDecoder decoder;
    public MediaPlayer(AudioDecoder decoder) { this.decoder = decoder; }
    public void play(String file) { decoder.decode(file); }
}

The dependency arrow inverts in the strongest sense: the core defines AudioDecoder, and plugins built later — possibly by third parties who never see the core's source — depend on it. New formats arrive by adding a plugin, never by editing the player. This is the same inversion as DI, scaled up to whole modules and deferred to the latest possible moment. Concretely, Java's built-in discovery mechanism is java.util.ServiceLoader: the core calls ServiceLoader.load(AudioDecoder.class) and the JVM finds every implementation declared on the classpath (via META-INF/services or a module-info provides clause) — the player never names MP3Decoder.

DI versus plugins — don't conflate them. DI is about how a single class receives its collaborators (a fine-grained wiring mechanism). A plugin architecture is about designing a system to be extended by independently built modules (a coarse-grained extensibility strategy, usually with runtime discovery). DI is a technique; a plugin architecture is an architectural style that often uses DI internally.

diagram
diagram

Tying it back to the three axes

Map the four patterns onto the axes from the start and the relationships fall out cleanly:

PatternWho chooses the concrete typeDependency visible in signature?Binding time
Dependency InjectionThe caller (composition root)Yes — constructor parameterObject construction
IoC ContainerThe container, from declarationsYes — constructor parameterApplication startup
Service LocatorA central registry, by nameNo — hidden lookup inside methodsCall time
Plugin ArchitectureRuntime discovery / configurationAt the boundary (the plugin interface)Runtime

All four satisfy DIP's core demand — code depends on abstractions, not concretions. They differ in how explicit the dependencies remain and how late they bind. The practical guidance most authors converge on: prefer constructor injection (optionally automated by an IoC container) as the default because it keeps dependencies explicit and failures early; reach for a plugin architecture when you genuinely need third-party or runtime extensibility; and adopt a service locator only with eyes open to the hidden-dependency cost that Mark Seemann (not Fowler) warns about.

Failure fingerprints (how each answer breaks in production)

Each point on the spectrum fails in a characteristic way, and the failure moves later as the binding moves later:

Sources

🤖 Don't fully get this? Learn it with Claude

Stuck on DIP in Practice Real-World Examples? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **DIP in Practice Real-World Examples** (OO & Low-Level Design) and want to truly understand it. Explain DIP in Practice Real-World Examples from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **DIP in Practice Real-World Examples** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **DIP in Practice Real-World Examples** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **DIP in Practice Real-World Examples** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes