Introduction
Dependency Injection is almost unavoidable in modern .NET development. Open an ASP.NET Core project and you'll quickly find interfaces, constructors, service registrations, and dependencies being wired together.
And that's a good thing—until it isn't.
DI helps us write loosely coupled and testable code, but as applications grow, we can sometimes take the idea too far. More interfaces, more services, and more dependencies don't automatically mean better architecture. In fact, they can make a codebase harder to navigate and even harder to understand.
Consider a typical service:
public class OrderService : IOrderService
{
private readonly IOrderRepository _orderRepository;
private readonly INotificationService _notificationService;
private readonly ILogger<OrderService> _logger;
public OrderService(
IOrderRepository orderRepository,
INotificationService notificationService,
ILogger<OrderService> logger)
{
_orderRepository = orderRepository;
_notificationService = notificationService;
_logger = logger;
}
}
Nothing looks wrong here. In fact, this is perfectly reasonable DI.
But now imagine that each of those dependencies has several dependencies of its own, those dependencies are registered in different projects, and some implementations are hidden behind extension methods or factories.
Suddenly, answering a simple question like "What does this service actually depend on?" requires following a dependency graph across the application.
Dependency Injection isn't the problem. Overusing it is.
What DI Actually Solves
Dependency Injection primarily separates object construction from object usage. Instead of a class deciding how to create its dependencies, those dependencies are provided to it.
Without DI
OrderService
+-- creates OrderRepository
With DI
OrderService
+-- receives IOrderRepository
↑
Composition Root
This gives us flexibility, controlled lifetimes, easier substitution, and better testability.
But there is an important trade-off: the implementation and the place where that implementation is selected are now separated.
DI can reduce coupling around object construction without necessarily reducing the conceptual complexity of the application.
The Hidden Cost: Following the Dependency Graph
When a service depends on interfaces, understanding that service may require finding each registered implementation and then inspecting that implementation's dependencies.
JobService
¦
+-- IJobRepository
¦ +-- JobRepository
¦ +-- DbContext
¦
+-- IVehicleService
+-- VehicleService
+-- IVehicleRepository
+-- IVehicleAllocationService
+-- INotificationService
Each individual dependency may be perfectly reasonable. The problem appears when understanding one class requires understanding a large portion of the application.
The question is not whether this graph is technically valid. The question is whether the graph is still easy for a developer to reason about.
The Interface Explosion Problem
One easy way to make DI harder to understand is to create an interface for practically everything.
public interface IUserService { }
public interface IUserValidator { }
public interface IUserMapper { }
public interface IUserFactory { }
public interface IUserProvider { }
An interface is useful when it represents a meaningful boundary. An interface that exists only because every class should have one adds another layer without necessarily adding value.
Compare that with an abstraction around a genuine external boundary:
public interface IPaymentGateway
{
Task<PaymentResult> ChargeAsync(PaymentRequest request);
}
The second interface communicates a clear architectural boundary. There may be multiple implementations, an external dependency, or a need to isolate infrastructure from application logic.
The question shouldn't be "Can I create an interface?" The better question is "What boundary does this interface represent?"
DI Registration Can Become a Second Programming Language
ASP.NET Core applications often organize registrations through Program.cs and extension methods:
services.AddApplication();
services.AddInfrastructure();
services.AddPersistence();
This organization can be useful in large applications. But it also means that the actual dependency relationship may be defined somewhere far away from the implementation.
A developer trying to understand one class may have to jump through several files to answer a simple question: which implementation is actually being injected?
The application effectively has two places where the dependency relationship is described: the implementation and the composition root.
When DI Becomes Invisible
ASP.NET Core creates objects for you. That convenience is valuable, but it also means object construction is no longer visible at the point where a dependency is consumed.
public OrderController(IOrderService service)
{
_service = service;
}
The controller tells us that it needs IOrderService. It doesn't immediately tell us which implementation will be used or what the complete dependency chain looks like.
IOrderService
↓
OrderService
↓
IOrderRepository
↓
OrderRepository
↓
DbContext
This is not inherently bad. The framework is doing exactly what DI is designed to do. The important point is that some complexity has moved rather than disappeared.
Constructor Injection Can Become a Warning Sign
A large constructor is not automatically a design failure. There is no universal dependency count that tells us when a constructor is too large.
However, a constructor with many unrelated dependencies is often an architectural signal.
public JobService(
IJobRepository jobs,
IVehicleService vehicles,
IEmployeeService employees,
IPaymentService payments,
INotificationService notifications,
IAuditService audit)
{
}
Instead of asking how DI can handle all these dependencies, ask why this class needs them in the first place.
If a class coordinates jobs, vehicles, employees, payments, notifications, and auditing, the problem may not be DI. The class may simply have too many responsibilities.
The Service-to-Service Chain
Another common pattern is a long chain of services calling other services:
Controller
↓
JobService
↓
VehicleService
↓
AllocationService
↓
NotificationService
↓
EmailService
Each service may look reasonable in isolation. But the overall application flow becomes difficult to reason about.
Long chains can also make it harder to identify where a business rule actually belongs. When every service can call several other services, responsibilities can slowly become blurred.
DI Does Not Replace Good Architecture
Dependency Injection is a composition mechanism, not an architecture.
You can build Clean Architecture, Hexagonal Architecture, Modular Monoliths, Microservices, or poorly structured applications with DI.
DI makes object composition easier. It does not decide where responsibilities belong, how modules should communicate, or where business rules should live.
A poorly designed application with DI is still a poorly designed application. It may simply have cleaner constructors.
When DI Actually Helps
DI is particularly valuable when used for genuine boundaries, especially where implementations can vary or where infrastructure should be separated from application logic.
• Database access
• External APIs
• Message brokers
• Payment providers
• Email providers
• File systems
• Infrastructure services
• Components with multiple meaningful implementations
These are boundaries where dependency substitution or isolation usually provides real architectural value.
When DI Is Probably Overkill
Not every small class needs an interface.
public class TaxCalculator
{
public decimal Calculate(decimal amount)
=> amount * 0.18m;
}
There is nothing inherently wrong with using this concrete class directly. You don't automatically need an ITaxCalculator simply because the application uses Dependency Injection.
If there is only one implementation and no meaningful boundary, adding an interface may simply increase navigation overhead.
The goal should be useful abstraction, not maximum abstraction.
DI and Testing
DI makes it easy to substitute dependencies in tests. That is valuable, but mockability is not the same thing as good architecture.
If a class requires six mocks just to test a small piece of behavior, the fact that the test can be written does not necessarily mean the design is good.
Difficulty testing a class with many dependencies can be a signal that the class is doing too much.
Good architecture should make testing easier because the design is good—not because every concrete type has been replaced with an interface.
The Runtime Cost of DI
For most ASP.NET Core applications, the built-in dependency injection container is not going to be the first performance bottleneck.
Database queries, network calls, serialization, allocations, and application-level processing are usually much more significant.
Extremely large dependency graphs can introduce additional object creation and resolution overhead, but this should be optimized only when profiling demonstrates a real problem.
For this discussion, the bigger cost is not CPU time. It is cognitive cost.
Practical Ways to Keep DI Under Control
1. Inject meaningful boundaries. Use DI where a dependency represents infrastructure, an external system, or a meaningful interchangeable component.
2. Don't create interfaces by default. An interface should solve a real design problem, not satisfy a rule that every class must have one.
3. Treat large constructors as architectural signals. Look for excessive responsibilities rather than simply trying to hide the number of dependencies.
4. Avoid unnecessary service chains. Keep business flows understandable and avoid turning every operation into a chain of service-to-service calls.
5. Keep registrations discoverable. Organize dependency registration so developers can quickly find where implementations are selected.
6. Prefer explicit dependencies over hidden behavior. A clear dependency graph is easier to reason about than behavior hidden behind factories, service locators, or excessive indirection.
The Real Questions to Ask
Before adding another interface or registering another service, ask:
• What is the boundary?
• Who owns this behavior?
• Why does this dependency exist?
• Can this class have fewer responsibilities?
• Is this interface providing real value?
• Can a developer understand the object graph quickly?
• Can I find the implementation without searching the entire solution?
• Is the dependency likely to vary?
• Does the abstraction make testing or composition meaningfully easier?
• What complexity am I moving, and am I actually reducing it?
Final Thought
Dependency Injection solved an important problem in software design: separating object construction from object usage.
But every abstraction has a cost.
When DI is used deliberately, it gives us flexibility, testability, and clear boundaries. When it is applied mechanically, it can produce interface-heavy code, long dependency chains, large service classes, and an application that is technically decoupled but difficult to understand.
The goal isn't to use less DI.
The goal is to use the right amount of DI.
Don't ask:
"Should this class have an interface?"
Ask:
"What problem does this abstraction solve?"
If you have a good answer, keep it.
If the answer is simply "because that's how we do DI in .NET," it may be time to reconsider.
Good architecture isn't about having more abstractions. It's about having abstractions that earn their place.
Key Takeaways
• Dependency Injection separates object construction from object usage.
• DI does not automatically reduce conceptual complexity.
• Too many interfaces can increase navigation and cognitive overhead.
• Large constructors can be architectural signals rather than DI problems.
• Long service-to-service chains can hide the actual application flow.
• DI is a composition mechanism, not an architecture by itself.
• Abstractions should represent meaningful boundaries.
• Testability alone is not a reason to create an interface for everything.
• Prefer understandable dependency graphs over maximum abstraction.
• The best DI design is not the one with the most interfaces—it is the one that makes the system easiest to understand.
Recent Posts
-
Aug 22 2026
-
Aug 22 2026
-
Aug 22 2026