Skip to main content

.NET Architecture at Scale: Visual Guide to Modern Design Patterns

.NET Architecture at Scale: Visual Guide to Modern Design Patterns
Table of Contents

.NET Architecture at Scale: Visual Guide to Modern Design Patterns
#

I keep a page like this open in a browser tab. When a team asks which pattern fits a new service, I would rather point at a decision tree and a comparison table than argue it from memory for the third time that month. So I wrote the reference I wanted: the 16 patterns I actually reach for in .NET systems, each with its diagram, a code sketch, and the trade-off it forces on you.

One caveat before you scroll. Most projects need three or four of these, not sixteen. The patterns that get teams in trouble are almost always the ones adopted because they sounded advanced, not because a real constraint demanded them. Treat this as a menu you order from on purpose, not a checklist to finish.

The page is deliberately long. Use Ctrl+F or the table of contents to jump straight to the pattern you came for.

What’s covered
#

  • 16 patterns grouped by the job they do: foundational structure, service communication, integration, resilience, deployment, and data.
  • A decision tree and comparison matrix up front, so you can shortlist before reading a single pattern in depth.
  • For each pattern: architecture and flow diagrams, a when-to-use decision tree, a .NET code sketch, and the cost it carries.
  • Combination strategies: the pairings that show up in real systems, plus the migration paths between them.
  • A .NET and Azure lens: recent runtime and Aspire features, Azure API Management, and Container Apps with Dapr where they apply.

Table of Contents
#

  1. Foundational Architecture Patterns
  2. Advanced Communication Patterns
  3. Service Integration Patterns
  4. Resilience and Data Patterns
  5. Modern .NET and Azure Integration
  6. Deployment and Infrastructure Patterns
  7. Pattern Selection Guide
  8. Best Practices for Implementation

Pattern Selection Guide
#

This section pairs interactive decision flows with comparison matrices to help you choose patterns for your .NET applications.

Interactive Decision Flow
#

Use this decision tree to narrow toward the patterns that fit your requirements:

flowchart TD Start([Architecture Design Decision]) --> A{Application Type?} A -->|Simple CRUD| B[Layered Architecture] A -->|Complex Business Logic| C{Domain Complexity?} A -->|Microservices| D{Communication Style?} A -->|Legacy Modernization| E[Strangler Fig Pattern] A -->|Event-Driven/Stateless| F1[Serverless Pattern] C -->|High| F[Onion Architecture] C -->|Medium| G[Hexagonal Architecture] D -->|Synchronous| H{Load Distribution?} D -->|Asynchronous| I[Event-Driven Architecture] H -->|High| J[API Gateway + BFF] H -->|Medium| K[API Gateway] I --> L{Transaction Consistency?} L -->|Required| M[Saga Pattern] L -->|Eventual| N[CQRS + Event Sourcing] L -->|Message Reliability| O1[Outbox Pattern] B --> O{Scaling Needs?} F --> P{Read/Write Patterns?} G --> P O -->|High| Q[Add Resilience Patterns] P -->|Different| R[CQRS] P -->|Same| S[Repository Pattern] Q --> T[Circuit Breaker + Bulkhead] %% Infrastructure Patterns J --> U{Service Management?} K --> U U -->|Cross-cutting| V[Sidecar Pattern] U -->|External Communication| W[Ambassador Pattern] style Start fill:#ffeb3b style B fill:#c8e6c9 style F fill:#ffcdd2 style G fill:#f8bbd9 style I fill:#e1bee7 style J fill:#b3e5fc style F1 fill:#e8f5e8 style O1 fill:#fff3e0 style V fill:#f3e5f5 style W fill:#e3f2fd

Complexity vs. Benefit Analysis
#

graph LR subgraph "Low Complexity" L1[Layered Architecture
⭐⭐⭐⭐⭐] L2[API Gateway
⭐⭐⭐⭐] L3[Circuit Breaker
⭐⭐⭐⭐] end subgraph "Medium Complexity" M1[Hexagonal Architecture
⭐⭐⭐] M2[CQRS
⭐⭐⭐] M3[BFF
⭐⭐⭐] M4[Serverless
⭐⭐⭐] end subgraph "High Complexity" H1[Onion Architecture
⭐⭐] H2[Event Sourcing
⭐⭐] H3[Saga Pattern
⭐⭐] H4[Strangler Fig
⭐⭐] end style L1 fill:#c8e6c9 style L2 fill:#c8e6c9 style L3 fill:#c8e6c9 style M1 fill:#fff3e0 style M2 fill:#fff3e0 style M3 fill:#fff3e0 style M4 fill:#fff3e0 style H1 fill:#ffcdd2 style H2 fill:#ffcdd2 style H3 fill:#ffcdd2 style H4 fill:#ffcdd2

Pattern Comparison Matrix
#

PatternTeam Skill LevelSetup TimeMaintenanceScalabilityTestabilityUse Case Fit
LayeredBeginnerFastLowMediumMediumCRUD Apps
HexagonalIntermediateMediumMediumHighVery HighClean Architecture
OnionAdvancedSlowHighVery HighVery HighEnterprise DDD
CQRSIntermediateMediumMediumVery HighHighRead/Write Split
Event-DrivenAdvancedSlowHighVery HighMediumReal-time Systems
SagaExpertVery SlowVery HighHighMediumDistributed Transactions
API GatewayBeginnerFastLowVery HighMediumMicroservices
BFFIntermediateMediumMediumHighHighMulti-client Apps
ServerlessIntermediateFastLowAutoMediumEvent Processing
Strangler FigAdvancedVariableHighHighMediumLegacy Migration
Circuit BreakerBeginnerFastLowMediumHighFault Tolerance
OutboxIntermediateMediumMediumHighMediumMessage Reliability
SidecarIntermediateMediumMediumHighMediumCross-cutting Concerns
AmbassadorAdvancedMediumHighHighMediumService Proxy
BulkheadAdvancedSlowHighVery HighMediumResource Isolation
Event SourcingExpertVery SlowVery HighHighMediumAudit Trail

Quick Selection Guide
#

