Nitin Kumar SinghSolutions Architect

Type to search. to move, Enter to open.

    move open esc close

    .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

    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:

    Application type?Then askAnd that decidesScaling needs?Domain complexity?Communication style?Circuit breaker and bulkheadRead/write patterns?Transaction consistency?Service management?Simple CRUDComplex business logicMicroservicesLegacy modernizationEvent-driven or statelessLayered architectureAdd resilience patternsif scaling needs are highOnion architecturehighHexagonal architecturemediumAPI gatewaysynchronous; add BFF under highloadEvent-driven architectureasynchronousStrangler fig patternServerless patternResilience patternsCQRSdifferentRepository patternthe sameSaga patternrequiredCQRS and event sourcingeventualOutbox patternmessage reliabilitySidecar patterncross-cutting concernsAmbassador patternexternal communication
    The whole guide as one question and its consequences. Read a row left to right: the application type narrows to a follow-up question, and that question is what decides the pattern. Two rows end immediately, and those are the two cases where the shape of the problem already answers it.

    Complexity vs. Benefit Analysis

    Low complexityMedium complexityHigh complexityHigh payoff, low costEarn it with a requirementOnly with a hard constraintLayered architecture5 out of 5API gateway4 out of 5Circuit breaker4 out of 5Hexagonal architecture3 out of 5CQRS3 out of 5BFF3 out of 5Serverless3 out of 5Onion architecture2 out of 5Event sourcing2 out of 5Saga pattern2 out of 5Strangler fig2 out of 5
    Rated out of five for payoff against what they cost to run. The pattern is not that low complexity is better — it is that the high-complexity column has to be earned by a requirement, because none of them pays for itself on general principle.

    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

    What's your primary need?Start hereSimple CRUD applicationHigh performance and scaleClean, testable codeLegacy modernizationMulti-client supportFault toleranceLayered architectureCQRS and event-drivenHexagonal or onionStrangler figAPI gateway and BFFCircuit breaker and bulkhead
    Six needs, six answers. Nothing here is a ranking — the right column is what the rest of this guide argues for, given the constraint on the left.

    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

    1Presentation layercontrollers, views, APIs2Business logic layerservices, domain logic3Data access layerrepositories, ORM, DAL4Database layerSQL Server, Entity Framework
    Four layers, and the only rule that matters is that the arrows never point back up.

    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

    junior or mixedlow to mediumlimitedseniorConsider the advanced patternshighConsider hexagonal or onionhighPlan the migration path nowwhile it is still cheapConsidering layered architectureTeam experience?Project complexity?Use layeredFuture scalability?Layered is the right call
    Three questions, and two of them can end it early. Layered is the answer when the team is mixed and the complexity is moderate — which describes most applications, and is why this is the first pattern in the guide rather than the fallback.
    • 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

    Primary adaptersPorts and coreSecondary adaptersThings that drive itApplication coreThings it drivesWeb APIUIConsoleInbound interfacesthe ports it offersDomainbusiness logic, noinfrastructureOutbound interfacesthe ports it requiresDatabaseEmail serviceExternal APIs
    The core in the middle knows about neither side. Both columns of adapters depend inward on an interface the core owns, which is the whole of hexagonal architecture — everything else is naming.

    Dependency Flow

    Outside worldAdaptersPortsCoreUser interfaceAPI clientsDatabaseExternal servicesPrimary adaptersSecondary adaptersInput portsOutput portsBusiness logic
    The same architecture read as a dependency direction. Everything points inward: the outside world knows about adapters, adapters know about ports, and only the ports know about the core. Nothing in the core knows any of it exists.

    Testing Strategy

    Test levelWhat it coversUnit testsIntegration testsAcceptance testsDomain logicfast, isolatedUse casesadapters mockedAdapter testsreal infrastructurePort testscontract validationEnd to endthe full system
    Three levels, and the split follows the ports. Unit tests never touch an adapter; integration tests exist to prove the adapters honour the contract the core assumed.

    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

    Outer ringApplication layerDomain layerPresentation layerInfrastructure layerDepends on nothingWeb APIMVCBlazorData accessExternal servicesFile systemApplication servicesUse casesDTOsEntitiesDomain servicesDomain events
    Concentric, drawn flat. Presentation and infrastructure both point inward at the application layer, and only the application layer knows the domain — so the domain compiles without either of them, which is the test of whether the structure is real.

    Dependency Flow Rules

    LayerWhat it may depend onDomain layerApplication layerInfrastructure layerPresentation layerNothingpure business logic, domain eventsThe domain onlyuse cases, services, interfacedefinitionsThe interfaces above itexternal concerns, data access, APIsThe application layercontrollers, views
    Four layers, four rules, and they are all the same rule stated from a different position: depend inward. The domain layer is the one with nothing to its right.

    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 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

    InHandled byAgainstCommand sideQuery sideKept up to date by eventsCommandsQueriesCommand handlersQuery handlersWrite databaseDomain eventson the event busRead store updaterRead databasepublishes
    Two paths that never touch, joined by one event. Commands write; queries read; and the only thing crossing between them is the read-store updater, which is where every consistency question in CQRS actually lives.

    Decision Flow for CQRS

    yeshigheventual is finenoUse a simple repositorylowSimple CQRS on one databasea MediatR implementationstrongrequiredCQRS and the saga patterndistributed transactionsConsidering CQRSRead and write patterns different?Scalability requirements?Consistency requirements?CQRS and event sourcingseparate read and write stores
    Three questions, and the first one stops most proposals. CQRS is not a scaling decision until the read and write shapes actually differ — before that it is two handlers and a lot of ceremony.

    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

    PhaseWhat it involvesSeparate handlersphase 1Separate modelsphase 2Separate storesphase 3MediatR, command and queryhandlersstill one databaseWrite models, read models, eventpublishingWrite database, read database,event sourcing
    Three phases, and only the third one costs you a second database. Most teams get the value they wanted at phase two and stop, which is a legitimate place to stop.

    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

    Event producersEvent infrastructureEvent consumersOrder servicePayment serviceInventory serviceEvent busAzure Service Bus or RabbitMQEmail serviceAnalytics serviceAudit service
    Producers and consumers never name each other. That is the property being bought — and the cost of it is that no single service can tell you whether the whole thing worked.

    Event Processing Patterns

    StrategyHow it worksWhat it costsChoreographyOrchestrationDistributed logicservices react to events; no centralcoordinatorCentral coordinatorworkflow management, explicitprocess controlLoose couplingand it is hard to debugA clear flowand a central point of failure
    Choreography and orchestration are the same trade made twice. One buys loose coupling and pays in debuggability; the other buys a readable flow and pays with a component that can take the whole process down.

    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.

    ClientsThe gatewayServicesCross-cutting concernsMobile clientWeb clientThird partyAPI gatewayRate limitingAuthenticationCachingMonitoringAuth serviceProduct serviceOrder servicePayment service
    One address for three kinds of caller, and the cross-cutting column is why it exists. Rate limiting and authentication are implemented once at the door rather than four times behind it.

    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.

    ClientsBFF layerBackend servicesOne per client, shaped for itMobile appWeb appThird party APIMobile BFFproducts, orders, notificationsWeb BFFproducts, orders, usersPartner API BFFproducts, ordersProduct serviceOrder serviceUser serviceNotification service
    One BFF per client, and none of them calls the same set. The mobile BFF wants notifications and no user service; the partner API wants neither — which is the argument for three thin layers instead of one shared one that has to satisfy all three.

    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.

    Event sourcesAzure FunctionsOutputsHTTP triggersTimer triggersQueue triggersBlob triggersOrder processingImage resizingEmail sendingData cleanupDatabaseBlob storageService BusCosmos DB
    Nothing in the middle column runs unless something in the left column fires. That is the whole model — and it is also why the right column matters more than usual, since a function with no output is a function nobody can prove ran.

    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.

    Phase 1Phase 2Phase 3Phase 4Initial stateFacade introducedGradual migrationCompleteClientLegacy systemFacade / routerLegacy systemModern service Afirst sliceFacade / routerLegacy systemshrinkingModern service AModern service BModern service AModern service BModern service C
    Four phases, one client. The facade appears in phase two and disappears in phase four, and everything the pattern is worth happens in phase three — where legacy and modern are both live behind the same router and either can be rolled back.

    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<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 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:<br/>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

    Container AppsDapr runtimeAzure infrastructureOne environmentBuilding blocksOutside AzureOrder servicePayment serviceInventory serviceService discoveryState managementPub/subSecret managementAzure Service Busmessage brokerCosmos DBstate storeKey Vaultsecret storageContainer Registryimage storageApplication InsightsmonitoringExternal APIsLegacy systemstelemetry
    Dapr sits between the services and the infrastructure so the services do not name any of it. A container app asks for state; whether that is Cosmos DB is a configuration decision made outside the code.

    .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

    Pod or container groupSidecarsExternal servicesMain applicationSharedSame lifecycle, separate processOrder service.NET APIShared volumeslogs, config, tempLoggingFluentdMonitoringPrometheus exporterSecurityservice mesh proxyConfigurationconfig syncELK stacklog aggregationPrometheusmetrics collectionHashiCorp Vaultsecret managementConsulservice discoveryvia the volume
    Four sidecars beside one application, sharing a volume with it and nothing else. The application writes a log file; the logging sidecar is what knows Fluentd exists — so swapping the aggregator never touches the service.

    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

    ApproachWhich meansAnd ends asTraditional monolithicSidecar patternApplication and infrastructurecodetightly coupledClean applicationbusiness logic onlyCross-cutting concernsmixed into the business logicInfrastructure sidecarsloosely coupled, separate processesTechnology lock-inhard to changeTechnology independenceeasy to swap
    The same three consequences, read twice. What the sidecar column buys is not less code — it is that the second box is a different process, so the third box stops being a lock-in.

    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

    Application clusterAmbassadorsExternal servicesConsumer applicationOne per dependencyOrder servicemain business logicPayment ambassadorenhanced connectivityInventory ambassadorretry and circuit breakerNotification ambassadorrate limitingPayment servicethird-party APIInventory servicelegacy systemNotification serviceemail provider
    An ambassador per outbound dependency, each one holding the connectivity concern that dependency actually needs. Retry belongs with the legacy inventory system; rate limiting belongs with the email provider; the order service holds neither.

    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

    ApproachWhat you end up withDirect integrationAmbassador patternApplication codeplus auth, retry, monitoring andcircuit breakingClean applicationbusiness logic onlyAmbassador serviceconnectivity logicA complex codebasehard to test, coupled to thetechnologySpecialised teamseasy testing, technology abstraction
    Direct integration puts five concerns in one box; the ambassador puts four of them in a second process. The gain is testability — a clean application can be tested without a payment provider on the other end of it.

    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

    Application layerBulkhead poolsWhat runs thereCriticalStandardBackgroundAPI controllerThreads: 10connections 5, memory 2 GBThreads: 5connections 3, memory 1 GBThreads: 3connections 2, memory 512 MBCritical servicespayment, authenticationStandard servicesproduct catalog, searchBackground tasksanalytics, cleanup
    Three pools, sized deliberately. Analytics cannot exhaust the threads payment needs, because it was never allowed to borrow them — which is the entire mechanism, and the reason the numbers matter more than the boxes.

    Failure Isolation Demonstration

    PoolWhat that meansWithout bulkheadWith bulkheadShared thread pool20 threads, for everythingCritical pool10 threadsStandard pool5 threadsBackground pool3 threadsAll services competeand one failure affects all ofthemPayment serviceisolated and protectedCatalog servicelimited impactAnalytics servicefailure contained
    One shared pool means one failure. Three sized pools mean three blast radii, and the top row is the only one where a failing analytics query can take payment down with it.

    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

    Command sideEvent storeDerived from itIn orderAppend onlyRead side (CQRS)Temporal queriesCommandsAggregate rootEvent storeStream Order-123OrderCreated v1, ItemAdded v2,PaymentProcessed v3,OrderShipped v4Stream Order-456Stream Order-789Event projectionsOrder summaryAnalyticsAudit logTime-travel queriesstate at any past point, ornow
    The event store is the only writer of record, and everything to its right is derived. Read models can be rebuilt, deleted and rebuilt differently; the stream they were built from cannot be edited, which is the whole bargain.

    Event Sourcing vs Traditional Storage

    Storage modelWhich givesBecause writes areTraditional CRUDEvent sourcingCurrent state onlyorder status: shippedEvent streamevery change recordedLost historyno audit trailComplete historya full audit trailUpdates that overwritethe previous data is goneAppend onlyimmutable eventsTemporal queriesstate at any point
    CRUD keeps the answer; event sourcing keeps the question and every answer since. The cost is the fourth box — you now have a stream to replay rather than a row to read.

    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 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:

    Start withThenThenAnd finallyEnterpriseMicroservicesLegacy migrationOnion architectureAPI gatewayStrangler figCQRSBFFEvent-drivenEvent sourcingCircuit breakerOutbox patternSaga patternBulkhead
    Three combinations that hold together, read left to right. Each row is an order of adoption, not a shopping list — the saga pattern only makes sense once events are the source of truth, and the bulkhead only once something is in front of it to protect.

    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

    a statedecisionData and eventsstate management — event sourcingFoundationcore architecture — layered, hexagonal, onionCommunicationdata and messages — CQRS, event-driven, sagaIntegrationservice connection — API gateway, BFF, serverless,strangler figResiliencefault tolerance — circuit breaker, bulkhead, outboxInfrastructuredeployment support — sidecar, ambassador
    Six categories in the order a system usually needs them. Foundation comes first because everything else assumes one; data and events branch off communication rather than following it, because event sourcing is a choice about state, not about how services talk.

    2. Foundation to Communication Flow

    Foundation patternsCommunication patternsLayeredsimple 3-tier — junior level, lowcomplexityHexagonalports and adapters — senior, mediumOniondomain-centric — senior, mediumCQRSseparate read and write — senior,mediumEvent-drivenasync messaging — senior, highSagadistributed transactions — senior,highenables clean testingdomain-driven designoften combinedorchestrates workflows
    Foundation to communication. Layered has no arrow leaving it, and that is the point: the communication patterns all assume a boundary that hexagonal or onion has already drawn.

    3. Integration and Resilience Patterns

    Integration patternsResilience patternsStrangler figlegacy modernization — senior,mediumAPI gatewaysingle entry point — intermediate,mediumBFFbackend for frontend —intermediate, lowServerlessevent-driven functions —intermediate, lowCircuit breakerfail fast — intermediate, lowBulkheadresource isolation — intermediate,lowOutboxreliable messaging — senior, mediumprotection layerresource protection
    Integration and resilience, and the arrow between them is the one that matters: the gateway is where a circuit breaker goes, because it is the only place that sees every call.

    4. Infrastructure and Data Patterns

    PatternWhat it gives youInfrastructureData and eventsSidecarcross-cutting concerns —intermediate, mediumAmbassadorexternal service proxy —intermediate, mediumEvent sourcingcomplete audit trail — senior, highEnhanced monitoringService meshTemporal queries
    Three patterns, three capabilities they hand you. None of these is an architecture — they are things you bolt to one, which is why they sit last in the guide.

    5. Common Pattern Combinations

    FirstThenThenAndEnterpriseMicroservicesCloud-nativeLegacy modernizationOnion architectureHexagonal architectureServerlessStrangler figCQRSEvent-drivenBFFAPI gatewayEvent sourcingAPI gatewayAmbassadorEvent-drivenOutbox patternCircuit breakerSidecarCircuit breaker
    Four stacks that hold together, each read left to right as an order of adoption. Two of them start with a foundation pattern and two do not, which is the difference between building a system and changing one.

    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

    Team sizeProject typeWhich recommendsSmall team1 to 3 developers — favoursimplicityMedium team4 to 8 developers — favourstructureLarge team9 or more — favour boundariesMVP or prototypespeed firstEnterprise applicationquality firstMicroservicesscale firstLegacy migrationsafety firstLayered and serverlessthen API gateway and circuitbreakerHexagonal or onion, with CQRSthen BFF, outbox and sidecarEvent-driven and API gatewaythen circuit breaker, ambassador,sagaStrangler fig and hexagonalthen event-driven and API gatewaywith guidance
    Team size does not pick the architecture; it picks which project types are safe to attempt. A small team can build an enterprise application, with guidance — and the recommendation only appears once the project type is settled.

    | 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

    LatencyScalabilityUnder 50ms50 to 200msOver 200msOver 1000 TPS100 to 1000 TPSUnder 100 TPSLayered architectureRepository patternCircuit breakerHexagonal architectureAPI gatewayCQRSBFFEvent-driven architectureSaga patternEvent sourcingMicroservices meshCQRS with event sourcingServerless functionsEvent-driven with bulkheadAPI gateway with BFFHexagonal with repositoryLayered with cachingSimple layeredMonolithic architecture
    Latency on the left, throughput on the right, and the two do not line up. Event sourcing is in the slowest latency band and the highest throughput band at the same time — which is the trade being made, not a contradiction.

    Pattern Migration Paths

    Where you areFirst moveThenArriving atCurrent architecture?Then, and only thenMonolithic systemLayered architectureLegacy systemAdd an API gatewayImplement CQRSStrangler fig patternExtract microservicesAdd event-drivenModern serviceimplementationModern microservicesEvent-driven architectureModernized systemAdvanced patternscircuit breaker, bulkhead,event sourcing, saga
    Three starting points, three different first moves, and one shared destination. The advanced patterns are deliberately last: none of them is a migration step, they are what you add once the migration has somewhere stable to stand.

    Best Practices for Implementation

    Pattern Implementation Lifecycle

    assess again, with evidenceAssessmentteam skills, current architecture, businessrequirements, performance needsDesignpattern selection, architecture, technologystack, migration strategyImplementationcode, infrastructure, integration,documentationTestingunit, integration, performance, securityDeploymentblue-green, feature flags, gradual rollout,rollback planMonitoringperformance, errors, business metrics, healthchecksEvolutionpattern effectiveness, scaling needs,technology updates
    Seven phases, and the seventh returns to the first. Evolution is not a tidy-up at the end — it is the phase that decides whether the pattern you chose is still the right one, which means the assessment happens again with evidence this time.

    Team Skill Requirements Matrix

    JuniorIntermediateSeniorExpert0 to 2 years2 to 5 years5 years and up8 years and upLayered architectureAPI gatewayCircuit breakerHexagonal architectureCQRSBFFServerlessSidecarOutboxOnion architectureEvent-drivenSaga patternStrangler figAmbassadorBulkheadEvent sourcingComplex pattern combinationsEnterprise architecture
    What a team can build is a function of what it has already built. Nothing in the expert column is a different technology — it is the same patterns combined, which is the part that takes the years.

    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

    ServicesOpenTelemetryWhere it landsEmit onceMetricsTracingLoggingAzureOrder servicePayment serviceInventory service.NET SDK integrationOpenTelemetry collectorPrometheusGrafana dashboardsJaegeror ZipkinELK stackvia Fluentd or Fluent BitApplication InsightsLog Analytics workspaceAzure alerts
    One SDK, one collector, four destinations. The services emit once and never name Prometheus, Jaeger or Application Insights — which is the point of putting OpenTelemetry in the middle rather than four client libraries in each service.

    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.

    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.

    Comments

    Comments are GitHub discussions. Sign in with GitHub to post; reactions need no account.