Dependency Inversion Principle: DIP vs DI vs IoC and the Composition Root
DIP asks for the compile-time dependency to flow from details to abstractions and for the abstraction to be owned by its user; DI is only the technique of supplying dependencies from outside, and IoC is control of the flow moving to a framework. Which project the interface belongs in, using Program.cs as the composition root, and why a service locator defeats DIP.
Version note: The code in this post was built and run with C# 14 and .NET 10.
Guess First
This is how the Bookstore sends an order confirmation email. The solution has three projects: Bookstore.Orders holds the order rules, Bookstore.Infrastructure holds outside-world details like SMTP (the email sending protocol), and Bookstore.App is the console application that wires everything together. OrderService depends on an interface and receives it through its constructor:
namespace Bookstore.Infrastructure;
public interface IEmailSender
{
void Send(string to, string subject, string body);
}
public sealed class SmtpEmailSender(string host) : IEmailSender
{
public void Send(string to, string subject, string body) =>
Console.WriteLine($"[smtp://{host}] to={to} subject=\"{subject}\"");
}
using Bookstore.Infrastructure;
namespace Bookstore.Orders;
public sealed record Order(int Id, string CustomerEmail, decimal Total);
public sealed class OrderService(IEmailSender email)
{
public void PlaceOrder(Order order)
{
// save the order, decrease stock...
email.Send(order.CustomerEmail, $"Order #{order.Id} confirmed", $"Total: {order.Total:0.00}");
}
}
Bookstore.Orders.csproj contains a single line:
<ProjectReference Include="..\Bookstore.Infrastructure\Bookstore.Infrastructure.csproj" />
Running it:
[smtp://mail.bookstore.local] to=ada@example.com subject="Order #1042 confirmed"
The code works, there is no new inside OrderService, and you can pass a fake IEmailSender in a test. In code review someone says "this applies DIP (the Dependency Inversion Principle)". Are they right? Second question: marketing wants the email to list the order's items, and the natural place to do that is next to SmtpEmailSender, which means Infrastructure needs access to the Order type. What happens when you add a reference to Orders in Bookstore.Infrastructure.csproj?
Dependency Injection Yes, Dependency Inversion No
The compiler answers the second question. Add the reference, run dotnet build, and the build stops during restore (path shortened):
error MSB4006: There is a circular dependency in the target dependency graph involving target "_GenerateRestoreProjectPathWalk". [.../Bookstore.Infrastructure/Bookstore.Infrastructure.csproj]
0 Warning(s)
1 Error(s)
Interestingly, dotnet add reference writes this cycle into the .csproj without complaint; you only see the error at build time. Orders already depends on Infrastructure, so Infrastructure depending on Orders creates a cycle between the two projects, and MSBuild refuses it.
That also answers the first question: the reviewer is wrong. OrderService uses dependency injection (DI), meaning it receives its dependency from outside. But the compile-time dependency still points the old way: the project holding the business rule references the project holding the SMTP detail. Because the interface sits next to the detail, compiling Orders, testing it or reusing it in another application means dragging Infrastructure and all of its dependencies along. If an SMTP library, a cloud SDK (software development kit) or a database driver is added to Infrastructure tomorrow, all of it leaks into Orders' test project.
DI is a technique; DIP is a rule about direction. You can have one without the other. This post goes deeper into the D of SOLID (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion), which the C# OOP (Object-Oriented Programming) series covered in a single paragraph in "Composition in C# and an Introduction to SOLID": it separates three similar concepts, discusses which project an interface belongs in, and looks at how dependencies are wired in a single place, the composition root.
Three Concepts, Three Different Questions
The Dependency Inversion Principle comes from Robert C. Martin's (the author who collected and popularized the SOLID principles, "Uncle Bob") 1996 article in C++ Report. It has two parts: 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. "High level" here means the policy that explains why the application exists: when an order is placed, the customer is notified. "Low level" is how that policy happens to be carried out today: the SMTP server, the port, the credentials.
The term dependency injection comes from Martin Fowler's (author of Refactoring and Patterns of Enterprise Application Architecture) January 2004 article "Inversion of Control Containers and the Dependency Injection pattern". Fowler explains that calling what the containers of the day did "inversion of control" felt too generic, so they looked for a narrower name. The definition of DI is simple: an object does not create or fetch its dependencies itself; it receives them from outside, through a constructor parameter, a method parameter or a property.
Inversion of Control (IoC) is an older and broader idea than both: control of the flow lives in a framework, not in your code. Fowler's bliki (the blog-and-wiki mix of short articles on his site) entry on IoC traces the term to Ralph Johnson (one of the four authors of Design Patterns) and Brian Foote's 1988 paper "Designing Reusable Classes"; its popular name is the Hollywood principle, "don't call us, we'll call you". ASP.NET Core calling a controller action, the host running BackgroundService.ExecuteAsync, and xUnit finding and running a test method are all examples of IoC. None of them requires DI or DIP.
| Concept | Question it answers | Kind | In the Bookstore |
|---|---|---|---|
| DIP | Which way should the compile-time dependency point? | Design principle | IOrderNotifier in the Orders project |
| DI | How does a dependency reach the object? | Technique | OrderService(IOrderNotifier notifier) |
| IoC | Who drives the flow? | General idea | The host calling a BackgroundService |
| DI container | Who builds the object graph? | Tool | ServiceCollection |
The opening code applies DI but not DIP. The reverse is possible too: a design that follows DIP can be wired by hand with new; a container is just a tool that makes that job easier.
Inverting the Dependency Arrow
The fix is to move the interface. Orders, which uses the interface, becomes the side that defines and owns it; Infrastructure references Orders and implements the interface:
namespace Bookstore.Orders;
public sealed record OrderItem(string Isbn, string Title, int Quantity, decimal UnitPrice);
public sealed record Order(int Id, string CustomerEmail, IReadOnlyList<OrderItem> Items)
{
public decimal Total => Items.Sum(i => i.Quantity * i.UnitPrice);
}
// The consumer (Orders) defines and owns the interface
public interface IOrderNotifier
{
void OrderPlaced(Order order);
}
public sealed class OrderService(IOrderNotifier notifier)
{
public void PlaceOrder(Order order)
{
// save the order, decrease stock...
notifier.OrderPlaced(order);
}
}
using Bookstore.Orders;
namespace Bookstore.Infrastructure;
public sealed class EmailOrderNotifier(string smtpHost) : IOrderNotifier
{
public void OrderPlaced(Order order)
{
// Infrastructure now knows Order: it can list the items
string lines = string.Join(", ", order.Items.Select(i => $"{i.Quantity} x {i.Title}"));
Console.WriteLine($"[smtp://{smtpHost}] to={order.CustomerEmail} subject=\"Order #{order.Id} confirmed\" items={lines}");
}
}
The project references now look like this: Bookstore.Orders.csproj references no project, Bookstore.Infrastructure.csproj references Orders, and Bookstore.App.csproj references both.

Nothing changed at run time: OrderService still calls the notification code, and the call still flows from Orders to Infrastructure. What changed is the direction of the compile-time dependency. The "Architectural principles" page on Microsoft Learn describes it exactly this way: in most applications the compile-time dependency points the same way as the runtime flow, and DIP flips it. That is why the principle has "inversion" in its name. The item list request solved itself as well; Infrastructure now knows Order, and there is no cycle.
Who Owns the Interface?
You may have noticed that the interface's name and shape changed too. IEmailSender.Send(to, subject, body) spoke the language of email; what Orders actually needed was to say "an order was placed, notify whoever cares". When the client owns the interface, you design it around the client's needs and vocabulary. Martin describes this in Agile Software Development: Principles, Patterns, and Practices (2002) as ownership inversion: the abstraction is owned by whoever uses it, not by whoever implements it. The Separated Interface pattern in Fowler's Patterns of Enterprise Application Architecture (2002) expresses the same idea: the interface in one package, the implementation in another.
This design has a side effect: when you want to send an SMS (Short Message Service) or a push notification instead of an email, you write a new class that implements IOrderNotifier, and Orders does not change. Even if IEmailSender lived in Orders, its name and signature would stay locked to email.
.NET itself makes this split at the package level. The ILogger, ILoggerFactory and ILoggerProvider interfaces live in the Microsoft.Extensions.Logging.Abstractions package; the console, debug and third-party providers live in separate packages that implement them. Looking at the .nuspec files in my local NuGet cache, Npgsql 10.0.3, StackExchange.Redis 2.7.27 and Microsoft.Extensions.Caching.Memory 10.0.14 depend only on the Abstractions package for logging, not on the logging implementation package. The library wants to write logs, but the application decides where they go. The dependency flows from the detail to the abstraction, which is exactly what DIP asks for.
The Composition Root: All the Wiring in One Place
If nothing creates its own dependencies, something has to. Mark Seemann (author of Dependency Injection in .NET) calls that place the composition root, and in his 2011 post he describes it as a preferably unique location where modules are composed together, as close as possible to the application's entry point. In a console application that is Main, the top-level Program.cs; in ASP.NET Core it is the same file where registrations go into builder.Services.
using Bookstore.Infrastructure;
using Bookstore.Orders;
using Microsoft.Extensions.DependencyInjection;
// Composition root: concrete class names appear only here
ServiceCollection services = new();
services.AddSingleton<IOrderNotifier>(new EmailOrderNotifier("mail.bookstore.local"));
services.AddTransient<OrderService>();
using ServiceProvider provider = services.BuildServiceProvider(
new ServiceProviderOptions { ValidateOnBuild = true, ValidateScopes = true });
OrderService orders = provider.GetRequiredService<OrderService>();
orders.PlaceOrder(new Order(1042, "ada@example.com",
[new OrderItem("978-0441013593", "Dune", 2, 12.99m), new OrderItem("978-0141439518", "Emma", 1, 7.50m)]));
[smtp://mail.bookstore.local] to=ada@example.com subject="Order #1042 confirmed" items=2 x Dune, 1 x Emma
The name EmailOrderNotifier appears only in this file. A container is not mandatory, you could build the same graph with two new calls; but if you do use one, there is a good reason to keep ValidateOnBuild on. Forget the IOrderNotifier registration and the error arrives while the container is built, not when the first order comes in:
Unhandled exception. System.AggregateException: Some services are not able to be constructed (Error while validating the service descriptor 'ServiceType: Bookstore.Orders.OrderService Lifetime: Transient ImplementationType: Bookstore.Orders.OrderService': Unable to resolve service for type 'Bookstore.Orders.IOrderNotifier' while attempting to activate 'Bookstore.Orders.OrderService'.)
The container can only run this check because OrderService's constructor states clearly what it needs. Seemann underlines one more thing in the same post: only applications have a composition root; libraries and frameworks do not. A library can offer a registration extension method like AddXxx, but the decision about which implementation to use is made at the application's entry point. Lifetimes and the container itself are covered in "Dependency Injection Basics in C#" in the Advanced C# series and in the DI post of the ASP.NET Core Web API (Application Programming Interface) series.
Why a Service Locator Defeats DIP
The most common way to break the composition root idea is to carry the container into the application. Give OrderService an IServiceProvider and let it ask for what it needs:
using Microsoft.Extensions.DependencyInjection;
public sealed class OrderService(IServiceProvider services)
{
public void PlaceOrder(Order order)
{
// save the order, decrease stock...
IOrderNotifier notifier = services.GetRequiredService<IOrderNotifier>();
notifier.OrderPlaced(order);
}
}
Run the program with the same forgotten registration:
Container built.
OrderService resolved.
Unhandled exception. System.InvalidOperationException: No service for type 'Bookstore.Orders.IOrderNotifier' has been registered.
Even with ValidateOnBuild on, the container was built without complaint, because IServiceProvider can always be resolved. The error appears the moment the first order is placed, that is, in production. That is not the only loss. OrderService's constructor now lies: someone looking from outside cannot see that this class needs to send notifications; they have to read method bodies to find out. In tests you discover which services to register by trial and error. On top of that, this change forced the Orders project to reference the Microsoft.Extensions.DependencyInjection.Abstractions package: the business rule now depends on the tool that assembles it.
In his 2004 article Fowler discusses the service locator as a legitimate alternative to DI, but he states the key difference plainly: every class that uses a locator depends on the locator itself. That is why Seemann called the service locator an anti-pattern in 2010. Microsoft's .NET DI guidelines take the same position: don't fall back on the service locator pattern by calling GetService where you can use DI, and injecting a factory that resolves dependencies at run time is a variation of the same thing. The legitimate place for GetRequiredService is the composition root itself, such as resolving the root of the graph once in the Program.cs above.
When Is a Single-Implementation Interface Unnecessary?
A common reflex in teams that have just learned DIP is to give every class an I-prefixed twin: OrderService and IOrderService, PriceCalculator and IPriceCalculator. Fowler calls these header interfaces: a one-to-one copy of a class's public methods, like a C++ header file. The role interface he contrasts them with describes what a client expects from a collaborator. IOrderNotifier is a role interface; IOrderService is usually a header interface.
The cost of a header interface looks small but adds up. Every signature change happens in two files, "Go to Definition" in the IDE (a development environment such as Visual Studio or Rider) takes you to the interface instead of the implementation, and since nobody implements it differently, the promise it makes is never tested. Most importantly, the dependency direction never changes: as long as IOrderService and OrderService sit in the same project, you have gained nothing in DIP terms.
The places where an interface really pays off are well defined. First, process boundaries: database, email, file system, clock, HTTP (Hypertext Transfer Protocol); Seemann and Steven van Deursen, in their book Dependency Injection Principles, Practices, and Patterns (2019), call these volatile dependencies because they cannot be controlled in tests or vary by environment. That is why TimeProvider arrived in .NET 8. Second, behaviors that really have more than one implementation today. Third, places where you need to invert the dependency arrow at a project boundary, as above. Outside of these, using a pure calculation class like PriceCalculator or a Money value object directly is perfectly fine. Writing new Money(10m, "EUR") is not a DIP violation; depending on something stable and side-effect free carries no risk. When an interface becomes necessary, the IDE's "Extract Interface" refactoring produces it in a minute.
Common Pitfalls
Putting the interface next to the implementation. This is the essence of the opening example: as long as IEmailSender lives in Infrastructure, it does not matter how clean the constructor injection looks, the arrow still points the wrong way. "Where does the interface live, and whose language does it speak?" is the litmus test for DIP.
Drawing the abstraction in the shape of the detail. An interface such as ISqlOrderRepository or IRepository.ExecuteSql(string) is technically an abstraction, but it violates Martin's second clause: the abstraction depends on the detail. If things the client should never see (SQL, HTTP status codes, SMTP ports) leak into the interface, the wrong side owns it.
Scattering the composition root. Building a new ServiceCollection() inside a class, keeping a static ServiceLocator.Current, or storing IApplicationBuilder.ApplicationServices in a static field; Microsoft's guidelines explicitly list the last one among things to avoid. They all lead to the same place: dependencies become invisible from the constructor.
Mistaking DI for DIP, or the other way around. Using a container does not prove the design points the right way, and not using one does not mean it breaks DIP. What you should check is the project reference graph: if you see an infrastructure project or package in the business rule project's .csproj, the arrow points the wrong way.
Trying to solve layering rules here. DIP tells you the direction between two modules; how an entire solution is split into layers and how the "dependency rule" is protected belong to the upcoming "Clean, Onion and Hexagonal Architecture" series. We will compute metrics between projects in the 11th post of this series.
A Small Experiment
★☆☆ Write a ConsoleOrderNotifier that implements IOrderNotifier and use it by changing only the registration in Program.cs. How many files did you have to touch in the Orders or Infrastructure projects?
★★☆ Create an xUnit test project for Orders and verify with a fake IOrderNotifier that OrderService.PlaceOrder calls the notification exactly once. Does the test project need a reference to Infrastructure? Did it need one in the opening design?
★★★ Send both an email and an SMS for an order. Do it not by adding a second parameter to OrderService, but with a composite class that implements IOrderNotifier and calls each notifier in its list in turn. Which project should the composite class live in, and why?
With this post we have covered all five letters of SOLID. In the next post we step outside the letters and look at the three most quoted and most misunderstood acronyms: DRY (Don't Repeat Yourself), KISS (Keep It Simple, Stupid) and YAGNI (You Aren't Gonna Need It). Duplication of knowledge rather than code, the price of merging accidental similarity, and the four costs of saying "we don't need it yet".