flowchart TD A[What's your primary need?] --> B[Simple CRUD Application] A --> C[High Performance & Scale] A --> D[Clean Testable Code] A --> E[Legacy Modernization] A --> F[Multi-client Support] A --> G[Fault Tolerance] B --> B1[✅ Layered Architecture] C --> C1[✅ CQRS + Event-Driven] D --> D1[✅ Hexagonal/Onion] E --> E1[✅ Strangler Fig] F --> F1[✅ API Gateway + BFF] G --> G1[✅ Circuit Breaker + Bulkhead] style B1 fill:#c8e6c9 style C1 fill:#ffeb3b style D1 fill:#e3f2fd style E1 fill:#fff3e0 style F1 fill:#f3e5f5 style G1 fill:#ffcdd2

Architectural Pattern Categories
#

Here is how the patterns relate to each other:

mindmap root((Design Patterns)) Foundational Layered Hexagonal Onion Communication CQRS Event-Driven Saga Integration API Gateway BFF Serverless Strangler Fig Resilience Circuit Breaker Bulkhead Outbox Infrastructure Sidecar Ambassador Data Event Sourcing

Foundational Architecture Patterns
#

Layered Architecture
#

Layered Architecture remains the most familiar pattern for .NET developers, organizing applications into horizontal layers with clear separation of concerns. While simpler than modern alternatives, it provides an excellent foundation for understanding architectural principles.

Architecture Flow
#

graph TB A[Presentation Layer
Controllers, Views, APIs] --> B[Business Logic Layer
Services, Domain Logic] B --> C[Data Access Layer
Repositories, ORM, DAL] C --> D[Database Layer
SQL Server, Entity Framework] style A fill:#e1f5fe style B fill:#f3e5f5 style C fill:#e8f5e8 style D fill:#fff3e0

Request Processing Flow
#

sequenceDiagram participant C as Client participant P as Presentation Layer participant B as Business Layer participant D as Data Layer participant DB as Database C->>P: HTTP Request P->>P: Validate Input P->>B: Call Business Logic B->>B: Apply Business Rules B->>D: Repository Call D->>DB: SQL Query DB-->>D: Data D-->>B: Domain Objects B-->>P: Processing Result P-->>C: HTTP Response

When to Use Decision Tree
#

flowchart TD A[Consider Layered Architecture] --> B{Team Experience?} B -->|Junior/Mixed| C{Project Complexity?} B -->|Senior| D[Consider Advanced Patterns] C -->|Low-Medium| E[✅ Use Layered] C -->|High| F[Consider Hexagonal/Onion] E --> G{Future Scalability?} G -->|Limited| H[✅ Layered is Perfect] G -->|High| I[Plan Migration Path] style E fill:#c8e6c9 style H fill:#c8e6c9 style F fill:#ffcdd2 style D fill:#ffcdd2
  • Best for: CRUD applications, rapid prototyping, small teams, learning projects.
  • Benefits: Familiarity, simplicity, quick development cycles.
  • Challenges: Tight coupling between layers, difficulty in testing, scalability limitations.

Implementation Example
#

// Business Logic Layer - Service
public class ProductService : IProductService
{
    private readonly IProductRepository _productRepository;
    
    public ProductService(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
    
    public async Task<IEnumerable<Product>> GetAllProductsAsync()
    {
        return await _productRepository.GetAllAsync();
    }
    
    public async Task<Product> CreateProductAsync(CreateProductRequest request)
    {
        var product = new Product
        {
            Name = request.Name,
            Price = request.Price,
            CreatedAt = DateTime.UtcNow
        };
        
        return await _productRepository.CreateAsync(product);
    }
}

// Data Access Layer - Repository
public class ProductRepository : IProductRepository
{
    private readonly AppDbContext _context;
    
    public ProductRepository(AppDbContext context)
    {
        _context = context;
    }
    
    public async Task<IEnumerable<Product>> GetAllAsync()
    {
        return await _context.Products.ToListAsync();
    }
    
    public async Task<Product> CreateAsync(Product product)
    {
        _context.Products.Add(product);
        await _context.SaveChangesAsync();
        return product;
    }
}

Hexagonal Architecture
#

Hexagonal Architecture, also known as Ports and Adapters, isolates core business logic from external concerns through well-defined interfaces. It pays off when technology independence and testability actually matter.

Architecture Structure
#

graph TB subgraph "Primary Adapters" A1[Web API] A2[UI] A3[Console] end subgraph "Ports" P1[Inbound Interfaces] end subgraph "Application Core" AC[Domain
Business Logic] end subgraph "Ports" P2[Outbound Interfaces] end subgraph "Secondary Adapters" S1[Database] S2[Email Service] S3[External APIs] end A1 --> P1 A2 --> P1 A3 --> P1 P1 --> AC AC --> P2 P2 --> S1 P2 --> S2 P2 --> S3 style AC fill:#ffeb3b style P1 fill:#e3f2fd style P2 fill:#e3f2fd

Dependency Flow
#

flowchart LR subgraph "Outside World" UI[User Interface] API[API Clients] DB[(Database)] EXT[External Services] end subgraph "Adapters" PA[Primary Adapters] SA[Secondary Adapters] end subgraph "Ports" IP[Input Ports] OP[Output Ports] end subgraph "Core" BL[Business Logic] end UI --> PA API --> PA PA --> IP IP --> BL BL --> OP OP --> SA SA --> DB SA --> EXT style BL fill:#ffeb3b style IP fill:#c8e6c9 style OP fill:#c8e6c9

Testing Strategy
#

graph TD A[Testing Strategy] --> B[Unit Tests] A --> C[Integration Tests] A --> D[Acceptance Tests] B --> B1[Domain Logic
✓ Fast ✓ Isolated] B --> B2[Use Cases
✓ Mock Adapters] C --> C1[Adapter Tests
✓ Real Infrastructure] C --> C2[Port Tests
✓ Contract Validation] D --> D1[End-to-End
✓ Full System] style B1 fill:#c8e6c9 style B2 fill:#c8e6c9 style C1 fill:#fff3e0 style C2 fill:#fff3e0 style D1 fill:#ffcdd2

Implementation Guidelines
#

// Core Contracts - Keep Minimal
public interface IProductRepository
{
    Task<Product> GetByIdAsync(int id);
    Task<Product> SaveAsync(Product product);
}

public interface IProductService
{
    Task<ProductDto> CreateProductAsync(CreateProductRequest request);
}
  • Best for: Applications requiring high testability, technology independence, or complex business logic.
  • Benefits: Technology agnostic, highly testable, clean dependencies.
  • Challenges: Initial complexity, learning curve, potential over-engineering for simple applications.

Implementation Example
#

// Port - Interface defined by business needs
public interface IProductRepository
{
    Task<Product> GetByIdAsync(int id);
    Task<IEnumerable<Product>> GetAllAsync();
    Task<Product> SaveAsync(Product product);
}

// Application Service - Orchestrates business logic
public class ProductService
{
    private readonly IProductRepository _productRepository;
    private readonly INotificationService _notificationService;
    
    public ProductService(IProductRepository productRepository, 
                         INotificationService notificationService)
    {
        _productRepository = productRepository;
        _notificationService = notificationService;
    }
    
    public async Task<Product> CreateProductAsync(string name, decimal price)
    {
        var product = new Product(name, price);
        var savedProduct = await _productRepository.SaveAsync(product);
        
        await _notificationService.SendProductCreatedNotificationAsync(savedProduct);
        
        return savedProduct;
    }
}

Onion Architecture
#

Onion Architecture builds upon Hexagonal principles with explicit concentric layers, placing domain logic at the center and keeping dependencies flowing inward. It fits enterprise applications with complex business rules.

Concentric Layer Structure
#

graph TB subgraph "Infrastructure Layer" I1[Data Access] I2[External Services] I3[File System] end subgraph "Presentation Layer" P1[Web API] P2[MVC] P3[Blazor] end subgraph "Application Layer" A1[Application Services] A2[Use Cases] A3[DTOs] end subgraph "Domain Layer" D1[Entities] D2[Domain Services] D3[Domain Events] end P1 -.-> A1 P2 -.-> A1 P3 -.-> A1 A1 --> D1 A2 --> D1 I1 -.-> A1 I2 -.-> A1 I3 -.-> A1 style D1 fill:#ffeb3b style D2 fill:#ffeb3b style D3 fill:#ffeb3b style A1 fill:#e3f2fd style A2 fill:#e3f2fd style A3 fill:#e3f2fd

Dependency Flow Rules
#

flowchart TD A[Onion Architecture Rules] --> B[Domain Layer] A --> C[Application Layer] A --> D[Infrastructure Layer] A --> E[Presentation Layer] B --> B1[✓ No Dependencies
✓ Pure Business Logic
✓ Domain Events] C --> C1[✓ Depends on Domain Only
✓ Use Cases & Services
✓ Interface Definitions] D --> D1[✓ Implements Interfaces
✓ External Concerns
✓ Data Access & APIs] E --> E1[✓ Depends on Application
✓ User Interface
✓ Controllers & Views] style B1 fill:#ffeb3b style C1 fill:#e3f2fd style D1 fill:#f3e5f5 style E1 fill:#e8f5e8

Use Case Flow
#

sequenceDiagram participant UI as Presentation participant App as Application participant Dom as Domain participant Inf as Infrastructure UI->>App: Execute Use Case App->>Dom: Business Logic Call Dom->>Dom: Apply Business Rules Dom->>App: Domain Events App->>Inf: Persist Changes Inf-->>App: Confirmation App-->>UI: Response DTO Note over Dom: Core business logic
remains isolated

Project Structure Guidelines
#

Property.Management.Domain/
├── Entities/
├── ValueObjects/
├── DomainServices/
└── Events/

Property.Management.Application/
├── UseCases/
├── Services/
├── DTOs/
└── Interfaces/

Property.Management.Infrastructure/
├── Persistence/
├── ExternalServices/
└── Configuration/

Property.Management.Presentation/
├── Controllers/
├── Models/
└── Views/
  • Best for: Enterprise applications with complex business rules, Domain-Driven Design implementations, long-term maintainable systems.
  • Benefits: Clear separation of concerns, testable architecture, business logic isolation.
  • Challenges: Initial setup complexity, learning curve for teams new to DDD.

Implementation Example
#

// Domain Layer - Rich domain model
public class Order
{
    private readonly List<OrderItem> _items = new();
    
    public int Id { get; private set; }
    public string CustomerName { get; private set; }
    public DateTime OrderDate { get; private set; }
    public OrderStatus Status { get; private set; }
    public decimal TotalAmount => _items.Sum(i => i.Price * i.Quantity);
    
    public Order(string customerName)
    {
        OrderDate = DateTime.UtcNow;
        Status = OrderStatus.Draft;
    }
    
    public void AddItem(string productName, decimal price, int quantity)
    {
        if (Status != OrderStatus.Draft)
            throw new InvalidOperationException("Cannot modify confirmed order");
            
        _items.Add(new OrderItem(productName, price, quantity));
    }
    
    public void ConfirmOrder()
    {
        if (!_items.Any())
            throw new InvalidOperationException("Cannot confirm empty order");
        if (Status != OrderStatus.Draft)
            throw new InvalidOperationException("Order already confirmed");
            
        Status = OrderStatus.Confirmed;
    }
}

// Application Layer - Use cases
public class OrderService : IOrderService
{
    private readonly IOrderRepository _orderRepository;
    private readonly IEmailService _emailService;
    
    public OrderService(IOrderRepository orderRepository, IEmailService emailService)
    {
        _orderRepository = orderRepository;
        _emailService = emailService;
    }
    
    public async Task<OrderDto> CreateOrderAsync(CreateOrderRequest request)
    {
        var order = new Order(request.CustomerName);
        var savedOrder = await _orderRepository.SaveAsync(order);
        
        return new OrderDto
        {
            Id = savedOrder.Id,
            CustomerName = savedOrder.CustomerName,
            OrderDate = savedOrder.OrderDate,
            Status = savedOrder.Status.ToString(),
            TotalAmount = savedOrder.TotalAmount
        };
    }
    
    public async Task<OrderDto> ConfirmOrderAsync(int orderId)
    {
        var order = await _orderRepository.GetByIdAsync(orderId);
        if (order == null)
            throw new ArgumentException("Order not found");
            
        order.ConfirmOrder();
        var confirmedOrder = await _orderRepository.SaveAsync(order);
        
        await _emailService.SendOrderConfirmationAsync(confirmedOrder);
        
        return MapToDto(confirmedOrder);
    }
}

Advanced Communication Patterns
#

CQRS (Command Query Responsibility Segregation)
#

Command Query Responsibility Segregation (CQRS) separates read and write operations, allowing independent optimization of each concern. This pattern enables different models for reading and writing data.

Architecture Overview
#

graph LR subgraph "Command Side" C1[Commands] --> CH[Command Handlers] CH --> WDB[(Write Database)] end subgraph "Query Side" Q1[Queries] --> QH[Query Handlers] QH --> RDB[(Read Database)] end subgraph "Event Bus" EB[Domain Events] end CH --> EB EB --> RS[Read Store Updater] RS --> RDB style C1 fill:#ffcdd2 style Q1 fill:#c8e6c9 style EB fill:#fff3e0

Decision Flow for CQRS
#

flowchart TD A[Consider CQRS] --> B{Read/Write Patterns Different?} B -->|No| C[Use Simple Repository] B -->|Yes| D{Scalability Requirements?} D -->|Low| E[Simple CQRS
Same Database] D -->|High| F{Consistency Requirements?} F -->|Eventual OK| G[CQRS + Event Sourcing] F -->|Strong Required| H[CQRS + Saga Pattern] E --> I[MediatR Implementation] G --> J[Separate Read/Write Stores] H --> K[Distributed Transactions] style C fill:#ffcdd2 style I fill:#c8e6c9 style J fill:#c8e6c9 style K fill:#fff3e0

Message Flow Patterns
#

sequenceDiagram participant C as Client participant API as API Gateway participant CH as Command Handler participant WS as Write Store participant EB as Event Bus participant QH as Query Handler participant RS as Read Store Note over C,RS: Command Flow C->>API: Create Order Command API->>CH: Process Command CH->>WS: Store Write Model CH->>EB: Publish Domain Event EB->>QH: Update Read Model QH->>RS: Store Read Model Note over C,RS: Query Flow C->>API: Get Order Query API->>QH: Process Query QH->>RS: Retrieve Read Model RS-->>QH: Read Model Data QH-->>API: Query Result API-->>C: Response

Implementation Strategy
#

graph TD A[CQRS Implementation] --> B[Phase 1: Separate Handlers] A --> C[Phase 2: Separate Models] A --> D[Phase 3: Separate Stores] B --> B1[✓ MediatR Setup
✓ Command/Query Handlers
✓ Same Database] C --> C1[✓ Write Models
✓ Read Models
✓ Event Publishing] D --> D1[✓ Write Database
✓ Read Database
✓ Event Sourcing] style B1 fill:#c8e6c9 style C1 fill:#fff3e0 style D1 fill:#ffcdd2

Core Contracts
#

// Keep contracts minimal and focused
public interface ICommand<TResult> : IRequest<TResult> { }
public interface IQuery<TResult> : IRequest<TResult> { }

// Example command
public record CreateOrderCommand(string CustomerId, List<OrderItem> Items) : ICommand<int>;

// Example query  
public record GetOrderQuery(int OrderId) : IQuery<OrderDto>;
  • Best for: Applications with different read/write patterns, high-scale systems, complex reporting requirements.
  • Benefits: Independent scaling, optimized data models, clear separation.
  • Challenges: Increased complexity, eventual consistency, debugging across models.

Implementation Example
#

// Controllers using MediatR
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IMediator _mediator;
    
    public ProductsController(IMediator mediator)
    {
        _mediator = mediator;
    }
    
    [HttpPost]
    public async Task<IActionResult> CreateProduct([FromBody] CreateProductCommand command)
    {
        var productId = await _mediator.Send(command);
        return CreatedAtAction(nameof(GetProduct), new { id = productId }, productId);
    }
    
    [HttpGet("{id}")]
    public async Task<IActionResult> GetProduct(int id)
    {
        var product = await _mediator.Send(new GetProductByIdQuery(id));
        return Ok(product);
    }
}

Event-Driven Architecture
#

Event-Driven Architecture enables loose coupling between components through asynchronous message passing, allowing systems to scale independently and react to business events in real-time.

Event Flow Architecture
#

graph TB subgraph "Event Producers" EP1[Order Service] EP2[Payment Service] EP3[Inventory Service] end subgraph "Event Infrastructure" EB[Event Bus
Azure Service Bus / RabbitMQ] end subgraph "Event Consumers" EC1[Email Service] EC2[Analytics Service] EC3[Audit Service] end EP1 --> EB EP2 --> EB EP3 --> EB EB --> EC1 EB --> EC2 EB --> EC3 style EB fill:#ffeb3b style EP1 fill:#ffcdd2 style EP2 fill:#ffcdd2 style EP3 fill:#ffcdd2 style EC1 fill:#c8e6c9 style EC2 fill:#c8e6c9 style EC3 fill:#c8e6c9

Event Processing Patterns
#

flowchart TD A[Event Processing Strategy] --> B[Choreography] A --> C[Orchestration] B --> B1[✓ Distributed Logic
✓ Services React to Events
✓ No Central Coordinator] C --> C1[✓ Central Coordinator
✓ Workflow Management
✓ Explicit Process Control] B1 --> B2[Pro: Loose Coupling
Con: Hard to Debug] C1 --> C2[Pro: Clear Flow
Con: Central Point of Failure] style B1 fill:#c8e6c9 style C1 fill:#fff3e0

Event Consistency Patterns
#

stateDiagram-v2 [*] --> EventPublished EventPublished --> Processing Processing --> Success: All Handlers Complete Processing --> PartialFailure: Some Handlers Fail Processing --> TotalFailure: Critical Handler Fails PartialFailure --> Retry: Retry Failed Handlers TotalFailure --> Compensate: Execute Compensation Retry --> Success: Retry Successful Retry --> Dead Letter: Max Retries Exceeded Success --> [*] Compensate --> [*] Dead Letter --> [*]

Implementation Approach
#

// Minimal event contract
public record OrderCreatedEvent(
    int OrderId,
    string CustomerId,
    decimal Amount,
    DateTime CreatedAt);

// Event handler interface
public interface IEventHandler<in TEvent>
{
    Task HandleAsync(TEvent eventData);
}
  • Best for: Microservices communication, real-time systems, scalable architectures.
  • Benefits: Loose coupling, scalability, resilience.
  • Challenges: Debugging complexity, eventual consistency, monitoring requirements.

Implementation Example
#

// Command Handler using MediatR
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, int>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IMediator _mediator;
    
    public async Task<int> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
    {
        var order = new Order(request.CustomerId, request.Items);
        await _orderRepository.SaveAsync(order);
        
        // Publish domain event
        await _mediator.Publish(new OrderCreatedEvent
        {
            OrderId = order.Id,
            CustomerId = request.CustomerId,
            Items = request.Items,
            TotalAmount = order.TotalAmount
        }, cancellationToken);
        
        return order.Id;
    }
}
    }
}

