top of page
Programming Console

“D” Is for Dependency Injection, Not Just Fancy Wiring The Dependency Inversion Principle: Stop hardcoding your heartbreak.

  • Writer: Maryanne
    Maryanne
  • May 8
  • 3 min read

Let’s get one thing straight: Dependency Injection is not the same as the Dependency Inversion Principle. The former is a design pattern, the latter is a foundational principle. Confusing the two is like thinking that buying a yoga mat makes you enlightened.


The “D” in SOLID is for Dependency Inversion, and it’s not about DI containers, fancy frameworks, or using attributes to summon your dependencies from the void. It’s about turning your code’s dependency direction upside down—so that high-level business logic doesn’t depend directly on low-level plumbing.

Let’s break it down.




What Dependency Inversion Actually Means

The Principle: High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.


Translation: Your business logic should define the contract. The messy details (database, email, APIs) should conform to it—not the other way around.

It’s about flipping the dependency hierarchy so that core logic stays clean, testable, and blissfully unaware of the chaos under the hood.


A Quick and Dirty Example (C# Style)

Before:

public class OrderService {
    private readonly SmtpClient _smtpClient = new SmtpClient();

    public void SendOrderConfirmation(string email)
    {
        _smtpClient.Send(new MailMessage("orders@company.com", email, 			
		"Thanks!", "Your order is confirmed."));
    }
}

This is classic tight coupling. OrderService is directly dependent on a low-level detail—SmtpClient. Good luck testing or swapping that out later.

Now let’s apply Dependency Inversion:

public interface IEmailSender
{
    void SendEmail(string to, string subject, string body);
}

public class SmtpEmailSender : IEmailSender
{
    public void SendEmail(string to, string subject, string body)
    {
        // use SmtpClient here
    }
}

public class OrderService
{
    private readonly IEmailSender _emailSender;

    public OrderService(IEmailSender emailSender)
    {
        _emailSender = emailSender;
    }

    public void SendOrderConfirmation(string email)
    {
        _emailSender.SendEmail(email, "Thanks!", "Your order is confirmed.");
    }
}

Now the high-level logic (OrderService) depends only on an abstraction (IEmailSender). The implementation can be anything: SMTP, SendGrid, smoke signals—you decide.


So Where Does Dependency Injection Come In?

Dependency Injection is just a delivery mechanism. It’s one way to supply the abstracted dependencies you defined through Dependency Inversion.

In our example above, we injected IEmailSender through the constructor. That’s constructor injection—just one form of Dependency Injection.

The important point: You can follow Dependency Inversion without using a DI container. You can also use a DI container and still completely botch the principle if your abstractions are upside down or meaningless.


Why It Matters in the Real World (Especially Legacy Code)

In old codebases, you often find logic welded to implementations like a Frankenstein’s monster of hardcoded constructors, static calls, and sad little new statements littering every class.

Dependency Inversion gives you seams. It lets you tease apart the system, introduce testability, swap out services gradually, and avoid pulling a thread that unravels the whole codebase.

It's not just academic purity—it’s a strategy for untangling legacy monsters and building systems that can change without collapsing.


How to Start Applying It Without a Full Rewrite

  1. Start with One Pain Point Take one place where tight coupling hurts—maybe sending an email or accessing the database—and invert that dependency. Introduce an interface. Create a thin implementation.

  2. Manual Injection Is Totally Fine Don’t get distracted by containers. Just pass dependencies through constructors at first. You’re practicing the principle, not doing ritual DI worship.

  3. Only Abstract Real Variability If you’re never going to swap out a string formatter, don’t abstract it. Focus on external dependencies or logic with multiple implementations.

  4. Use the Right Names Avoid leaking implementation into your interface names. Prefer IEmailSender over ISmtpService. The abstraction should describe what, not how.

  5. Test What Matters With your logic decoupled, you can now mock or stub the abstractions and test the core logic in isolation. Clean code, clean tests.

Watch Out for These Anti-Patterns

  • Service Locator Abuse: Hiding dependencies behind a global container is still tight coupling, just sneakier.

  • One Interface per Class Just Because: Don’t create interfaces for the sake of it. Abstract behaviors, not every single class.

  • Over-Abstracting Internal Logic: If you’re abstracting simple math or sorting logic, ask yourself why.


TL;DR: Inversion Gives You Control

Dependency Inversion puts your business logic in charge. It says, “I define the rules. You (low-level detail) can play if you follow them.” It’s not just about being testable—it’s about making sure your system is adaptable.

And yes, you might use Dependency Injection to implement it. But inversion is the goal—not the injection itself.

So don’t confuse the pattern for the principle. One helps you write cleaner code. The other helps you design cleaner systems.

Comments


bottom of page