Lesson 13 of 40 Architecture Advanced 45 min

Dependency Injection & Architecture Patterns

In this lesson, you will learn how dependency injection supports maintainable design and how architecture patterns help you organize applications that are easier to test, extend, and understand.

← Back to Visual Studio 2026 Tutorial Home

What you will learn

Why this matters: Good architecture helps projects stay flexible as they grow. Dependency injection helps reduce tight coupling and supports testing and reuse.

Part 1: Service lifetimes

When you register a service in the dependency injection container, you choose how long that service should live. The right lifetime depends on what the service does and how it is used.

LifetimeWhen to useRegistration
TransientLightweight, stateless servicesAddTransient
ScopedPer-request state, such as DbContextAddScoped
SingletonShared app-wide services, such as cachesAddSingleton
Important: Choosing the wrong lifetime can cause bugs, memory issues, or unexpected shared state.

Part 2: Keyed services

Sometimes you need more than one implementation of the same interface. Keyed services let you register multiple implementations and select the one you need more explicitly.

// Register multiple implementations builder.Services.AddKeyedSingleton("stripe"); builder.Services.AddKeyedSingleton("paypal"); // Inject by key public CheckoutService([FromKeyedServices("stripe")] IPaymentGateway gateway)

This is useful when different providers or strategies share the same contract but behave differently.

Part 3: MediatR and CQRS ideas

A mediator-based approach can help separate requests from the code that handles them. This often fits well with CQRS, where reads and writes are treated as different kinds of operations.

public record CreateOrderCommand(OrderDto Dto) : IRequest; public class CreateOrderHandler : IRequestHandler { public async Task Handle(CreateOrderCommand cmd, CancellationToken ct) { // Business logic here } }

This pattern can improve organization in larger applications, especially when use cases become more complex.

Part 4: Decorator pattern with dependency injection

A decorator adds behavior around an existing service without changing the original implementation. This is useful for concerns such as caching, logging, retries, or metrics.

// Add caching decorator without modifying original builder.Services.AddScoped(); builder.Services.Decorate();

This helps you keep responsibilities separate and avoids overloading one class with too many duties.

When to use common architecture ideas

ApproachBest used for
Dependency InjectionReducing coupling and improving configurability
Scoped lifetimesRequest-based services such as data access
Keyed servicesSelecting between multiple implementations
Mediator patternSeparating request handling from controllers or UI logic
Decorator patternAdding cross-cutting behavior without changing core classes

A practical architecture workflow

Step 1: Identify the main services and responsibilities in the application
Step 2: Register services with appropriate lifetimes
Step 3: Separate business logic from UI or transport layers
Step 4: Use patterns only where they solve a real problem
Step 5: Add decorators or mediator-style handlers when complexity grows
Step 6: Keep the design understandable for future maintenance

Best practices

Summary

In this lesson, you learned how dependency injection lifetimes work, how keyed services support multiple implementations, and how architectural patterns such as mediator-style handling and decorators can improve application structure.

In the next lesson, you will continue with real-time application patterns.