// Event Handlers
public class InventoryEventHandler : INotificationHandler<OrderCreatedEvent>
{
    private readonly IInventoryService _inventoryService;
    
    public async Task Handle(OrderCreatedEvent notification, CancellationToken cancellationToken)
    {
        foreach (var item in notification.Items)
        {
            await _inventoryService.ReserveInventoryAsync(item.ProductId, item.Quantity);
        }
    }
}

public class EmailNotificationHandler : INotificationHandler<OrderCreatedEvent>
{
    private readonly IEmailService _emailService;
    
    public async Task Handle(OrderCreatedEvent notification, CancellationToken cancellationToken)
    {
        await _emailService.SendOrderConfirmationAsync(
            notification.CustomerId,
            notification.OrderId);
    }
}

Saga Pattern
#

The Saga Pattern manages distributed transactions across multiple microservices by coordinating a sequence of local transactions with compensating actions for rollback scenarios.

stateDiagram-v2 [*] --> Started Started --> PaymentProcessed: Process Payment PaymentProcessed --> InventoryReserved: Reserve Inventory InventoryReserved --> OrderConfirmed: Confirm Order OrderConfirmed --> [*] PaymentProcessed --> Compensating: Payment Failed InventoryReserved --> Compensating: Inventory Failed Compensating --> Failed: Rollback Complete Failed --> [*] Compensating: Execute compensating transactions in reverse order

Implementation Example
#

// Saga State Machine
public class OrderProcessingSaga
{
    public string OrderId { get; set; }
    public string CustomerId { get; set; }
    public decimal Amount { get; set; }
    public SagaState State { get; set; }
    public List<string> CompletedSteps { get; set; } = new();
    
    public enum SagaState
    {
        Started,
        PaymentProcessed,
        InventoryReserved,
        OrderConfirmed,
        Failed,
        Compensating
    }
}

// Saga Orchestrator
public class OrderSagaOrchestrator
{
    private readonly IMediator _mediator;
    private readonly ISagaRepository _sagaRepository;
    
    public async Task HandleOrderCreatedAsync(OrderCreatedEvent orderCreated)
    {
        var saga = new OrderProcessingSaga
        {
            OrderId = orderCreated.OrderId.ToString(),
            CustomerId = orderCreated.CustomerId,
            Amount = orderCreated.TotalAmount,
            State = OrderProcessingSaga.SagaState.Started
        };
        
        await _sagaRepository.SaveAsync(saga);
        
        // Start the saga by processing payment
        await _mediator.Send(new ProcessPaymentCommand(
            saga.OrderId,
            saga.CustomerId,
            saga.Amount));
    }
    
    public async Task HandlePaymentProcessedAsync(PaymentProcessedEvent paymentProcessed)
    {
        var saga = await _sagaRepository.GetByOrderIdAsync(paymentProcessed.OrderId);
        saga.State = OrderProcessingSaga.SagaState.PaymentProcessed;
        saga.CompletedSteps.Add("Payment");
        
        await _sagaRepository.UpdateAsync(saga);
        
        // Next step: Reserve inventory
        await _mediator.Send(new ReserveInventoryCommand(
            saga.OrderId,
            paymentProcessed.Items));
    }
    
    public async Task HandleStepFailedAsync(SagaStepFailedEvent stepFailed)
    {
        var saga = await _sagaRepository.GetByOrderIdAsync(stepFailed.OrderId);
        saga.State = OrderProcessingSaga.SagaState.Compensating;
        
        await _sagaRepository.UpdateAsync(saga);
        
        // Execute compensating transactions in reverse order
        await ExecuteCompensatingTransactionsAsync(saga);
    }
    
    private async Task ExecuteCompensatingTransactionsAsync(OrderProcessingSaga saga)
    {
        var completedSteps = saga.CompletedSteps.AsEnumerable().Reverse();
        
        foreach (var step in completedSteps)
        {
            switch (step)
            {
                case "Inventory":
                    await _mediator.Send(new ReleaseInventoryCommand(saga.OrderId));
                    break;
                case "Payment":
                    await _mediator.Send(new RefundPaymentCommand(saga.OrderId, saga.Amount));
                    break;
            }
        }
        
        saga.State = OrderProcessingSaga.SagaState.Failed;
        await _sagaRepository.UpdateAsync(saga);
    }
}
  • Benefits: data consistency without distributed transactions, cleaner error handling, and better resilience.
  • Challenges: more moving parts, harder debugging, and compensation logic to maintain.

Service Integration Patterns
#

API Gateway
#

The API Gateway Pattern provides a single entry point for clients to access multiple backend services, commonly implemented using Azure API Management or Azure Application Gateway.

graph TB C1[Mobile Client] --> GW[API Gateway] C2[Web Client] --> GW C3[Third Party] --> GW GW --> S1[Auth Service] GW --> S2[Product Service] GW --> S3[Order Service] GW --> S4[Payment Service] subgraph "Cross-cutting Concerns" CC1[Rate Limiting] CC2[Authentication] CC3[Caching] CC4[Monitoring] end GW -.-> CC1 GW -.-> CC2 GW -.-> CC3 GW -.-> CC4 style GW fill:#ffeb3b style C1 fill:#e3f2fd style C2 fill:#e3f2fd style C3 fill:#e3f2fd

Azure API Management Configuration
#

// Rate limiting policy
@{
    return context.Request.IpAddress == "192.168.1.1" ? 1000 : 100;
}

// Route based on User-Agent
@{
    return context.Request.Headers.GetValueOrDefault("User-Agent", "").Contains("Mobile") 
        ? "mobile-backend" 
        : "web-backend";
}

Azure API Management gives you the gateway itself: Layer 7 routing, authentication and authorization, rate limiting, request/response transformation, caching, and monitoring. It ships in tiers from Consumption (serverless) through Developer, Basic, Standard, and Premium, each with different features and pricing.

  • When to use: e-commerce routing to catalog, inventory, and payment services; multi-tenant tenant-based routing; or API versioning.
  • Advantages: lower latency via caching, cost efficiency, and centralized infrastructure management.
  • Trade-offs: a potential single point of failure and performance bottleneck.

Backend for Frontend (BFF)
#

The Backend for Frontend (BFF) Pattern creates dedicated backend services for specific frontend applications or interfaces, optimizing each backend for particular client needs rather than using a single general-purpose API.

graph TB subgraph "Clients" M[Mobile App] W[Web App] T[Third Party API] end subgraph "BFF Layer" MBFF[Mobile BFF] WBFF[Web BFF] TBFF[Partner API BFF] end subgraph "Backend Services" PS[Product Service] OS[Order Service] US[User Service] NS[Notification Service] end M --> MBFF W --> WBFF T --> TBFF MBFF --> PS MBFF --> OS MBFF --> NS WBFF --> PS WBFF --> OS WBFF --> US TBFF --> PS TBFF --> OS style MBFF fill:#ffcdd2 style WBFF fill:#c8e6c9 style TBFF fill:#fff3e0

Implementation Example
#

// Mobile BFF Service
[ApiController]
[Route("api/mobile/[controller]")]
public class MobileProductController : ControllerBase
{
    private readonly IProductService _productService;
    private readonly IImageService _imageService;
    
    [HttpGet("{id}")]
    public async Task<MobileProductDto> GetProduct(int id)
    {
        var product = await _productService.GetByIdAsync(id);
        
        // Mobile-specific optimization: smaller images, essential data only
        return new MobileProductDto
        {
            Id = product.Id,
            Name = product.Name,
            Price = product.Price,
            ThumbnailUrl = await _imageService.GetThumbnailAsync(product.ImageUrl, "mobile"),
            Rating = product.AverageRating,
            InStock = product.StockLevel > 0
        };
    }
}

// Web BFF Service
[ApiController]
[Route("api/web/[controller]")]
public class WebProductController : ControllerBase
{
    private readonly IProductService _productService;
    private readonly IReviewService _reviewService;
    
    [HttpGet("{id}")]
    public async Task<WebProductDto> GetProduct(int id)
    {
        var product = await _productService.GetByIdAsync(id);
        var reviews = await _reviewService.GetRecentReviewsAsync(id, 10);
        
        // Web-specific: full data with reviews, recommendations
        return new WebProductDto
        {
            Id = product.Id,
            Name = product.Name,
            Description = product.Description,
            Price = product.Price,
            ImageUrls = product.ImageUrls,
            Specifications = product.Specifications,
            Reviews = reviews,
            StockLevel = product.StockLevel,
            EstimatedDelivery = CalculateDeliveryDate(product)
        };
    }
}
  • When to use: multi-platform apps (web, mobile, IoT), diverging client needs, or API versioning.
  • Advantages: optimized payloads, client-specific tuning, and independent evolution.
  • Trade-offs: code duplication, more infrastructure, and extra maintenance.

Serverless Pattern
#

The Serverless Pattern enables event-driven, scalable applications using Azure Functions with .NET, supporting various hosting models and orchestration patterns.

graph TB subgraph "Event Sources" HTTP[HTTP Triggers] TIMER[Timer Triggers] QUEUE[Queue Triggers] BLOB[Blob Triggers] end subgraph "Azure Functions" F1[Order Processing] F2[Image Resizing] F3[Email Sending] F4[Data Cleanup] end subgraph "Outputs" DB[(Database)] STORAGE[Blob Storage] SB[Service Bus] COSMOS[(Cosmos DB)] end HTTP --> F1 TIMER --> F4 QUEUE --> F2 BLOB --> F2 F1 --> DB F1 --> SB F2 --> STORAGE F3 --> SB F4 --> DB F4 --> COSMOS style F1 fill:#ffeb3b style F2 fill:#ffeb3b style F3 fill:#ffeb3b style F4 fill:#ffeb3b

