DEV Community

Cristian Sifuentes
Cristian Sifuentes

Posted on

C# Architecture Mastery — Dependency Injection, Lifetimes & Composition Roots (Part 3)

C# Architecture Mastery — Dependency Injection, Lifetimes & Composition Roots (Part 3)

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();
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode
  • 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>();
Enter fullscreen mode Exit fullscreen mode
  • 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>();
Enter fullscreen mode Exit fullscreen mode
  • 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>();
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

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>();
Enter fullscreen mode Exit fullscreen mode

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:

  1. Are dependencies explicit via constructors?
  2. Do lifetimes match correctly?
  3. Is object creation centralized?
  4. Can I replace infrastructure easily?
  5. 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)