.NET Architecture at Scale: Visual Guide to Modern Design Patterns
A working reference to 16 architecture patterns for .NET, with workflow diagrams, decision trees, and code sketches for each.
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.
Table of Contents
- Foundational Architecture Patterns
- Advanced Communication Patterns
- Service Integration Patterns
- Resilience and Data Patterns
- Modern .NET and Azure Integration
- Deployment and Infrastructure Patterns
- Pattern Selection Guide
- 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:
Complexity vs. Benefit Analysis
Pattern Comparison Matrix
| Pattern | Team Skill Level | Setup Time | Maintenance | Scalability | Testability | Use Case Fit |
|---|---|---|---|---|---|---|
| Layered | Beginner | Fast | Low | Medium | Medium | CRUD Apps |
| Hexagonal | Intermediate | Medium | Medium | High | Very High | Clean Architecture |
| Onion | Advanced | Slow | High | Very High | Very High | Enterprise DDD |
| CQRS | Intermediate | Medium | Medium | Very High | High | Read/Write Split |
| Event-Driven | Advanced | Slow | High | Very High | Medium | Real-time Systems |
| Saga | Expert | Very Slow | Very High | High | Medium | Distributed Transactions |
| API Gateway | Beginner | Fast | Low | Very High | Medium | Microservices |
| BFF | Intermediate | Medium | Medium | High | High | Multi-client Apps |
| Serverless | Intermediate | Fast | Low | Auto | Medium | Event Processing |
| Strangler Fig | Advanced | Variable | High | High | Medium | Legacy Migration |
| Circuit Breaker | Beginner | Fast | Low | Medium | High | Fault Tolerance |
| Outbox | Intermediate | Medium | Medium | High | Medium | Message Reliability |
| Sidecar | Intermediate | Medium | Medium | High | Medium | Cross-cutting Concerns |
| Ambassador | Advanced | Medium | High | High | Medium | Service Proxy |
| Bulkhead | Advanced | Slow | High | Very High | Medium | Resource Isolation |
| Event Sourcing | Expert | Very Slow | Very High | High | Medium | Audit Trail |
Quick Selection Guide
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 SourcingFoundational 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
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 ResponseWhen to Use Decision Tree
- 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 - Servicepublic 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 - Repositorypublic 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
Dependency Flow
Testing Strategy
Implementation Guidelines
// Core Contracts - Keep Minimalpublic 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 needspublic interface IProductRepository{ Task<Product> GetByIdAsync(int id); Task<IEnumerable<Product>> GetAllAsync(); Task<Product> SaveAsync(Product product);}
// Application Service - Orchestrates business logicpublic 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
Dependency Flow Rules
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<br/>remains isolatedProject 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 modelpublic 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 casespublic 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
Decision Flow for CQRS
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: ResponseImplementation Strategy
Core Contracts
// Keep contracts minimal and focusedpublic interface ICommand<TResult> : IRequest<TResult> { }public interface IQuery<TResult> : IRequest<TResult> { }
// Example commandpublic record CreateOrderCommand(string CustomerId, List<OrderItem> Items) : ICommand<int>;
// Example querypublic 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
Event Processing Patterns
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 contractpublic record OrderCreatedEvent( int OrderId, string CustomerId, decimal Amount, DateTime CreatedAt);
// Event handler interfacepublic 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 MediatRpublic 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 Handlerspublic 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 orderImplementation Example
// Saga State Machinepublic 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 Orchestratorpublic 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.
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.
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.
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.
Implementation Example
// Routing Facade Implementationpublic 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<br/>Requests Pass Through<br/>Monitor Failures Open: Circuit Tripped<br/>Fail Fast<br/>No Requests Allowed HalfOpen: Testing Recovery<br/>Limited Test Requests<br/>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 noteImplementation Example
// Modern .Net Implementationservices.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 v8var 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:<br/>At-least-once semanticsOutbox 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 : referencesImplementation Example
// Outbox Event Entitypublic 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 Implementationpublic 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 Processorpublic 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
.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 Integrationvar 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
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 PrometheusSidecar vs Traditional Architecture
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.ymlA minimal health-check sidecar in .NET:
// Health Check Sidecarpublic 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
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 endAmbassador vs Direct Integration
Implementation Example
// Ambassador Servicepublic 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 Ambassadorpublic 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
Failure Isolation Demonstration
Implementation Example
// Resource Pool Configurationpublic class BulkheadConfiguration{ public int CriticalServiceThreads { get; set; } = 10; public int StandardServiceThreads { get; set; } = 5; public int BackgroundTaskThreads { get; set; } = 3;}
// Bulkhead Implementationpublic 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
Event Sourcing vs Traditional Storage
Event Stream Timeline
timeline title Order-123 Event Timeline Jan 15, 9am : OrderCreated : Customer "John Doe" : Items Product-A, Product-B Jan 15, 915am : ItemAdded : Product "Product-C" : Quantity 2 Jan 15, 1030am : PaymentProcessed : Amount $150.00 : Method "Credit Card" Jan 16, 2pm : OrderShipped : Carrier "FedEx" : Tracking "123456789" Jan 18, 4pm : OrderDelivered : Status "Completed" : Signature "J.Doe"Implementation Example
// Domain Eventspublic 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 Storepublic 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 Sourcingpublic 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 Implementationpublic 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:
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
2. Foundation to Communication Flow
3. Integration and Resilience Patterns
4. Infrastructure and Data Patterns
5. Common Pattern Combinations
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:
- Modern Microservices Stack:
Hexagonal + CQRS + Event-Driven + API Gateway + Circuit Breaker - Enterprise Application:
Onion + CQRS + Outbox + BFF + Sidecar - Cloud-Native Solution:
Serverless + Event-Driven + Circuit Breaker + Ambassador - Legacy Modernization:
Strangler Fig + Hexagonal + Event-Driven + API Gateway
Architecture Decision Framework
Full Pattern Comparison Matrix
| Pattern | Complexity | Scalability | Maintainability | Team Skill | Time to Value | Primary Use Cases |
|---|---|---|---|---|---|---|
| 🏗️ Foundation Patterns | ||||||
| Layered | Low ⭐ | Medium ⭐⭐ | Medium ⭐⭐ | Junior+ 👥 | Fast ⚡ | CRUD apps, rapid prototyping |
| Hexagonal | Medium ⭐⭐ | High ⭐⭐⭐ | High ⭐⭐⭐ | Senior 👨💻 | Medium 🔄 | Complex business logic, testability |
| Onion | Medium ⭐⭐ | High ⭐⭐⭐ | High ⭐⭐⭐ | Senior 👨💻 | Medium 🔄 | DDD, enterprise applications |
| 💬 Communication Patterns | ||||||
| CQRS | Medium ⭐⭐ | Very High ⭐⭐⭐⭐ | Medium ⭐⭐ | Senior 👨💻 | Medium 🔄 | Read/write optimization |
| Event-Driven | High ⭐⭐⭐ | Very High ⭐⭐⭐⭐ | Medium ⭐⭐ | Senior 👨💻 | Slow 🐌 | Microservices, real-time |
| Saga | High ⭐⭐⭐ | High ⭐⭐⭐ | Medium ⭐⭐ | Senior 👨💻 | Slow 🐌 | Distributed transactions |
| 🔗 Integration Patterns | ||||||
| API Gateway | Medium ⭐⭐ | High ⭐⭐⭐ | Medium ⭐⭐ | Intermediate 👥 | Fast ⚡ | Service aggregation |
| BFF | Low ⭐ | Medium ⭐⭐ | Medium ⭐⭐ | Intermediate 👥 | Fast ⚡ | Client-specific APIs |
| Serverless | Low ⭐ | Very High ⭐⭐⭐⭐ | Medium ⭐⭐ | Intermediate 👥 | Fast ⚡ | Event-driven, auto-scale |
| Strangler Fig | Medium ⭐⭐ | Medium ⭐⭐ | High ⭐⭐⭐ | Senior 👨💻 | Slow 🐌 | Legacy modernization |
| 🛡️ Resilience Patterns | ||||||
| Circuit Breaker | Low ⭐ | High ⭐⭐⭐ | High ⭐⭐⭐ | Intermediate 👥 | Fast ⚡ | Fault tolerance |
| Bulkhead | Low ⭐ | High ⭐⭐⭐ | High ⭐⭐⭐ | Intermediate 👥 | Fast ⚡ | Resource isolation |
| Outbox | Medium ⭐⭐ | Medium ⭐⭐ | High ⭐⭐⭐ | Senior 👨💻 | Medium 🔄 | Reliable messaging |
| 🚀 Infrastructure Patterns | ||||||
| Sidecar | Medium ⭐⭐ | High ⭐⭐⭐ | High ⭐⭐⭐ | Intermediate 👥 | Medium 🔄 | Cross-cutting concerns |
| Ambassador | Medium ⭐⭐ | High ⭐⭐⭐ | High ⭐⭐⭐ | Intermediate 👥 | Medium 🔄 | External service proxy |
| 📊 Data & Event Patterns | ||||||
| Event Sourcing | High ⭐⭐⭐ | High ⭐⭐⭐ | Medium ⭐⭐ | Senior 👨💻 | Slow 🐌 | Audit trail, temporal queries |
Team Size and Project Type Recommendations
| 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
Pattern Migration Paths
Best Practices for Implementation
Pattern Implementation Lifecycle
Team Skill Requirements Matrix
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
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 Configurationservices.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.
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 ProductivityWhere 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.
Comments
Comments are GitHub discussions. Sign in with GitHub to post; reactions need no account.