Implementation Example
#

// .NET Isolated Worker Model
[Function("ProcessOrder")]
public async Task<IActionResult> ProcessOrder(
    [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req,
    [CosmosDBOutput("OrdersDB", "Orders", Connection = "CosmosDB")] IAsyncCollector<Order> orders)
{
    var order = await req.ReadFromJsonAsync<Order>();
    await orders.AddAsync(order);
    
    return new OkObjectResult(new { OrderId = order.Id, Status = "Processing" });
}

// Durable Functions Orchestration
[Function("OrderProcessingOrchestrator")]
public async Task<string> RunOrchestrator(
    [OrchestrationTrigger] IDurableOrchestrationContext context)
{
    var order = context.GetInput<Order>();
    
    // Function chaining pattern
    await context.CallActivityAsync("ValidateOrder", order);
    await context.CallActivityAsync("ProcessPayment", order);
    await context.CallActivityAsync("UpdateInventory", order);
    await context.CallActivityAsync("ShipOrder", order);
    
    return "Order processed successfully";
}
  • Benefits: automatic scaling, pay-per-execution pricing, and less operational overhead.
  • Challenges: cold starts, execution-time limits, and vendor lock-in.

Strangler Fig Pattern
#

The Strangler Fig Pattern enables incremental modernization of legacy systems by gradually replacing functionality while maintaining operational continuity.

graph TB subgraph "Phase 1: Initial State" C1[Client] --> LS1[Legacy System] end subgraph "Phase 2: Facade Introduction" C2[Client] --> F[Facade/Router] F --> LS2[Legacy System] F -.-> MS1[Modern Service A] end subgraph "Phase 3: Gradual Migration" C3[Client] --> F2[Facade/Router] F2 --> LS3[Legacy System] F2 --> MS2[Modern Service A] F2 --> MS3[Modern Service B] end subgraph "Phase 4: Complete Migration" C4[Client] --> MS4[Modern Service A] C4 --> MS5[Modern Service B] C4 --> MS6[Modern Service C] end style F fill:#ffeb3b style F2 fill:#ffeb3b style MS1 fill:#c8e6c9 style MS2 fill:#c8e6c9 style MS3 fill:#c8e6c9 style MS4 fill:#c8e6c9 style MS5 fill:#c8e6c9 style MS6 fill:#c8e6c9

Implementation Example
#

// Routing Facade Implementation
public class StranglerFacade : IHostedService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly IConfiguration _configuration;
    
    public async Task RouteRequest(HttpContext context)
    {
        var feature = DetermineFeature(context.Request.Path);
        
        if (IsModernizedFeature(feature))
        {
            await RouteToModernService(context, feature);
        }
        else
        {
            await RouteToLegacySystem(context);
        }
    }
    
    private bool IsModernizedFeature(string feature)
    {
        return _configuration.GetValue<bool>($"Features:{feature}:Modernized");
    }
    
    private async Task RouteToModernService(HttpContext context, string feature)
    {
        // Route to new microservice
        var modernServiceUrl = _configuration[$"Services:{feature}:Url"];
        // Implementation details...
    }
    
    private async Task RouteToLegacySystem(HttpContext context)
    {
        // Route to legacy system
        var legacyUrl = _configuration["LegacySystem:BaseUrl"];
        // Implementation details...
    }
}
  • Phases: establish a facade, migrate incrementally, migrate data, then retire the legacy system.
  • Advantages: lower risk, continuous operation, and a flexible timeline.
  • Trade-offs: running two systems at once and keeping their data consistent.

Resilience and Data Patterns
#

Circuit Breaker Pattern: Fault Tolerance
#

The Circuit Breaker Pattern prevents cascading failures by monitoring service health and failing fast when dependencies are unhealthy, like an electrical circuit breaker.

Circuit Breaker State Flow
#

stateDiagram-v2 [*] --> Closed Closed --> Open: Failure Threshold Exceeded Open --> HalfOpen: Timeout Expires HalfOpen --> Closed: Test Request Succeeds HalfOpen --> Open: Test Request Fails Closed: Normal Operation
Requests Pass Through
Monitor Failures Open: Circuit Tripped
Fail Fast
No Requests Allowed HalfOpen: Testing Recovery
Limited Test Requests
Evaluate Health note right of Closed Success Rate > Threshold Reset Failure Counter end note note right of Open Immediate Rejection Resource Protection Wait for Recovery end note note right of HalfOpen Single Test Request Quick Failure Detection Gradual Recovery end note

Implementation Example
#

// Modern .Net Implementation
services.AddHttpClient<PaymentService>()
    .AddStandardResilienceHandler(options =>
    {
        options.CircuitBreaker.FailureRatio = 0.5;
        options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(10);
        options.CircuitBreaker.MinimumThroughput = 8;
        options.CircuitBreaker.BreakDuration = TimeSpan.FromSeconds(30);
    });

// Custom Circuit Breaker with Polly v8
var circuitBreakerOptions = new CircuitBreakerStrategyOptions
{
    FailureRatio = 0.5,
    SamplingDuration = TimeSpan.FromSeconds(10),
    MinimumThroughput = 8,
    BreakDuration = TimeSpan.FromSeconds(30),
    ShouldHandle = new PredicateBuilder()
        .Handle<HttpRequestException>()
        .Handle<TimeoutRejectedException>()
        .HandleResult<HttpResponseMessage>(r => !r.IsSuccessStatusCode)
};

var resiliencePipeline = new ResiliencePipelineBuilder()
    .AddCircuitBreaker(circuitBreakerOptions)
    .Build();
  • States: Closed (normal), Open (tripped), and Half-Open (testing recovery).
  • Advantages: stops cascading failures, conserves resources, and improves the user experience.
  • Trade-offs: tuning the thresholds, and the odd false positive.

Outbox Pattern: Reliable Message Delivery
#

The Outbox Pattern ensures reliable message delivery by storing outbound messages in the same database transaction as business data, then publishing them asynchronously.

Outbox Pattern Flow
#

sequenceDiagram participant App as Application participant DB as Database participant OT as Outbox Table participant BP as Background Processor participant MB as Message Bus participant CS as Consuming Services Note over App,CS: Business Transaction with Reliable Messaging App->>DB: Begin Transaction App->>DB: Save Business Data App->>OT: Store Outbox Event App->>DB: Commit Transaction Note over BP: Polling Process (every 5s) BP->>OT: Query Unprocessed Events OT-->>BP: Pending Events loop For Each Event BP->>MB: Publish Event MB-->>BP: Acknowledge BP->>OT: Mark as Processed end MB->>CS: Deliver Event CS-->>MB: Acknowledge Note over App,CS: Guaranteed Delivery:
At-least-once semantics

Outbox Table Structure
#

erDiagram OUTBOX_EVENTS { guid Id PK string Type string Data datetime CreatedAt boolean IsProcessed datetime ProcessedAt int RetryCount string LastError } BUSINESS_DATA { int Id PK string Content datetime CreatedAt } OUTBOX_EVENTS ||--o{ BUSINESS_DATA : references

Implementation Example
#

// Outbox Event Entity
public class OutboxEvent
{
    public Guid Id { get; set; }
    public string Type { get; set; }
    public string Data { get; set; }
    public DateTime CreatedAt { get; set; }
    public bool IsProcessed { get; set; }
    public DateTime? ProcessedAt { get; set; }
}

// Service Implementation
public class OrderService
{
    private readonly ApplicationDbContext _context;
    
    public async Task CreateOrderAsync(Order order)
    {
        using var transaction = await _context.Database.BeginTransactionAsync();
        
        try
        {
            // Save business data
            _context.Orders.Add(order);
            
            // Save outbox event
            var outboxEvent = new OutboxEvent
            {
                Id = Guid.NewGuid(),
                Type = "OrderCreated",
                Data = JsonSerializer.Serialize(new OrderCreatedEvent(order.Id)),
                CreatedAt = DateTime.UtcNow,
                IsProcessed = false
            };
            
            _context.OutboxEvents.Add(outboxEvent);
            
            await _context.SaveChangesAsync();
            await transaction.CommitAsync();
        }
        catch
        {
            await transaction.RollbackAsync();
            throw;
        }
    }
}

// Outbox Processor
public class OutboxProcessor : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await ProcessOutboxEventsAsync();
            await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
        }
    }
    
    private async Task ProcessOutboxEventsAsync()
    {
        using var scope = _serviceProvider.CreateScope();
        var context = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
        var serviceBus = scope.ServiceProvider.GetRequiredService<IServiceBus>();
        
        var pendingEvents = await context.OutboxEvents
            .Where(e => !e.IsProcessed)
            .OrderBy(e => e.CreatedAt)
            .Take(100)
            .ToListAsync();
        
        foreach (var outboxEvent in pendingEvents)
        {
            try
            {
                await serviceBus.PublishAsync(outboxEvent.Type, outboxEvent.Data);
                
                outboxEvent.IsProcessed = true;
                outboxEvent.ProcessedAt = DateTime.UtcNow;
                
                await context.SaveChangesAsync();
            }
            catch (Exception ex)
            {
                // Log error and continue
            }
        }
    }
}
  • Benefits: guaranteed delivery, data consistency, and resilience to failures.
  • Challenges: added complexity, latency from async processing, and duplicate-message handling.

Modern .Net and Azure Integration
#

Latest .Net Features
#

.Net introduces significant performance improvements with Dynamic Profile-Guided Optimization (PGO) enabled by default, providing up to 20% performance gains. Enhanced JIT compilation with On Stack Replacement (OSR), AVX-512 support, and Native AOT improvements deliver faster startup times and reduced memory consumption.

Key scalability features include improved parallelism support, enhanced garbage collection optimized for high-throughput scenarios, and System.Text.Json improvements with source generators to avoid reflection overhead.

Azure Container Apps and Dapr Integration
#

Azure Container Apps provides fully managed Dapr integration with built-in support for Dapr runtime APIs, serverless container orchestration, and automatic scaling from zero to thousands of instances.

Azure Integration Architecture
#

graph TB subgraph "Azure Cloud Services" subgraph "Container Apps Environment" CA1[Order Service
Container App] CA2[Payment Service
Container App] CA3[Inventory Service
Container App] end subgraph "Dapr Runtime" D1[Service Discovery] D2[State Management] D3[Pub/Sub] D4[Secret Management] end subgraph "Azure Infrastructure" ASB[Azure Service Bus
Message Broker] ACR[Azure Container Registry
Image Storage] KV[Azure Key Vault
Secret Storage] COSMOS[Cosmos DB
State Store] AI[Application Insights
Monitoring] end end subgraph "External Systems" EXT[External APIs] LEGACY[Legacy Systems] end CA1 --> D1 CA2 --> D2 CA3 --> D3 D1 -.-> ASB D2 -.-> COSMOS D3 -.-> ASB D4 -.-> KV CA1 --> AI CA2 --> AI CA3 --> AI CA1 -.-> EXT CA2 -.-> LEGACY style CA1 fill:#e3f2fd style CA2 fill:#c8e6c9 style CA3 fill:#fff3e0 style ASB fill:#ffeb3b style COSMOS fill:#ffeb3b style KV fill:#ffeb3b

.NET Aspire Integration Flow
#

