Skip to main content
SEMastery
book

Dependency Injection Book Review and Study Guide (Seemann and van Deursen)

A full review and study guide for Dependency Injection Principles, Practices, and Patterns, with good vs bad C# examples and a 6-week plan.

14 min readUpdated October 30, 2025

What this book is

Dependency Injection Principles, Practices, and Patterns is a book by Mark Seemann and Steven van Deursen. It is a bigger, rewritten version of an older classic called Dependency Injection in .NET. The whole book is about one big idea: how to wire the parts of your program together so the code stays clean, easy to test, and easy to change.

It is published by Manning. The code examples are in C#, but the ideas work in any object-oriented language. Steven van Deursen also built and maintains the Simple Injector DI library, so the authors really know this topic from the inside.

This page is a review plus a study guide. By the end you will know what the book teaches, see good and bad C# side by side, learn who should read it, and have a week-by-week plan to follow.

A real-life way to think about it

Imagine you are baking a cake. You need eggs, flour, and sugar.

There are two ways to get them. In the first way, you walk into the kitchen and make everything yourself. You raise the chickens for eggs. You grow the wheat for flour. You can never bake a cake without first doing all that work. That is slow and silly.

In the second way, someone hands you a bowl with the eggs, flour, and sugar already measured out. You just bake. If they hand you gluten-free flour instead, you do not care. You still bake the same way.

Dependency injection is the second way. Your class does not go out and build the things it needs. Someone hands those things to it. The "someone" is usually the constructor. This small change makes a huge difference, and the whole book explains why.

// The "make everything yourself" way (hard to change, hard to test)
public class CakeBaker
{
    private readonly Oven _oven = new GasOven(); // built inside, locked in
 
    public Cake Bake() => _oven.Heat();
}
 
// The "someone hands it to you" way (dependency injection)
public class CakeBaker
{
    private readonly IOven _oven;
 
    public CakeBaker(IOven oven) // handed in through the constructor
    {
        _oven = oven;
    }
 
    public Cake Bake() => _oven.Heat();
}

In the second version you can hand in a real oven in production and a fake oven in a test. The baker never changes. That is the heart of the whole book.

How the book is laid out

The book is split into three parts. Knowing the shape helps you plan your reading.

PartNameWhat you learn
Part 1Putting DI on the mapWhat DI is, and tight vs loose coupling
Part 2CatalogDI patterns, DI anti-patterns, and code smells
Part 3Pure DI and the futureComposition Root, object lifetime, interception, and using a container

Part 1 sets up the problem. Part 2 is a reference catalog you will come back to for years. Part 3 shows how to put it all together in a real application.

The three parts of the book

Part 1: Map
Part 2: Catalog
Part 3: Pure DI

Steps

1

Part 1: Map

Why loose coupling matters

2

Part 2: Catalog

Patterns and anti-patterns to copy or avoid

3

Part 3: Pure DI

Composition Root and object lifetimes

Read them in order the first time, then treat Part 2 as a reference.

The big idea: loose coupling

Two classes are tightly coupled when one cannot live without the exact other one. Change one, and the other breaks. Loose coupling is the opposite. A class depends on a small promise (an interface), not on a specific concrete class.

The book makes a strong claim that surprised a lot of readers: you should depend on abstractions you own, and you should let the caller decide what the real thing is. This keeps your important code free of details like which database or which email service you picked.

// An abstraction the application owns
public interface IGreetingService
{
    string Greet(string name);
}
 
// A concrete detail that can be swapped out
public class EnglishGreetingService : IGreetingService
{
    public string Greet(string name) => $"Hello, {name}!";
}

The class that uses IGreetingService does not know or care that the real one speaks English. You could swap in a French one tomorrow and not touch a single line of the user code.

Constructor injection: the default move

The book teaches several ways to inject dependencies. The one it tells you to reach for first is constructor injection. You list what a class needs in its constructor. You store it. You use it. That is it.

public class OrderProcessor
{
    private readonly IPaymentGateway _payments;
    private readonly IEmailSender _email;
 
    public OrderProcessor(IPaymentGateway payments, IEmailSender email)
    {
        // Guard clauses: fail fast if someone forgets to hand one in
        _payments = payments ?? throw new ArgumentNullException(nameof(payments));
        _email = email ?? throw new ArgumentNullException(nameof(email));
    }
 
    public void Process(Order order)
    {
        _payments.Charge(order.Total);
        _email.Send(order.CustomerEmail, "Thanks for your order!");
    }
}

