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.
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.
| Part | Name | What you learn |
|---|---|---|
| Part 1 | Putting DI on the map | What DI is, and tight vs loose coupling |
| Part 2 | Catalog | DI patterns, DI anti-patterns, and code smells |
| Part 3 | Pure DI and the future | Composition 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
Steps
Part 1: Map
Why loose coupling matters
Part 2: Catalog
Patterns and anti-patterns to copy or avoid
Part 3: Pure DI
Composition Root and object lifetimes
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.
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-pattern | What it looks like | Why it hurts |
|---|---|---|
| Control Freak | A class news up its own dependencies with new | You cannot swap or test the dependency |
| Service Locator | Classes ask a global "locator" for what they need | Dependencies are hidden, not honest |
| Ambient Context | A dependency lives in a static, global spot | Hard to test, easy to forget, surprising |
| Constrained Construction | Constructors must follow a rigid hidden rule | Brittle 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.
| Lifetime | Lives for | Good for |
|---|---|---|
| Transient | A new one every time it is asked for | Lightweight, stateless helpers |
| Scoped | One per web request | Things tied to a single request, like a DbContext |
| Singleton | One for the whole app | Stateless 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.
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
Steps
Week 1
Part 1: tight vs loose coupling
Week 2
Constructor injection in a small app
Week 3
The four anti-patterns
Week 4
Composition Root in Program.cs
Week 5
Object lifetimes and captive deps
Week 6
Pure DI then a container
- 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.
- Week 2 — Constructor injection. Refactor that one place to use constructor injection. Add a guard clause. Notice how the class reads more clearly.
- 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.
- Week 4 — Composition Root. Move all your wiring into
Program.cs. Make sure no other class news up its own services. - Week 5 — Lifetimes. Read the lifestyle chapter. Check every registration. Look for a singleton holding a scoped object.
- 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
- Dependency Injection Principles, Practices, and Patterns — Manning
- Book listing on O'Reilly
- Free Chapter 1 sample (PDF)
- Our Clean Architecture folder structure guide for where the Composition Root lives.
- Our modular monolith article, where clean DI keeps module boundaries honest.