sequenceDiagram participant Dev as Developer participant Aspire as .NET Aspire participant Docker as Docker participant ACA as Azure Container Apps participant Dapr as Dapr Runtime participant Azure as Azure Services Dev->>Aspire: Define Distributed App Aspire->>Docker: Generate Containers Docker->>ACA: Deploy to Container Apps ACA->>Dapr: Initialize Dapr Sidecar Dapr->>Azure: Connect to Azure Services Note over Dev,Azure: Simplified Cloud-Native Development Azure-->>Dapr: Service Responses Dapr-->>ACA: Process Results ACA-->>Aspire: Runtime Telemetry Aspire-->>Dev: Monitoring Dashboard
// .NET Aspire Integration
var builder = DistributedApplication.CreateBuilder(args);

var catalog = builder.AddProject<Projects.Catalog_API>("catalog")
    .WithDaprSidecar();

var ordering = builder.AddProject<Projects.Ordering_API>("ordering")
    .WithDaprSidecar();

var gateway = builder.AddProject<Projects.Gateway>("gateway")
    .WithReference(catalog)
    .WithReference(ordering);

builder.Build().Run();

.NET Aspire provides opinionated tooling for building observable, production-ready distributed applications with 40+ pre-built integrations and built-in telemetry capabilities.

Deployment and Infrastructure Patterns
#

Sidecar Pattern: Auxiliary Service Deployment
#

The Sidecar Pattern deploys auxiliary services alongside main applications, providing cross-cutting functionality without modifying the core application logic.

Sidecar Architecture Overview
#

graph TB subgraph "Pod/Container Group" subgraph "Main Application" MA[Order Service
.Net API] end subgraph "Sidecar Services" LS[Logging Sidecar
Fluentd] MS[Monitoring Sidecar
Prometheus Exporter] SS[Security Sidecar
Service Mesh Proxy] CS[Configuration Sidecar
Config Sync] end SV[Shared Volumes
Logs, Config, Temp] end subgraph "External Services" ELK[ELK Stack
Log Aggregation] PROM[Prometheus
Metrics Collection] VAULT[HashiCorp Vault
Secret Management] CONSUL[Consul
Service Discovery] end MA -.-> SV LS -.-> SV MS -.-> SV SS -.-> SV CS -.-> SV LS --> ELK MS --> PROM SS --> VAULT CS --> CONSUL style MA fill:#ffeb3b style LS fill:#c8e6c9 style MS fill:#e3f2fd style SS fill:#ffcdd2 style CS fill:#fff3e0

Sidecar Communication Patterns
#

sequenceDiagram participant C as Client participant P as Proxy Sidecar participant M as Main App participant L as Logging Sidecar participant Mon as Monitoring Sidecar participant E as External Services C->>P: HTTP Request P->>P: Apply Security Policies P->>M: Forward Request P->>Mon: Record Metrics M->>M: Process Business Logic M->>L: Write Application Logs M->>P: Return Response P->>Mon: Record Response Metrics P->>C: Forward Response L->>E: Ship Logs to ELK Mon->>E: Send Metrics to Prometheus

Sidecar vs Traditional Architecture
#

graph LR subgraph "Traditional Monolithic" T1[Application + Infrastructure Code
Tightly Coupled] T1 --> T2[Cross-cutting Concerns
Mixed with Business Logic] T2 --> T3[Technology Lock-in
Hard to Change] end subgraph "Sidecar Pattern" S1[Clean Application
Business Logic Only] S2[Infrastructure Sidecars
Loosely Coupled] S1 -.-> S2 S2 --> S3[Technology Independence
Easy to Swap] end style T3 fill:#ffcdd2 style S3 fill:#c8e6c9

Implementation Example
#

// Main Application Service
[ApiController]
[Route("api/[controller]")]
public class OrderController : ControllerBase
{
    private readonly IOrderService _orderService;
    
    [HttpPost]
    public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest request)
    {
        // Main business logic - sidecar handles logging, monitoring automatically
        var order = await _orderService.CreateAsync(request);
        return Ok(order);
    }
}

// Sidecar Configuration (docker-compose.yml)
version: '3.8'
services:
  order-service:
    image: myapp/order-service:latest
    ports:
      - "8080:80"
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
    
  # Logging Sidecar
  fluentd-sidecar:
    image: fluent/fluentd:latest
    volumes:
      - ./fluentd.conf:/fluentd/etc/fluent.conf
      - order-logs:/var/log/orders
    depends_on:
      - order-service
  
  # Monitoring Sidecar
  prometheus-exporter:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml

A minimal health-check sidecar in .NET:

// Health Check Sidecar
public class HealthCheckSidecar : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly ILogger<HealthCheckSidecar> _logger;
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using var scope = _serviceProvider.CreateScope();
            var healthCheckService = scope.ServiceProvider.GetRequiredService<HealthCheckService>();
            
            var result = await healthCheckService.CheckHealthAsync(stoppingToken);
            
            // Report health status to external monitoring
            await ReportHealthStatusAsync(result);
            
            await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
        }
    }
}
  • When to use: cross-cutting concerns like logging and monitoring, enhancing a legacy app, or shared functionality across microservices.
  • Advantages: language-agnostic, isolated, and it keeps the main app simple.
  • Trade-offs: more resource use and orchestration overhead.

Ambassador Pattern: Proxy-Based Service Enhancement
#

The Ambassador Pattern creates helper services that send network requests on behalf of consumer applications, acting as an out-of-process proxy for enhanced connectivity.

Ambassador Pattern Architecture
#

graph TB subgraph "Application Cluster" subgraph "Consumer Application" CA[Order Service
Main Business Logic] end subgraph "Ambassador Services" PA[Payment Ambassador
Enhanced Connectivity] IA[Inventory Ambassador
Retry & Circuit Breaker] NA[Notification Ambassador
Rate Limiting] end end subgraph "External Services" PS[Payment Service
Third-party API] IS[Inventory Service
Legacy System] NS[Notification Service
Email Provider] end CA --> PA CA --> IA CA --> NA PA --> PS IA --> IS NA --> NS style CA fill:#ffeb3b style PA fill:#c8e6c9 style IA fill:#e3f2fd style NA fill:#fff3e0

Ambassador Request Flow
#

sequenceDiagram participant App as Main Application participant Amb as Ambassador participant Auth as Auth Service participant Ext as External Service participant Cache as Cache participant Log as Logging App->>Amb: Process Payment Request Note over Amb: Ambassador Handles All Complexity Amb->>Cache: Check Cached Token Cache-->>Amb: Token Expired Amb->>Auth: Acquire Access Token Auth-->>Amb: New Token Amb->>Cache: Cache Token Amb->>Log: Log Request Start Amb->>Ext: Payment API Call (with retry logic) alt Success Ext-->>Amb: Payment Success Amb->>Log: Log Success Amb-->>App: Payment Response else Failure Ext-->>Amb: Payment Failed Amb->>Amb: Apply Circuit Breaker Amb->>Log: Log Failure Amb-->>App: Fallback Response end

Ambassador vs Direct Integration
#

graph LR subgraph "Direct Integration" D1[Application Code
+ Auth Logic
+ Retry Logic
+ Monitoring
+ Circuit Breaker] D1 --> D2[Complex Codebase
Hard to Test
Technology Coupling] end subgraph "Ambassador Pattern" A1[Clean Application
Business Logic Only] A2[Ambassador Service
Connectivity Logic] A1 --> A2 A2 --> A3[Specialized Teams
Easy Testing
Technology Abstraction] end style D2 fill:#ffcdd2 style A3 fill:#c8e6c9

Implementation Example
#

// Ambassador Service
public class PaymentAmbassador
{
    private readonly HttpClient _httpClient;
    private readonly ILogger<PaymentAmbassador> _logger;
    private readonly CircuitBreakerPolicy _circuitBreaker;
    
    public PaymentAmbassador(HttpClient httpClient, ILogger<PaymentAmbassador> logger)
    {
        _httpClient = httpClient;
        _logger = logger;
        _circuitBreaker = Policy
            .Handle<HttpRequestException>()
            .CircuitBreakerAsync(
                exceptionsAllowedBeforeBreaking: 3,
                durationOfBreak: TimeSpan.FromSeconds(30));
    }
    
    public async Task<PaymentResponse> ProcessPaymentAsync(PaymentRequest request)
    {
        return await _circuitBreaker.ExecuteAsync(async () =>
        {
            _logger.LogInformation("Processing payment for Order {OrderId}", request.OrderId);
            
            // Add authentication headers
            _httpClient.DefaultRequestHeaders.Authorization = 
                new AuthenticationHeaderValue("Bearer", await GetAccessTokenAsync());
            
            // Add tracing headers
            _httpClient.DefaultRequestHeaders.Add("X-Trace-Id", Activity.Current?.Id);
            
            var response = await _httpClient.PostAsJsonAsync("/api/payments", request);
            response.EnsureSuccessStatusCode();
            
            var result = await response.Content.ReadFromJsonAsync<PaymentResponse>();
            
            _logger.LogInformation("Payment processed successfully for Order {OrderId}", request.OrderId);
            
            return result;
        });
    }
    
    private async Task<string> GetAccessTokenAsync()
    {
        // Token acquisition logic
        return await GetCachedTokenAsync();
    }
}

// Main Application using Ambassador
public class OrderService
{
    private readonly PaymentAmbassador _paymentAmbassador;
    
    public async Task<Order> ProcessOrderAsync(CreateOrderRequest request)
    {
        var order = new Order(request);
        
        // Ambassador handles all payment complexity
        var paymentResult = await _paymentAmbassador.ProcessPaymentAsync(
            new PaymentRequest(order.Id, order.TotalAmount));
        
        if (paymentResult.IsSuccessful)
        {
            order.MarkAsPaid();
        }
        
        return order;
    }
}
  • When to use: enhancing a legacy app, shared connectivity patterns, or specialized network requirements.
  • Advantages: a specialist team can own it, it abstracts the transport, and it adds capability without touching the app.
  • Trade-offs: extra network latency and trickier failure handling.

Bulkhead Pattern: Resource Isolation for Resilience
#

The Bulkhead Pattern isolates resources into separate pools to prevent failure in one component from affecting the entire system, similar to watertight compartments in a ship.

Resource Isolation Architecture
#

graph TB subgraph "Application Layer" AC[API Controller] end subgraph "Bulkhead Service Pools" subgraph "Critical Services Pool" CP1[Thread Pool: 10] CP2[Connection Pool: 5] CP3[Memory Pool: 2GB] end subgraph "Standard Services Pool" SP1[Thread Pool: 5] SP2[Connection Pool: 3] SP3[Memory Pool: 1GB] end subgraph "Background Tasks Pool" BP1[Thread Pool: 3] BP2[Connection Pool: 2] BP3[Memory Pool: 512MB] end end subgraph "Service Types" CS[Critical Services
Payment, Authentication] SS[Standard Services
Product Catalog, Search] BT[Background Tasks
Analytics, Cleanup] end AC --> CP1 AC --> SP1 AC --> BP1 CP1 --> CS SP1 --> SS BP1 --> BT style CP1 fill:#ffcdd2 style SP1 fill:#fff3e0 style BP1 fill:#e8f5e8 style CS fill:#ffcdd2 style SS fill:#fff3e0 style BT fill:#e8f5e8

Failure Isolation Demonstration
#

graph LR subgraph "Without Bulkhead" WB1[Shared Thread Pool
20 Threads] --> WB2[All Services
Competing for Resources] WB2 --> WB3[One Service Failure
Affects All Services] end subgraph "With Bulkhead" B1[Critical Pool
10 Threads] --> B2[Payment Service
Isolated & Protected] B3[Standard Pool
5 Threads] --> B4[Catalog Service
Limited Impact] B5[Background Pool
3 Threads] --> B6[Analytics Service
Failure Contained] end style WB3 fill:#ffcdd2 style B2 fill:#c8e6c9 style B4 fill:#c8e6c9 style B6 fill:#c8e6c9

Implementation Example
#