Now you can read the class and instantly know what it needs. The dependencies are clear and honest. There is nowhere to hide a secret dependency.

The book also covers method injection (pass the dependency into a single method) and property injection (set it through a property). It explains when each fits, but constructor injection stays the safe default.

The Composition Root

This is the chapter people remember most. The Composition Root is the single place in your app where you build the object graph. All the new calls and all the container setup live here, near the start of the program. Nothing else in the app builds its own dependencies.

Think of it like the wiring closet in a building. All the cables meet in one room. The lights in each room just turn on; they do not know how the wiring works. If you keep one wiring closet, the building is easy to understand. If every room has its own secret wiring, the building is a mess.

The Composition Root is the one place where everything gets wired together.

In a modern .NET app the Composition Root usually lives in Program.cs. Here is the same idea using the built-in container.

var builder = WebApplication.CreateBuilder(args);
 
// The Composition Root: wire abstractions to concrete classes here, and only here
builder.Services.AddScoped<IPaymentGateway, StripePaymentGateway>();
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<OrderProcessor>();
 
var app = builder.Build();

The key rule: keep this wiring in one place, as close to the entry point as you can. When you read the book's advice next to a clean folder layout, it clicks fast. We cover that layout in our guide to the Clean Architecture folder structure.

The DI anti-patterns

Part 2 has the famous catalog of things not to do. The book names four main anti-patterns. Learning to spot them is half the value of the book.

Anti-patternWhat it looks likeWhy it hurts
Control FreakA class news up its own dependencies with newYou cannot swap or test the dependency
Service LocatorClasses ask a global "locator" for what they needDependencies are hidden, not honest
Ambient ContextA dependency lives in a static, global spotHard to test, easy to forget, surprising
Constrained ConstructionConstructors must follow a rigid hidden ruleBrittle wiring that breaks in odd ways

Control Freak

A "Control Freak" class insists on building its own dependencies. It uses new inside itself. That locks the class to one concrete thing forever.

// BAD: Control Freak. It builds its own gateway, so you can never swap it.
public class OrderProcessor
{
    private readonly StripePaymentGateway _payments = new StripePaymentGateway();
 
    public void Process(Order order) => _payments.Charge(order.Total);
}
 
// GOOD: hand the dependency in, and depend on the interface
public class OrderProcessor
{
    private readonly IPaymentGateway _payments;
    public OrderProcessor(IPaymentGateway payments) => _payments = payments;
 
    public void Process(Order order) => _payments.Charge(order.Total);
}

Service Locator

A "Service Locator" is a global object you ask for things at runtime. It looks helpful, but the book argues hard against it. The problem is honesty. You can no longer read a constructor and know what a class needs. The needs are hidden inside the method bodies.

// BAD: Service Locator. The real dependencies are hidden inside the methods.
public class OrderProcessor
{
    public void Process(Order order)
    {
        var payments = ServiceLocator.Get<IPaymentGateway>(); // surprise dependency
        var email = ServiceLocator.Get<IEmailSender>();        // another hidden one
        payments.Charge(order.Total);
        email.Send(order.CustomerEmail, "Thanks!");
    }
}

From the outside, this class looks like it needs nothing. That is the trap. Constructor injection makes the same needs visible and honest.

Ambient Context

An "Ambient Context" hides a dependency in a static, global place so any code can grab it. A common example is reading the current time or current user from a global static. It feels easy, but tests become flaky and the dependency is invisible.

// BAD: Ambient Context. The time comes from a global static.
public class TrialChecker
{
    public bool IsExpired(Trial trial) => DateTime.Now > trial.EndsOn;
}
 
// GOOD: inject an abstraction for the clock so tests can control "now"
public interface IClock { DateTime Now { get; } }
 
public class TrialChecker
{
    private readonly IClock _clock;
    public TrialChecker(IClock clock) => _clock = clock;
 
    public bool IsExpired(Trial trial) => _clock.Now > trial.EndsOn;
}

Now a test can hand in a fake clock set to any date. No magic, no flaky results.

Constrained Construction

"Constrained Construction" happens when your wiring forces every class to have a special constructor shape, often so a tool can build them by reflection with a connection string. The book shows how this rigid rule leads to fragile setup that fails at runtime instead of at compile time.

Object lifetimes

The book also explains how long an injected object should live. This matters a lot in web apps. .NET gives three main lifetimes, and the book's "lifestyle" chapter maps almost one-to-one to them.

