Skip to main content
๐Ÿ“œ WAYPOINT LESSON

Dependency Inversion and Constructor Injection

โญโญ intermediateโณ 17 min read๐Ÿ“ Lesson 120 of 265

Depend on abstractions, receive them from outside, and watch unit tests get easy.

The dependency problem

A OrderService that news up its own SqlEmailer is welded to it: can't test without a database, can't swap providers, can't see what it needs. Dependency INVERSION flips the arrow: high-level policy (OrderService) defines the abstraction it needs; low-level detail (SqlEmailer) implements it:

public interface IEmailer { void Send(string to, string body); }

public sealed class OrderService
{
    private readonly IEmailer _emailer;
    public OrderService(IEmailer emailer) => _emailer = emailer;   // injected

    public void Place(Order o)
    {
        // ... business rules ...
        _emailer.Send(o.Email, "order confirmed");
    }
}

CONSTRUCTOR INJECTION: dependencies arrive as constructor parameters, stored in readonly fields. The class can't be constructed half-wired.

The testability payoff

A fake replaces the real thing in tests โ€” no network, no database:

public sealed class FakeEmailer : IEmailer
{
    public System.Collections.Generic.List<(string To, string Body)> Sent = new();
    public void Send(string to, string body) => Sent.Add((to, body));
}

var fake = new FakeEmailer();
var svc = new OrderService(fake);
svc.Place(order);
// assert fake.Sent has the notification

This is why injected classes need no mocking frameworks for unit tests.

What NOT to inject

Value objects, DTOs, and truly static utilities stay as-is. Inject behaviors with state or I/O: gateways, repositories, clocks, config providers. Two constructor parameters + a news-up inside is fine; five is a design smell the class is doing too much.

Check your understanding

  • Who owns the interface โ€” the consumer or the implementation? (The consumer; detail implements it.)
  • Why readonly fields for injected dependencies? (Fixed at construction โ€” no half-wired instances.)