// Resource Pool Configuration
public class BulkheadConfiguration
{
    public int CriticalServiceThreads { get; set; } = 10;
    public int StandardServiceThreads { get; set; } = 5;
    public int BackgroundTaskThreads { get; set; } = 3;
}

// Bulkhead Implementation
public class BulkheadService
{
    private readonly SemaphoreSlim _criticalSemaphore;
    private readonly SemaphoreSlim _standardSemaphore;
    private readonly SemaphoreSlim _backgroundSemaphore;
    
    public BulkheadService(BulkheadConfiguration config)
    {
        _criticalSemaphore = new SemaphoreSlim(config.CriticalServiceThreads);
        _standardSemaphore = new SemaphoreSlim(config.StandardServiceThreads);
        _backgroundSemaphore = new SemaphoreSlim(config.BackgroundTaskThreads);
    }
    
    public async Task<T> ExecuteCriticalOperationAsync<T>(Func<Task<T>> operation)
    {
        await _criticalSemaphore.WaitAsync();
        try
        {
            return await operation();
        }
        finally
        {
            _criticalSemaphore.Release();
        }
    }
    
    public async Task<T> ExecuteStandardOperationAsync<T>(Func<Task<T>> operation)
    {
        await _standardSemaphore.WaitAsync();
        try
        {
            return await operation();
        }
        finally
        {
            _standardSemaphore.Release();
        }
    }
}

// Usage in Controller
[ApiController]
public class ProductController : ControllerBase
{
    private readonly BulkheadService _bulkheadService;
    private readonly IProductService _productService;
    private readonly IRecommendationService _recommendationService;
    
    [HttpGet("{id}")]
    public async Task<IActionResult> GetProduct(int id)
    {
        // Critical operation - guaranteed resources
        var product = await _bulkheadService.ExecuteCriticalOperationAsync(async () =>
            await _productService.GetByIdAsync(id));
        
        // Non-critical operation - limited resources
        var recommendations = await _bulkheadService.ExecuteStandardOperationAsync(async () =>
            await _recommendationService.GetRecommendationsAsync(id));
        
        return Ok(new { Product = product, Recommendations = recommendations });
    }
}
  • When to use: high-traffic apps, mixed workload priorities, or systems that need guaranteed resource availability.
  • Advantages: failure isolation, better resource use, and stronger resilience.
  • Trade-offs: more complexity and some pools sitting idle.

Event Sourcing Pattern: Complete Audit Trail
#

Event Sourcing stores application state as a sequence of events rather than current state, providing complete audit trails and enabling temporal queries.

Event Sourcing Architecture
#

graph TB subgraph "Command Side" C1[Commands] --> AR[Aggregate Root] AR --> ES[Event Store] end subgraph "Event Store" ES --> E1[Event Stream 1
Order-123] ES --> E2[Event Stream 2
Order-456] ES --> E3[Event Stream 3
Order-789] end subgraph "Event Stream Details" E1 --> EV1[OrderCreated
v1] EV1 --> EV2[ItemAdded
v2] EV2 --> EV3[PaymentProcessed
v3] EV3 --> EV4[OrderShipped
v4] end subgraph "Read Side (CQRS)" ES --> EP[Event Projections] EP --> RM1[Read Model 1
Order Summary] EP --> RM2[Read Model 2
Analytics] EP --> RM3[Read Model 3
Audit Log] end subgraph "Temporal Queries" ES --> TQ[Time Travel Queries] TQ --> HS1[State at 2024-01-01] TQ --> HS2[State at 2024-06-01] TQ --> HS3[Current State] end style AR fill:#ffeb3b style ES fill:#e3f2fd style EP fill:#c8e6c9 style TQ fill:#fff3e0

Event Sourcing vs Traditional Storage
#

graph LR subgraph "Traditional CRUD" T1[Current State Only
Order: {Status: Shipped}] --> T2[Lost History
No Audit Trail] T2 --> T3[Update Overwrites
Previous Data] end subgraph "Event Sourcing" E1[Event Stream
All Changes Recorded] --> E2[Complete History
Full Audit Trail] E2 --> E3[Append Only
Immutable Events] E3 --> E4[Temporal Queries
State at Any Point] end style T3 fill:#ffcdd2 style E4 fill:#c8e6c9

Event Stream Timeline
#

timeline title Order-123 Event Timeline 2024-01-15 09:00 : OrderCreated : Customer: "John Doe" : Items: [Product-A, Product-B] 2024-01-15 09:15 : ItemAdded : Product: "Product-C" : Quantity: 2 2024-01-15 10:30 : PaymentProcessed : Amount: $150.00 : Method: "Credit Card" 2024-01-16 14:00 : OrderShipped : Carrier: "FedEx" : Tracking: "123456789" 2024-01-18 16:00 : OrderDelivered : Status: "Completed" : Signature: "J.Doe"

Implementation Example
#

// Domain Events
public abstract record DomainEvent(Guid AggregateId, DateTime OccurredAt);

public record OrderCreated(Guid OrderId, string CustomerId, List<OrderItem> Items, DateTime OccurredAt) 
    : DomainEvent(OrderId, OccurredAt);

public record OrderItemAdded(Guid OrderId, string ProductId, int Quantity, decimal Price, DateTime OccurredAt)
    : DomainEvent(OrderId, OccurredAt);

public record OrderShipped(Guid OrderId, string TrackingNumber, DateTime OccurredAt)
    : DomainEvent(OrderId, OccurredAt);

// Event Store
public interface IEventStore
{
    Task SaveEventsAsync(Guid aggregateId, IEnumerable<DomainEvent> events, long expectedVersion);
    Task<IEnumerable<DomainEvent>> GetEventsAsync(Guid aggregateId);
    Task<IEnumerable<DomainEvent>> GetEventsAsync(Guid aggregateId, DateTime fromDate);
}

// Aggregate Root with Event Sourcing
public class Order
{
    private readonly List<DomainEvent> _uncommittedEvents = new();
    
    public Guid Id { get; private set; }
    public string CustomerId { get; private set; }
    public List<OrderItem> Items { get; private set; } = new();
    public OrderStatus Status { get; private set; }
    public long Version { get; private set; }
    
    // Constructor for new orders
    public Order(string customerId, List<OrderItem> items)
    {
        var orderCreated = new OrderCreated(Guid.NewGuid(), customerId, items, DateTime.UtcNow);
        Apply(orderCreated);
        _uncommittedEvents.Add(orderCreated);
    }
    
    // Constructor for rebuilding from events
    public Order(IEnumerable<DomainEvent> events)
    {
        foreach (var @event in events)
        {
            Apply(@event);
            Version++;
        }
    }
    
    public void AddItem(string productId, int quantity, decimal price)
    {
        var itemAdded = new OrderItemAdded(Id, productId, quantity, price, DateTime.UtcNow);
        Apply(itemAdded);
        _uncommittedEvents.Add(itemAdded);
    }
    
    public void Ship(string trackingNumber)
    {
        var orderShipped = new OrderShipped(Id, trackingNumber, DateTime.UtcNow);
        Apply(orderShipped);
        _uncommittedEvents.Add(orderShipped);
    }
    
    private void Apply(DomainEvent @event)
    {
        switch (@event)
        {
            case OrderCreated created:
                Id = created.OrderId;
                CustomerId = created.CustomerId;
                Items = created.Items.ToList();
                Status = OrderStatus.Created;
                break;
            case OrderItemAdded itemAdded:
                Items.Add(new OrderItem(itemAdded.ProductId, itemAdded.Quantity, itemAdded.Price));
                break;
            case OrderShipped shipped:
                Status = OrderStatus.Shipped;
                break;
        }
    }
    
    public IEnumerable<DomainEvent> GetUncommittedEvents() => _uncommittedEvents.AsReadOnly();
    
    public void MarkEventsAsCommitted() => _uncommittedEvents.Clear();
}

// Repository Implementation
public class OrderRepository
{
    private readonly IEventStore _eventStore;
    
    public async Task SaveAsync(Order order)
    {
        var events = order.GetUncommittedEvents();
        if (events.Any())
        {
            await _eventStore.SaveEventsAsync(order.Id, events, order.Version);
            order.MarkEventsAsCommitted();
        }
    }
    
    public async Task<Order> GetByIdAsync(Guid orderId)
    {
        var events = await _eventStore.GetEventsAsync(orderId);
        return events.Any() ? new Order(events) : null;
    }
    
    public async Task<Order> GetOrderStateAtDateAsync(Guid orderId, DateTime asOfDate)
    {
        var events = await _eventStore.GetEventsAsync(orderId, asOfDate);
        return events.Any() ? new Order(events) : null;
    }
}
  • Benefits: a complete audit trail, temporal queries, easier debugging, and a natural fit with event-driven systems.
  • Challenges: complexity, eventual consistency, and querying that usually pushes you toward CQRS.

Pattern Combination Strategies
#

Most real systems combine a few patterns. These pairings show up again and again:

graph LR subgraph "Enterprise Combo" E1[Onion Architecture] --> E2[CQRS] E2 --> E3[Event Sourcing] E3 --> E4[Saga Pattern] end subgraph "Microservices Combo" M1[API Gateway] --> M2[BFF] M2 --> M3[Circuit Breaker] M3 --> M4[Bulkhead] end subgraph "Legacy Migration Combo" L1[Strangler Fig] --> L2[Event-Driven] L2 --> L3[Outbox Pattern] end style E1 fill:#ffeb3b style M1 fill:#c8e6c9 style L1 fill:#fff3e0

Enterprise-Grade Pattern Combinations
#

  • Onion + CQRS + Event Sourcing: Complex enterprise applications with rich business logic

    • Use case: Financial systems, healthcare platforms, enterprise resource planning
    • Benefits: Domain-driven design, complete audit trail, high performance
    • Complexity: High, requires experienced team
  • Hexagonal + Event-Driven + Bulkhead: Technology-agnostic business logic with asynchronous communication and resource isolation

    • Use case: Microservices architectures, distributed systems
    • Benefits: Technology independence, fault isolation, scalability
    • Complexity: Medium to High

Client-Facing Pattern Combinations
#

  • API Gateway + BFF + Circuit Breaker: Client-facing applications requiring resilience and optimization

    • Use case: Multi-platform applications (web, mobile, IoT)
    • Benefits: Client optimization, centralized routing, fault tolerance
    • Complexity: Medium
  • BFF + Serverless + Ambassador: Modern client-optimized backends

    • Use case: Event-driven applications with multiple clients
    • Benefits: Auto-scaling, cost efficiency, enhanced connectivity
    • Complexity: Medium

Legacy Modernization Combinations
#

  • Strangler Fig + BFF + Serverless: Modernizing legacy systems using client-specific backends and event-driven Functions

    • Use case: Legacy system modernization with minimal disruption
    • Benefits: Gradual migration, reduced risk, modern architecture
    • Complexity: Medium to High
  • Strangler Fig + Event-Driven + Outbox: Reliable legacy integration with modern event systems

    • Use case: Legacy systems requiring reliable message delivery
    • Benefits: Reliable messaging, gradual modernization, data consistency
    • Complexity: High

Resilience-Focused Combinations
#

  • Circuit Breaker + Bulkhead + Outbox: layered resilience with failure isolation and reliable messaging

    • Use case: High-availability systems, critical business applications
    • Benefits: Fault tolerance, resource isolation, message reliability
    • Complexity: Medium
  • Sidecar + Ambassador + Circuit Breaker: Infrastructure patterns for cross-cutting concerns

    • Use case: Microservices requiring common infrastructure capabilities
    • Benefits: Technology independence, enhanced connectivity, fault tolerance
    • Complexity: Medium to High