LifetimeLives forGood for
TransientA new one every time it is asked forLightweight, stateless helpers
ScopedOne per web requestThings tied to a single request, like a DbContext
SingletonOne for the whole appStateless shared services, caches, config

Pick the wrong lifetime and you get nasty bugs. The classic mistake, which the book warns about, is a "captive dependency": a long-lived singleton that holds onto a short-lived scoped object. The scoped object then lives far longer than it should, which can corrupt data across requests.

Lifetimes decide how long an injected object survives.

Pure DI vs a container

One refreshing choice in the book is that it teaches Pure DI first. Pure DI means wiring everything by hand, with plain new calls in the Composition Root. No container, no magic.

// Pure DI: just plain constructors, wired by hand in one place
IPaymentGateway payments = new StripePaymentGateway(apiKey);
IEmailSender email = new SmtpEmailSender(smtpHost);
var processor = new OrderProcessor(payments, email);

The authors argue you should understand Pure DI before you trust a container to do it for you. A container is just an automatic helper for the same job. If you do not understand the hand-wired version, the container becomes a black box that hides bugs.

A note on modern .NET

The book's ideas have aged very well. On .NET 10, which is the current LTS release, the built-in Microsoft.Extensions.DependencyInjection container is everywhere, and the book's lessons map straight onto it. C# 14 has shipped, and C# 15 brings union types in the .NET 11 preview, but none of that changes the core advice. Good wiring is good wiring.

One modern wrinkle the book predates: some popular helper libraries, like MediatR, MassTransit, and AutoMapper, have moved to commercial licenses. That is worth knowing when you pick tools, but it does not change the DI principles themselves. You can apply every lesson in this book using only the free, built-in container.

Who this book suits

  • Mid-level .NET developers who use a container but do not fully understand it. This book fills that gap better than anything else.
  • Architects who want a shared vocabulary for code reviews. Saying "that's a Service Locator" ends an argument fast.
  • Anyone learning Clean Architecture or a modular monolith. DI is the glue that holds those styles together.

It is less ideal as a first-ever programming book. You should be comfortable writing C# classes and interfaces before you start.

A 6-week study plan

Here is a steady plan. One focused session per week is enough.

6-week study plan

Week 1
Week 2
Week 3
Week 4
Week 5
Week 6

Steps

1

Week 1

Part 1: tight vs loose coupling

2

Week 2

Constructor injection in a small app

3

Week 3

The four anti-patterns

4

Week 4

Composition Root in Program.cs

5

Week 5

Object lifetimes and captive deps

6

Week 6

Pure DI then a container

Read a little, then build a little. Repeat.
  1. Week 1 — The problem. Read Part 1. Take a tiny app you already have and find one place that is tightly coupled. Write it down.
  2. Week 2 — Constructor injection. Refactor that one place to use constructor injection. Add a guard clause. Notice how the class reads more clearly.
  3. Week 3 — Anti-patterns. Read the anti-pattern catalog. Hunt your own codebase for a Control Freak or a Service Locator. You will find one.
  4. Week 4 — Composition Root. Move all your wiring into Program.cs. Make sure no other class news up its own services.
  5. Week 5 — Lifetimes. Read the lifestyle chapter. Check every registration. Look for a singleton holding a scoped object.
  6. Week 6 — Pure DI then container. Wire one feature by hand with Pure DI. Then re-do it with the built-in container. Now the container is no longer magic.

Tips while you read

  • Type the examples yourself. Reading code is not the same as writing it.
  • After each anti-pattern, search your own repo for it. Real examples stick.
  • Keep a small "DI cheat sheet" of the four anti-patterns on a sticky note.
  • Pair the book with our Clean Architecture folder structure guide so the Composition Root has a natural home.

Quick recap

  • The book teaches dependency injection from the ground up, in three parts: map, catalog, and Pure DI.
  • The core idea is loose coupling: depend on small abstractions, and let the caller hand in the real thing.
  • Constructor injection is the safe default. It makes a class's needs honest and visible.
  • The Composition Root is the one place where you wire everything together, near the app's entry point.
  • The four anti-patterns to avoid are Control Freak, Service Locator, Ambient Context, and Constrained Construction.
  • Object lifetimes (transient, scoped, singleton) matter; watch out for captive dependencies.
  • Learn Pure DI first, then let a container automate it. The ideas still fit .NET 10 and C# 14 perfectly.

References and further reading