C# Architecture Mastery — Dependency Injection, Lifetimes & Composition Roots (Part 3)
Most developers use Dependency Injection.
Few truly understand its architectural role.
In this Part 3, we go beyond syntax and focus on why DI exists, how lifetimes affect correctness, and why the composition root is one of the most important (and misunderstood) concepts in modern .NET systems.
Dependencies → Lifetimes → System Assembly
This is where architecture becomes real.
1. What Dependency Injection Actually Solves
Dependency Injection (DI) solves one problem:
Removing hard dependencies between high-level policies and low-level details.
Without DI, systems become:
- Rigid
- Hard to test
- Painful to extend
- Impossible to reason about safely
// ❌ Hard dependency
class OrderService
{
private readonly SqlOrderRepository _repo = new();
}
This code:
- Cannot be tested without a database
- Cannot change storage strategy
- Violates Dependency Inversion
2. Dependency Injection Is a CONSEQUENCE of DIP
DI is not the principle.
Dependency Inversion Principle (DIP) is.
// ✅ DIP-compliant
class OrderService
{
private readonly IOrderRepository _repo;
public OrderService(IOrderRepository repo)
{
_repo = repo;
}
}
DI is simply how we wire abstractions to implementations at runtime.
Architecture first.
Framework second.
3. The Three Core Lifetimes in .NET
.NET provides three primary service lifetimes.
Understanding them is critical.
3.1 Transient
services.AddTransient<IService, Service>();
- New instance every time
- Stateless operations
- Lightweight services
Use for:
- Calculators
- Formatters
- Pure services
Avoid for:
- Shared state
- Expensive objects
3.2 Scoped
services.AddScoped<IService, Service>();
- One instance per scope
- In web apps → one per HTTP request
Use for:
- Unit of Work
- EF Core DbContext
- Request-based logic
This is the most common lifetime in ASP.NET Core.
3.3 Singleton
services.AddSingleton<IService, Service>();
- One instance for the entire application lifetime
Use for:
- Configuration
- Caches
- Stateless coordinators
⚠️ Danger zone:
- Thread safety required
- No scoped dependencies allowed
4. Lifetime Mismatches — The Silent Killer
This is one of the most common production bugs.
// ❌ Singleton depending on Scoped
services.AddSingleton<AppService>();
services.AddScoped<RequestContext>();
This causes:
- Runtime exceptions
- Data leaks
- Undefined behavior
Rule of thumb:
Longer-lived services must not depend on shorter-lived services.
5. The Composition Root Explained
The composition root is:
The single place where object graphs are assembled.
In ASP.NET Core, this is typically:
Program.cs-
Startup.cs(older versions)
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<OrderService>();
After this point:
- No
new - No service resolution
- No infrastructure logic
Everything is wired once, at the edge.
6. Why the Composition Root Matters
Without a clear composition root:
- Dependencies leak everywhere
- Testing becomes painful
- Architecture collapses inward
With a clean composition root:
- Core remains framework-agnostic
- Infrastructure is replaceable
- Tests become trivial
This is Clean Architecture in practice.
7. Dependency Injection vs Service Locator (Important Distinction)
// ❌ Service Locator anti-pattern
var repo = serviceProvider.GetService<IOrderRepository>();
Problems:
- Hidden dependencies
- Runtime failures
- Impossible to reason about requirements
Constructor injection makes dependencies:
- Explicit
- Enforceable
- Self-documenting
DI ≠ Service Locator.
8. Dependency Graphs, Not Containers
DI containers are tools, not architecture.
What matters is:
- The dependency graph
- Direction of dependencies
- Stability of abstractions
If your domain references:
- ASP.NET Core
- Entity Framework
- Microsoft.Extensions.*
Your architecture is already compromised.
9. A Senior-Level DI Checklist
Before shipping code, ask:
- Are dependencies explicit via constructors?
- Do lifetimes match correctly?
- Is object creation centralized?
- Can I replace infrastructure easily?
- Is the domain framework-free?
If yes—you’re using DI correctly.
Final Thoughts
Dependency Injection is not magic.
It is controlled wiring.
Lifetimes are not configuration.
They are correctness constraints.
The composition root is not boilerplate.
It is the assembly line of your system.
Master this, and Clean Architecture stops being theory.
✍️ Written by Cristian Sifuentes — designing maintainable .NET systems and teaching teams how to reason about dependencies at scale.

Top comments (0)