Pattern Relationship Map
#

How the pattern families feed into each other:

1. Pattern Categories Overview
#

graph LR subgraph "Pattern Categories" F["🏗️ FOUNDATION
Core Architecture
▫️ Layered
▫️ Hexagonal
▫️ Onion"] C["💬 COMMUNICATION
Data & Messages
▫️ CQRS
▫️ Event-Driven
▫️ Saga"] I["🔗 INTEGRATION
Service Connection
▫️ API Gateway
▫️ BFF
▫️ Serverless
▫️ Strangler Fig"] R["🛡️ RESILIENCE
Fault Tolerance
▫️ Circuit Breaker
▫️ Bulkhead
▫️ Outbox"] IN["🚀 INFRASTRUCTURE
Deployment Support
▫️ Sidecar
▫️ Ambassador"] D["📊 DATA & EVENTS
State Management
▫️ Event Sourcing"] end F --> C C --> I I --> R R --> IN C --> D style F fill:#e8f5e8,stroke:#4caf50,stroke-width:3px style C fill:#f3e5f5,stroke:#9c27b0,stroke-width:3px style I fill:#e1f5fe,stroke:#03a9f4,stroke-width:3px style R fill:#fff8e1,stroke:#ffc107,stroke-width:3px style IN fill:#fce4ec,stroke:#e91e63,stroke-width:3px style D fill:#f1f8e9,stroke:#8bc34a,stroke-width:3px

2. Foundation to Communication Flow
#

graph TB subgraph "🏗️ Foundation Patterns" F1["LAYERED
Simple 3-tier
👥 Junior Level
⭐ Low Complexity"] F2["HEXAGONAL
Ports & Adapters
👨‍💻 Senior Level
⭐⭐ Medium Complexity"] F3["ONION
Domain-Centric
👨‍💻 Senior Level
⭐⭐ Medium Complexity"] end subgraph "💬 Communication Patterns" C1["CQRS
Separate Read/Write
👨‍💻 Senior Level
⭐⭐ Medium Complexity"] C2["EVENT-DRIVEN
Async Messaging
👨‍💻 Senior Level
⭐⭐⭐ High Complexity"] C3["SAGA
Distributed Transactions
👨‍💻 Senior Level
⭐⭐⭐ High Complexity"] end F2 ==>|"Enables Clean Testing"| C1 F3 ==>|"Domain-Driven Design"| C1 C1 ==>|"Often Combined"| C2 C2 ==>|"Orchestrates Complex Workflows"| C3 style F1 fill:#e8f5e8,stroke:#4caf50,stroke-width:3px,color:#000 style F2 fill:#e8f5e8,stroke:#4caf50,stroke-width:3px,color:#000 style F3 fill:#e8f5e8,stroke:#4caf50,stroke-width:3px,color:#000 style C1 fill:#f3e5f5,stroke:#9c27b0,stroke-width:3px,color:#000 style C2 fill:#f3e5f5,stroke:#9c27b0,stroke-width:3px,color:#000 style C3 fill:#f3e5f5,stroke:#9c27b0,stroke-width:3px,color:#000

3. Integration and Resilience Patterns
#

graph TB subgraph "� Integration Patterns" I1["API GATEWAY
Single Entry Point
� Intermediate
⭐⭐ Medium Complexity"] I2["BFF
Backend for Frontend
👥 Intermediate
⭐ Low Complexity"] I3["SERVERLESS
Event-Driven Functions
� Intermediate
⭐ Low Complexity"] I4["STRANGLER FIG
Legacy Modernization
👨‍💻 Senior
⭐⭐ Medium Complexity"] end subgraph "🛡️ Resilience Patterns" R1["CIRCUIT BREAKER
Fail Fast Protection
👥 Intermediate
⭐ Low Complexity"] R2["BULKHEAD
Resource Isolation
👥 Intermediate
⭐ Low Complexity"] R3["OUTBOX
Reliable Messaging
👨‍💻 Senior
⭐⭐ Medium Complexity"] end I1 ==>|"Client Optimization"| I2 I1 ==>|"Auto-Scaling"| I3 I1 ==>|"Protection Layer"| R1 R1 ==>|"Resource Protection"| R2 I4 ==>|"Modern Architecture"| I1 style I1 fill:#e1f5fe,stroke:#03a9f4,stroke-width:3px,color:#000 style I2 fill:#e1f5fe,stroke:#03a9f4,stroke-width:3px,color:#000 style I3 fill:#e1f5fe,stroke:#03a9f4,stroke-width:3px,color:#000 style I4 fill:#fff3e0,stroke:#ff9800,stroke-width:3px,color:#000 style R1 fill:#fff8e1,stroke:#ffc107,stroke-width:3px,color:#000 style R2 fill:#fff8e1,stroke:#ffc107,stroke-width:3px,color:#000 style R3 fill:#fff8e1,stroke:#ffc107,stroke-width:3px,color:#000

4. Infrastructure and Data Patterns
#

graph TB subgraph "🚀 Infrastructure Patterns" IN1["SIDECAR
Cross-Cutting Concerns
👥 Intermediate
⭐⭐ Medium Complexity"] IN2["AMBASSADOR
External Service Proxy
👥 Intermediate
⭐⭐ Medium Complexity"] end subgraph "📊 Data & Event Patterns" D1["EVENT SOURCING
Complete Audit Trail
👨‍💻 Senior
⭐⭐⭐ High Complexity"] end subgraph "Integration Support" IS1["Enhanced Monitoring"] IS2["Service Mesh"] IS3["Temporal Queries"] end IN1 ==>|"Provides"| IS1 IN2 ==>|"Enables"| IS2 D1 ==>|"Supports"| IS3 style IN1 fill:#fce4ec,stroke:#e91e63,stroke-width:3px,color:#000 style IN2 fill:#fce4ec,stroke:#e91e63,stroke-width:3px,color:#000 style D1 fill:#f1f8e9,stroke:#8bc34a,stroke-width:3px,color:#000 style IS1 fill:#e8eaf6,stroke:#3f51b5,stroke-width:2px,color:#000 style IS2 fill:#e8eaf6,stroke:#3f51b5,stroke-width:2px,color:#000 style IS3 fill:#e8eaf6,stroke:#3f51b5,stroke-width:2px,color:#000

5. Common Pattern Combinations
#

graph LR subgraph "🏢 Enterprise Stack" E1["Onion Architecture"] E2["CQRS"] E3["Event Sourcing"] E4["Outbox Pattern"] E1 --> E2 E2 --> E3 E2 --> E4 end subgraph "🌐 Microservices Stack" M1["Hexagonal Architecture"] M2["Event-Driven"] M3["API Gateway"] M4["Circuit Breaker"] M1 --> M2 M2 --> M3 M3 --> M4 end subgraph "☁️ Cloud-Native Stack" C1["Serverless"] C2["BFF"] C3["Ambassador"] C4["Sidecar"] C1 --> C2 C2 --> C3 C3 --> C4 end subgraph "🔄 Legacy Modernization" L1["Strangler Fig"] L2["API Gateway"] L3["Event-Driven"] L4["Circuit Breaker"] L1 --> L2 L2 --> L3 L3 --> L4 end style E1 fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px style M1 fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px style C1 fill:#e8f5e8,stroke:#4caf50,stroke-width:2px style L1 fill:#fff3e0,stroke:#ff9800,stroke-width:2px

The arrows and styling in the map above read as:

  • Solid arrows (—›): Strong dependencies or common combinations
  • Dotted arrows (-.→): Optional enhancements or frequent pairings
  • Line thickness: Indicates how commonly patterns are used together
  • Color coding: Groups patterns by primary purpose and complexity

Four combinations worth committing to memory:

  1. Modern Microservices Stack: Hexagonal + CQRS + Event-Driven + API Gateway + Circuit Breaker
  2. Enterprise Application: Onion + CQRS + Outbox + BFF + Sidecar
  3. Cloud-Native Solution: Serverless + Event-Driven + Circuit Breaker + Ambassador
  4. Legacy Modernization: Strangler Fig + Hexagonal + Event-Driven + API Gateway

Architecture Decision Framework
#

Full Pattern Comparison Matrix
#

PatternComplexityScalabilityMaintainabilityTeam SkillTime to ValuePrimary Use Cases
🏗️ Foundation Patterns
LayeredLow ⭐Medium ⭐⭐Medium ⭐⭐Junior+ 👥Fast ⚡CRUD apps, rapid prototyping
HexagonalMedium ⭐⭐High ⭐⭐⭐High ⭐⭐⭐Senior 👨‍💻Medium 🔄Complex business logic, testability
OnionMedium ⭐⭐High ⭐⭐⭐High ⭐⭐⭐Senior 👨‍💻Medium 🔄DDD, enterprise applications
💬 Communication Patterns
CQRSMedium ⭐⭐Very High ⭐⭐⭐⭐Medium ⭐⭐Senior 👨‍💻Medium 🔄Read/write optimization
Event-DrivenHigh ⭐⭐⭐Very High ⭐⭐⭐⭐Medium ⭐⭐Senior 👨‍💻Slow 🐌Microservices, real-time
SagaHigh ⭐⭐⭐High ⭐⭐⭐Medium ⭐⭐Senior 👨‍💻Slow 🐌Distributed transactions
🔗 Integration Patterns
API GatewayMedium ⭐⭐High ⭐⭐⭐Medium ⭐⭐Intermediate 👥Fast ⚡Service aggregation
BFFLow ⭐Medium ⭐⭐Medium ⭐⭐Intermediate 👥Fast ⚡Client-specific APIs
ServerlessLow ⭐Very High ⭐⭐⭐⭐Medium ⭐⭐Intermediate 👥Fast ⚡Event-driven, auto-scale
Strangler FigMedium ⭐⭐Medium ⭐⭐High ⭐⭐⭐Senior 👨‍💻Slow 🐌Legacy modernization
🛡️ Resilience Patterns
Circuit BreakerLow ⭐High ⭐⭐⭐High ⭐⭐⭐Intermediate 👥Fast ⚡Fault tolerance
BulkheadLow ⭐High ⭐⭐⭐High ⭐⭐⭐Intermediate 👥Fast ⚡Resource isolation
OutboxMedium ⭐⭐Medium ⭐⭐High ⭐⭐⭐Senior 👨‍💻Medium 🔄Reliable messaging
🚀 Infrastructure Patterns
SidecarMedium ⭐⭐High ⭐⭐⭐High ⭐⭐⭐Intermediate 👥Medium 🔄Cross-cutting concerns
AmbassadorMedium ⭐⭐High ⭐⭐⭐High ⭐⭐⭐Intermediate 👥Medium 🔄External service proxy
📊 Data & Event Patterns
Event SourcingHigh ⭐⭐⭐High ⭐⭐⭐Medium ⭐⭐Senior 👨‍💻Slow 🐌Audit trail, temporal queries

Team Size and Project Type Recommendations
#

graph LR subgraph TeamSize["👥 Team Size Guide"] T1["Small Team
(1-3 devs)
📊 Focus: Simplicity"] T2["Medium Team
(4-8 devs)
📊 Focus: Structure"] T3["Large Team
(9+ devs)
📊 Focus: Boundaries"] end subgraph ProjectType["🎯 Project Type"] P1["MVP/Prototype
⚡ Speed First"] P2["Enterprise App
🏢 Quality First"] P3["Microservices
🌐 Scale First"] P4["Legacy Migration
🔄 Safety First"] end T1 --> P1 T1 --> |"with guidance"| P2 T2 --> P2 T2 --> P3 T3 --> P2 T3 --> P3 T3 --> P4 P1 -.-> R1["✅ Layered + Serverless
🚀 API Gateway + Circuit Breaker"] P2 -.-> R2["✅ Hexagonal/Onion + CQRS
🚀 BFF + Outbox + Sidecar"] P3 -.-> R3["✅ Event-Driven + API Gateway
🚀 Circuit Breaker + Ambassador + Saga"] P4 -.-> R4["✅ Strangler Fig + Hexagonal
🚀 Event-Driven + API Gateway"] style T1 fill:#e8f5e8 style T2 fill:#fff3e0 style T3 fill:#ffebee style P1 fill:#e3f2fd style P2 fill:#f3e5f5 style P3 fill:#e8f5e8 style P4 fill:#fff8e1

| BFF | Medium | High | Medium | Intermediate | Multi-platform apps, API optimization | | Sidecar | Low | Medium | High | Intermediate | Cross-cutting concerns, auxiliary services | | Ambassador | Medium | Medium | High | Intermediate | Legacy enhancement, proxy patterns | | Bulkhead | Medium | High | High | Senior | Resource isolation, fault tolerance | | Event Sourcing | High | Very High | Medium | Senior | Audit trails, temporal queries |

Performance and Migration Trade-offs
#

Two more lenses on the same patterns: what each costs at runtime, and how you migrate an existing system toward them.

Performance Impact Comparison
#

graph TB subgraph "Performance Metrics by Pattern" subgraph "Low Latency (< 50ms)" LL1[Layered Architecture] LL2[Repository Pattern] LL3[Circuit Breaker] end subgraph "Medium Latency (50-200ms)" ML1[Hexagonal Architecture] ML2[API Gateway] ML3[CQRS] ML4[BFF] end subgraph "Higher Latency (200ms+)" HL1[Event-Driven Architecture] HL2[Saga Pattern] HL3[Event Sourcing] HL4[Microservices Mesh] end end subgraph "Scalability Rating" subgraph "High Scalability (1000+ TPS)" HS1[CQRS + Event Sourcing] HS2[Serverless Functions] HS3[Event-Driven + Bulkhead] end subgraph "Medium Scalability (100-1000 TPS)" MS1[API Gateway + BFF] MS2[Hexagonal + Repository] MS3[Layered + Caching] end subgraph "Limited Scalability (< 100 TPS)" LS1[Simple Layered] LS2[Monolithic Architecture] end end style LL1 fill:#c8e6c9 style LL2 fill:#c8e6c9 style LL3 fill:#c8e6c9 style HS1 fill:#ffeb3b style HS2 fill:#ffeb3b style HS3 fill:#ffeb3b style HL1 fill:#ffcdd2 style HL2 fill:#ffcdd2 style HL3 fill:#ffcdd2

Pattern Migration Paths
#

flowchart TD Start[Existing System] --> Assessment{Current Architecture?} Assessment -->|Monolith| Mono[Monolithic System] Assessment -->|Layered| Layer[Layered Architecture] Assessment -->|Legacy| Legacy[Legacy System] Mono --> Step1[Step 1: Add API Gateway] Layer --> Step2[Step 2: Implement CQRS] Legacy --> Step3[Step 3: Strangler Fig Pattern] Step1 --> Step4[Step 4: Extract Microservices] Step2 --> Step5[Step 5: Add Event-Driven] Step3 --> Step6[Step 6: Modern Service Implementation] Step4 --> Final1[Modern Microservices] Step5 --> Final2[Event-Driven Architecture] Step6 --> Final3[Modernized System] Final1 --> Optimize[Optimization Phase] Final2 --> Optimize Final3 --> Optimize Optimize --> Patterns[Add Advanced Patterns:
• Circuit Breaker
• Bulkhead
• Event Sourcing
• Saga] style Start fill:#e3f2fd style Optimize fill:#ffeb3b style Patterns fill:#c8e6c9

Best Practices for Implementation
#

Pattern Implementation Lifecycle
#

graph TD A[Assessment Phase] --> B[Design Phase] B --> C[Implementation Phase] C --> D[Testing Phase] D --> E[Deployment Phase] E --> F[Monitoring Phase] F --> G[Evolution Phase] G --> A A --> A1[Team Skills Assessment
Current Architecture Review
Business Requirements
Performance Needs] B --> B1[Pattern Selection
Architecture Design
Technology Stack
Migration Strategy] C --> C1[Code Implementation
Infrastructure Setup
Integration Development
Documentation] D --> D1[Unit Testing
Integration Testing
Performance Testing
Security Testing] E --> E1[Blue-Green Deployment
Feature Flags
Gradual Rollout
Rollback Planning] F --> F1[Performance Monitoring
Error Tracking
Business Metrics
Health Checks] G --> G1[Pattern Effectiveness
Scaling Needs
Technology Updates
Optimization] style A fill:#e3f2fd style B fill:#fff3e0 style C fill:#c8e6c9 style D fill:#ffcdd2 style E fill:#f3e5f5 style F fill:#ffeb3b style G fill:#e1bee7

Team Skill Requirements Matrix
#

graph TB subgraph "Junior Level (0-2 years)" J1[Layered Architecture ✓] J2[API Gateway ✓] J3[Circuit Breaker ✓] end subgraph "Intermediate Level (2-5 years)" I1[Hexagonal Architecture ✓] I2[CQRS ✓] I3[BFF ✓] I4[Serverless ✓] I5[Sidecar ✓] I6[Outbox ✓] end subgraph "Senior Level (5+ years)" S1[Onion Architecture ✓] S2[Event-Driven ✓] S3[Saga Pattern ✓] S4[Strangler Fig ✓] S5[Ambassador ✓] S6[Bulkhead ✓] end subgraph "Expert Level (8+ years)" E1[Event Sourcing ✓] E2[Complex Pattern Combinations ✓] E3[Enterprise Architecture ✓] end style J1 fill:#c8e6c9 style J2 fill:#c8e6c9 style J3 fill:#c8e6c9 style I1 fill:#fff3e0 style I2 fill:#fff3e0 style I3 fill:#fff3e0 style I4 fill:#fff3e0 style I5 fill:#fff3e0 style I6 fill:#fff3e0 style S1 fill:#ffcdd2 style S2 fill:#ffcdd2 style S3 fill:#ffcdd2 style S4 fill:#ffcdd2 style S5 fill:#ffcdd2 style S6 fill:#ffcdd2 style E1 fill:#f3e5f5 style E2 fill:#f3e5f5 style E3 fill:#f3e5f5

Start with architectural assessment
#

Begin with an honest assessment of your current system, team, and business requirements. Simple applications benefit from Layered Architecture, while complex enterprise systems require Onion or Hexagonal approaches.

Implement monitoring and observability
#

Wire up OpenTelemetry for distributed tracing, Application Insights for performance monitoring, and custom metrics for the signals your business actually cares about.

Observability Architecture
#

graph TB subgraph "Application Layer" APP1[Order Service] APP2[Payment Service] APP3[Inventory Service] end subgraph "Observability Stack" subgraph "Metrics Collection" PROM[Prometheus] GRAFANA[Grafana Dashboards] end subgraph "Distributed Tracing" JAEGER[Jaeger Tracing] ZIPKIN[Zipkin Alternative] end subgraph "Logging" ELK[ELK Stack] FLUENT[Fluentd/Fluent Bit] end subgraph "Azure Monitoring" AI[Application Insights] LAW[Log Analytics Workspace] ALERTS[Azure Alerts] end end subgraph "OpenTelemetry" OTEL[OpenTelemetry Collector] SDK[.NET SDK Integration] end APP1 --> SDK APP2 --> SDK APP3 --> SDK SDK --> OTEL OTEL --> PROM OTEL --> JAEGER OTEL --> ELK OTEL --> AI PROM --> GRAFANA AI --> LAW LAW --> ALERTS style OTEL fill:#ffeb3b style SDK fill:#c8e6c9 style AI fill:#e3f2fd style GRAFANA fill:#fff3e0

Telemetry Data Flow
#

sequenceDiagram participant App as .NET Application participant OTEL as OpenTelemetry SDK participant Collector as OTEL Collector participant Metrics as Metrics Store participant Traces as Trace Store participant Logs as Log Store participant Dashboard as Monitoring Dashboard App->>OTEL: Business Events App->>OTEL: Performance Metrics App->>OTEL: Distributed Traces App->>OTEL: Application Logs OTEL->>Collector: Batched Telemetry Collector->>Metrics: Prometheus Metrics Collector->>Traces: Jaeger Spans Collector->>Logs: Structured Logs Dashboard->>Metrics: Query Metrics Dashboard->>Traces: Query Traces Dashboard->>Logs: Query Logs Note over Dashboard: Unified Observability View
// OpenTelemetry Configuration
services.AddOpenTelemetry()
    .WithTracing(builder =>
    {
        builder.AddSource("Polly");
        builder.AddAspNetCoreInstrumentation();
    })
    .WithMetrics(builder =>
    {
        builder.AddMeter("Polly");
        builder.AddMeter("Microsoft.Extensions.Http.Resilience");
    });

Adopt gradual migration strategies
#

When modernizing existing systems, use the Strangler Fig Pattern to gradually replace components while maintaining operational continuity. Start with low-risk components and expand systematically.

Build team readiness
#

Advanced patterns require skilled teams. Invest in training for Domain-Driven Design, microservices principles, and cloud-native development practices before implementing complex patterns.

Key takeaways
#

  • Choose patterns from constraints, not ambition. If you can’t name the specific problem a pattern solves on your project, you don’t need it yet.
  • Start with Layered or Hexagonal and earn your way into the rest. Onion, Saga, and Event Sourcing pay off only once the domain complexity is already there.
  • Resilience patterns travel as a set. Circuit Breaker, Bulkhead, and Outbox each cover a different failure mode; adopt them together once you go distributed.
  • CQRS and Event Sourcing are a package deal in practice. Reach for them when reads and writes have genuinely different shapes, not to look modern.
  • Modernize legacy with Strangler Fig, one low-risk slice at a time, and keep the old system running until each slice proves out.
  • The costly mistakes are premature complexity and skipping the migration path. Both cost more than starting simple ever does.

The mindmap below is the same advice in one picture: start simple, design with diagrams, plan the evolution, and keep measuring whether the pattern actually helped.

mindmap root((Architecture Success)) Start Simple Layered for MVPs Iterative Enhancement Team Learning Think Visual Diagram First Document Flows Share Understanding Plan Evolution Migration Paths Pattern Combinations Scalability Roadmap Monitor Continuously Pattern Effectiveness Performance Impact Team Productivity

Where to start
#

Open the decision tree at the top of this page, answer the first question honestly about your application type, and follow it to two or three candidate patterns. Read only those sections. Then pick the simplest candidate and prototype it against a real slice of your workload this week.

The diagrams are here to argue the design with your team, and the code sketches are here to get you to a spike, not a framework. When you are ready to combine patterns, work top-down: the selection tree to shortlist, the architecture diagrams to structure the solution, the code contracts as a starting point, and the relationship maps when it is time to evolve.

Related

Implementing AutoMapper for DTO Mapping with Audit Details

··6 mins
Early versions of the Contact Management Application had the same four lines at the end of every create and update method: set CreatedBy, set CreatedOn (or their Updated* twins), then save. Copy-pasted audit code gets forgotten just often enough to make the audit columns untrustworthy, and an audit column you can’t trust is worse than none. Moving that logic into AutoMapper profiles fixed it in one place.

Validating Inputs with FluentValidation

··6 mins
Introduction # Validation is the one part of a request I refuse to trust anywhere but the edge. Domain objects assume they are already valid; rejecting a malformed email or a missing name belongs at the API boundary, before that data reaches a service or a repository.