Microservices Architecture: Types, Subtypes, and Design Patterns Matrix
A practical, real-world engineering guide to Microservices: exploring service types, taxonomy, core principles, and an exhaustive pattern-to-problem-solving matrix in table format.

Intro
When we start building a brand-new project, a monolith is almost always the smartest choice. You spin up a single ASP.NET Core project, connect it to a relational database, write clean domain entities, and ship features quickly. In the early days, everything is fast: debugging happens right inside Visual Studio, database queries are straightforward SQL joins, and deploying means publishing a single container or binary.
Fast forward three years.
Your engineering team has grown from 5 engineers to 60. Pull requests sit in queues because everyone is touching the same codebase. A memory leak in an obscure PDF report generation module suddenly takes down the entire customer checkout flow on a busy Friday afternoon. Deployments turn into stressful late-night release windows where a dozen teams hold their breath hoping nothing breaks.
That is usually the moment someone in the room says: βWe need to break this down into microservices.β
Microservices solve very real organizational, deployment, and scalability bottlenecks. But letβs be candid: microservices are not a silver bullet. The moment you split your monolith into separate services, you trade the simplicity of in-memory method calls for network latency, ACID transactions for eventual consistency, and straightforward debugging for distributed tracing.
In this guide, I want to walk through microservices from an engineerβs perspective:
- What microservices actually look like in real production systems
- The distinct types and subtypes of services you will build
- A comprehensive master table showing exactly which design pattern solves which distributed systems headache
- Concrete .NET Core code examples and architecture diagrams
- An interactive checklist you can use to assess your systemβs production readiness
What is Microservices Architecture?
At its core, Microservices Architecture is an approach where a large application is split into a collection of small, loosely coupled, independently deployable servicesβeach modeled around a distinct business domain.
Instead of having one giant application that handles Users, Orders, Payments, Shipping, and Notifications all inside the same process:
- Each service runs in its own isolated process or container.
- Each service owns its own private database (the fundamental Database-per-Service rule).
- Services talk to one another over lightweight network protocols (REST, gRPC, or Asynchronous Message Brokers like Kafka and RabbitMQ).
- Individual teams can build, test, deploy, and scale their service without waiting on or coordinating with other teams.
+-----------------------------------------------------------------------------+| Client Layer || (Mobile Apps, Web SPAs, Partner APIs, IoT) |+-----------------------------------------------------------------------------+ β βΌ+-----------------------------------------------------------------------------+| API Gateway / BFF Layer || (Routing, SSL, AuthN/AuthZ, Rate Limiting, YARP) |+-----------------------------------------------------------------------------+ β β β βΌ βΌ βΌ +ββββββββββββββββββββββββββ+ +βββββββββββββββββββββββββ+ +βββββββββββββββββββββββββ+ | Order Service | | Payment Service | | Inventory Service | | (ASP.NET Core / gRPC) | | (ASP.NET Core / REST) | | (ASP.NET Core / gRPC) | | ββββββββββββββββββββ | | βββββββββββββββββββ | | βββββββββββββββββββ | | β Private SQL DB β | | β Private SQL DB β | | β Private NoSQL β | | ββββββββββββββββββββ | | βββββββββββββββββββ | | βββββββββββββββββββ | +ββββββββββββββββββββββββββ+ +βββββββββββββββββββββββββ+ +βββββββββββββββββββββββββ+ β β β ββββββββββββββββββββββββ¬ββββββ΄ββββββββββββββββββββββββββ βΌ+-----------------------------------------------------------------------------+| Event Streaming / Message Broker || (Apache Kafka / RabbitMQ / Azure Event Hubs) |+-----------------------------------------------------------------------------+ β β βΌ βΌ +ββββββββββββββββββββββββββ+ +βββββββββββββββββββββββββ+ | Notification Service | | Analytics / CQRS | | (Background Worker) | | (Elasticsearch / DB) | +ββββββββββββββββββββββββββ+ +βββββββββββββββββββββββββ+Core Pillars of Microservices
If you want your distributed architecture to survive in production, there are six non-negotiable principles you need to embrace:
- Domain-Driven Bounded Contexts (DDD): Split your services along real business capabilities (e.g., Billing, Catalog, Fulfillment), never by technical layers (UI Service, Business Logic Service, Database Service).
- Decentralized Data Ownership: If Service A writes directly to Service Bβs database tables, you donβt have microservicesβyou have a distributed monolith with double the network latency. Every service must strictly encapsulate its own storage.
- Smart Endpoints, Dumb Pipes: Put all business logic, validation, and domain rules inside your services. Your message broker (Kafka, RabbitMQ) should be a dumb, high-speed pipe that only transports messages.
- Design for Failure from Day One: In a network of 20 services, something is always failing. A database is slow, a pod restarted, or a network switch dropped packets. Your code must use timeouts, retries with backoff, circuit breakers, and fallbacks.
- Independent Deployability: If releasing a new feature in the
Order Servicerequires you to simultaneously deploy thePayment ServiceandInventory Service, your boundaries are wrong. Services must be deployable independently at any time. - Pragmatic Polyglot Freedom: Choose the right tool for the job. You can build your high-throughput transactional APIs in .NET 10 / C#, your AI/ML recommendation pipelines in Python, and lightweight websocket handlers in Node.js.
Monolith vs Modular Monolith vs Microservices
Before jumping into microservices, look at where your application actually sits on the architectural spectrum:
| Evaluation Dimension | Traditional Monolith | Modular Monolith | Microservices Architecture |
|---|---|---|---|
| Codebase Layout | Single solution where everything can reference everything | Single solution with strictly enforced domain project boundaries | Multiple independent repositories or monorepo with isolated deployables |
| Database Model | One big shared database schema with heavy cross-table SQL joins | Single database, but each module strictly owns its isolated tables/schemas | Database per service: completely isolated database instances |
| Deployment Speed | Slow: any single line change forces testing and deploying the whole app | Moderate: still deployed as one unit, but domain logic is clean and decoupled | Fast & autonomous: ship changes to one service in 5 minutes with zero downtime |
| Scaling Granularity | Coarse: horizontally scaling spins up full copies of the entire server | Coarse: you scale the whole application together | Fine-grained: spin up 30 instances of PaymentService and leave Reports at 2 |
| Fault Blast Radius | High: an unhandled exception or memory leak crashes everything | Moderate: shared process, but cleaner memory boundaries | Low: if Recommendations goes down, users can still search and checkout |
| Team Structure | High coordination overhead, merge conflicts, shared release calendars | Good: teams can own specific modules within the shared repository | Autonomous: cross-functional squads (5β8 engineers) own a service end-to-end |
| Operational Overhead | Very low (simple CI/CD pipeline, minimal infrastructure) | Low to medium (straightforward infrastructure, clean code) | High (Kubernetes, distributed tracing, service mesh, API gateway, monitoring) |
| Transaction Model | ACID transactions (instant consistency with simple rollbacks) | ACID transactions (shared DB transaction across modules) | BASE & Eventual Consistency (Saga orchestrations, Outbox patterns) |
| Call Latency | Nanoseconds (in-memory method execution) | Nanoseconds (in-memory method execution) | Milliseconds (network serialization + HTTP/gRPC/Kafka transport) |
Types and Subtypes of Microservices
In enterprise systems, services naturally fall into specialized archetypes based on what they do, how they communicate, and how they store data:
Microservices Architecture Taxonomyβββ 1. Core / Domain Servicesβ βββ 1.1 Entity & Aggregate Servicesβ βββ 1.2 Domain Logic & Calculation Enginesβ βββ 1.3 Master Data & Catalog Servicesβββ 2. Composite & Orchestration Servicesβ βββ 2.1 API Aggregator / Scatter-Gather Servicesβ βββ 2.2 Chained Processing Pipelinesβ βββ 2.3 Workflow & Saga Orchestratorsβββ 3. Edge & Ingress Servicesβ βββ 3.1 API Gatewayβ βββ 3.2 Backend for Frontend (BFF)β βββ 3.3 Edge Cache & Content Routersβββ 4. Asynchronous Workers & Stream Processorsβ βββ 4.1 Event Stream Consumersβ βββ 4.2 Transactional Outbox & CDC Relaysβ βββ 4.3 Notification & Webhook Dispatchersβ βββ 4.4 Scheduled Batch & Cron Workersβββ 5. Infrastructure & Cross-Cutting Servicesβ βββ 5.1 Identity & Access Management (IAM / OAuth2)β βββ 5.2 Centralized Configuration & Secret Providersβ βββ 5.3 Service Registry & Discovery Directoryβ βββ 5.4 Centralized Audit & Telemetry Collectorsβββ 6. Read-Model & Analytical Services (CQRS / Data Mesh) βββ 6.1 CQRS Materialized Read Projections βββ 6.2 Full-Text Search Engines βββ 6.3 Real-Time Stream Analytics & Metrics1. Core / Domain Services
These are the heart of your system. They represent business entities and encapsulate core business rules.
- 1.1 Entity & Aggregate Services:
- What they do: Manage transactional state and business rules for a primary aggregate root (e.g.,
OrderService,PaymentService,CustomerService). - Tech stack: ASP.NET Core API, Entity Framework Core, PostgreSQL / SQL Server.
- Communication: Synchronous gRPC or REST for command execution; asynchronous domain events for publishing state changes.
- What they do: Manage transactional state and business rules for a primary aggregate root (e.g.,
- 1.2 Domain Logic & Calculation Engines:
- What they do: Compute complex algorithms without storing heavy mutable state (e.g.,
DynamicPricingEngine,TaxCalculationService,FraudDetectionService). - Characteristics: Stateless, compute-intensive, horizontally auto-scalable based on CPU/RAM load.
- What they do: Compute complex algorithms without storing heavy mutable state (e.g.,
- 1.3 Master Data & Reference Services:
- What they do: Serve slowly changing reference data used across the whole enterprise (e.g.,
ProductCatalogService,CurrencyExchangeService,ZipCodeLookupService). - Characteristics: Extremely read-heavy, paired with distributed caching (Redis).
- What they do: Serve slowly changing reference data used across the whole enterprise (e.g.,
2. Composite & Orchestration Services
When a single business operation spans multiple domain services, composite services coordinate the workflow.
- 2.1 API Aggregator / Scatter-Gather Services:
- What they do: Receive one request from a client, fan out parallel calls to 3 or 4 downstream domain services, merge the results into a single clean JSON payload, and return it.
- Example: A
CustomerDashboardAggregatorthat callsUserService(profile info),OrderService(recent 5 orders), andRewardsService(loyalty points balance) simultaneously.
- 2.2 Chained Processing Pipelines:
- What they do: Pass the output of Service A directly as input to Service B, then Service C in a strictly ordered sequence (e.g.,
OrderPipeline: Validate $\rightarrow$ Apply Discount $\rightarrow$ Hold Inventory $\rightarrow$ Charge Card).
- What they do: Pass the output of Service A directly as input to Service B, then Service C in a strictly ordered sequence (e.g.,
- 2.3 Workflow & Saga Orchestrators:
- What they do: Maintain state machines for complex distributed transactions, issuing commands to participating services and triggering compensating rollback actions if any step fails (e.g.,
CheckoutSagaOrchestrator).
- What they do: Maintain state machines for complex distributed transactions, issuing commands to participating services and triggering compensating rollback actions if any step fails (e.g.,
3. Edge & Ingress Services
These live at the perimeter of your network and act as the single front door for all client traffic.
- 3.1 API Gateway:
- What they do: Accept incoming requests from web and mobile clients, handle SSL termination, authenticate JWT tokens, enforce IP rate limits, and route requests to internal Kubernetes services (e.g., YARP, Ocelot, Envoy, Kong).
- 3.2 Backend for Frontend (BFF):
- What they do: Provide dedicated gateways tailored specifically to the needs of different client applications. Your
iOSMobileBFFmight return lightweight, compact JSON payloads to save battery and mobile bandwidth, while yourWebAdminBFFreturns rich data tables.
- What they do: Provide dedicated gateways tailored specifically to the needs of different client applications. Your
- 3.3 Edge Cache & Router:
- What they do: Terminate requests at cloud edge locations (Cloudflare, Azure Front Door, AWS CloudFront) to serve cached responses with sub-10ms latency.
4. Asynchronous Workers & Stream Processors
Headless background workers that do not expose public HTTP endpoints. They listen to message brokers and process jobs asynchronously.
- 4.1 Event Stream Consumers:
- What they do: Consume high-throughput message topics from Apache Kafka or RabbitMQ, update local read-models, or trigger downstream business processes.
- 4.2 Transactional Outbox & CDC Relays:
- What they do: Read the
Outboxtable or tail database transaction logs (using Debezium) to reliably publish domain events to the message broker without risking dual-write inconsistencies.
- What they do: Read the
- 4.3 Notification & Webhook Dispatchers:
- What they do: Handle integrations with third-party providers (Twilio for SMS, SendGrid for Email, Apple APNs for Push) with built-in retry queues and dead-letter handling.
- 4.4 Scheduled Batch & Cron Workers:
- What they do: Execute recurring business tasks such as nightly reconciliation runs, subscription renewal processing, and old data archival sweeps.
5. Infrastructure & Cross-Cutting Services
Platform-level utilities that provide security, configuration, and telemetry to all domain services.
- 5.1 Identity & Access Management (IAM / OAuth2 / OIDC):
- What they do: Centralize user login, issue signed JWT access tokens, handle refresh tokens, and manage role-based access control (e.g., Keycloak, Duende IdentityServer, Microsoft Entra ID).
- 5.2 Centralized Configuration & Secret Providers:
- What they do: Inject environment variables, feature flags, and encrypted connection strings into services at runtime without hardcoding them in git (e.g., Azure App Configuration + Key Vault, HashiCorp Vault).
- 5.3 Service Registry & Discovery Directory:
- What they do: Keep track of the dynamically changing IP addresses and ports of running microservice containers (e.g., Kubernetes CoreDNS, Consul).
- 5.4 Centralized Audit & Telemetry Collectors:
- What they do: Aggregate structured JSON logs, metrics, and distributed traces from all pods (e.g., OpenTelemetry Collector, Prometheus, Grafana Loki, Seq).
6. Read-Model & Analytical Services (CQRS / Data Mesh)
Services designed exclusively for high-speed queries, search, and business intelligence without locking write databases.
- 6.1 CQRS Materialized Read Projections:
- What they do: Listen to domain events and maintain pre-joined, denormalized read stores (Redis, MongoDB) so UI screens can fetch complete views in a single lookup.
- 6.2 Full-Text Search Engines:
- What they do: Index product catalogs, blog articles, and customer records into Elasticsearch or OpenSearch to provide autocomplete, fuzzy matching, and multi-faceted filtering.
- 6.3 Real-Time Stream Analytics & Metrics:
- What they do: Ingest continuous event streams to calculate real-time analytics, such as live dashboard metrics, active shopping cart counts, or fraud anomaly detection.
Master Matrix: Which Pattern Solves Which Issue
Here is your comprehensive cheat sheet mapping distributed systems challenges to their proven design patterns:
| Category | Pattern Name | Problem / Issue Solved | How It Works in Practice | Whatβs the Catch? (Tradeoffs) | Real-World Example |
|---|---|---|---|---|---|
| Decomposition | Decompose by Business Capability | Giant monolith where 50 engineers constantly collide in the same repository | Splits services along business departments (Orders, Billing, Shipping, Inventory) | Requires a stable business model; organizational reorgs can cause service reshuffling | E-Commerce splitting into Order, Billing, and Shipping services |
| Decomposition | Decompose by Subdomain (DDD) | Unclear boundaries, spaghetti dependencies, shared domain models | Uses Domain-Driven Design Bounded Contexts to isolate Core, Supporting, and Generic domains | Requires deep domain knowledge and event storming sessions | Healthcare app separating PatientCare (Core) from Billing (Generic) |
| Decomposition | Strangler Fig | High risk, long delays, and failure rate of βBig-Bangβ monolith rewrites | Places an API gateway in front of the monolith and gradually migrates endpoints one by one | Monolith and microservices must coexist for months; data synchronization required | Routing /api/orders to a new microservice while keeping /api/users on the monolith |
| Decomposition | Bulkhead Pattern | One misbehaving or slow dependency consuming all CPU/threads and crashing the whole server | Isolates resources (thread pools, memory, connection pools) into distinct compartments | Resource overhead of managing and tuning individual pool capacities | Giving PaymentService a dedicated thread pool isolated from ReportExport |
| Data Management | Database per Service | Database lock contention, schema coupling, services blocking each otherβs migrations | Each microservice exclusively owns its private database instance or schema | Direct SQL joins across services are impossible; requires eventual consistency | OrderService uses SQL Server; CatalogService uses MongoDB |
| Data Management | Shared Database (Tactical) | Need to break code into services before databases can be physically separated | Services share a physical database instance but strictly access isolated tables/schemas | High risk of accidental table joins and connection pool exhaustion | Transitional stepping stone during early Strangler Fig migration |
| Data Management | Saga (Choreography) | Multi-service distributed transactions without introducing a central coordinator | Services publish domain events; downstream services react and execute local transactions | Hard to visualize the overall workflow; risk of circular event dependencies | OrderCreated $\rightarrow$ PaymentCharged $\rightarrow$ StockReserved via Kafka topics |
| Data Management | Saga (Orchestration) | Complex multi-step distributed workflows requiring centralized tracking and rollbacks | A central orchestrator service directs participants via commands and executes compensations | Coordinator logic can become complex; must be clustered for high availability | MassTransit State Machine managing Order $\rightarrow$ Payment $\rightarrow$ Inventory $\rightarrow$ Shipping |
| Data Management | CQRS | Read vs write performance mismatch; complex UI queries slowing down transactional writes | Separates the Command model (write-optimized ACID) from the Query model (read-optimized) | Eventual consistency delay; code duplication between read and write models | Storing Orders in PostgreSQL (Write), projecting denormalized JSON to Redis (Read) |
| Data Management | Event Sourcing | Audit trail loss, inability to see historical state changes, write lock contention | State is stored as an immutable, append-only log of domain events rather than current state | High storage overhead; schema evolution for events requires careful versioning | Banking Ledger storing AccountOpened, MoneyDeposited, MoneyWithdrawn |
| Data Management | Transactional Outbox | βDual-writeβ bug (Database transaction succeeds, but message broker publish fails) | Saves the entity update + event record to an Outbox table in a single local ACID transaction | Requires a background worker or CDC process to poll and publish outbox records | EF Core saving Order and OutboxMessage in one SaveChangesAsync() |
| Data Management | Change Data Capture (CDC) | Polling outbox tables creates database query load and latency spikes | Tails the database transaction log directly (WAL) to extract and publish committed events | Requires database-specific infrastructure like Debezium and Kafka Connect | Debezium tailing PostgreSQL WAL and publishing events to Kafka |
| Data Management | API Composition | Need to aggregate data across multiple microservices to render a single UI screen | An aggregator service calls downstream services in parallel and merges the responses | Higher latency if downstream calls are slow; requires partial failure fallbacks | MobileBFF calling OrderService, PaymentService, and UserService in parallel |
| Communication | Synchronous REST / HTTP | Need a simple, human-readable, universal request-response protocol | Standard HTTP methods (GET, POST, PUT, DELETE) with JSON payloads | Blocking I/O overhead; higher latency and tight temporal coupling | Public REST API endpoints consumed by Angular and React frontends |
| Communication | Synchronous gRPC | High CPU overhead, bandwidth consumption, and JSON parsing latency in internal calls | Binary serialization using Protocol Buffers over HTTP/2 multiplexed connections | Harder to test with standard browser tools; requires shared .proto contracts | High-throughput internal RPC between OrderService and InventoryService |
| Communication | Asynchronous Pub/Sub | High temporal coupling; services crashing because downstream dependencies are down | Publisher emits an event to a topic; multiple subscribers process it independently | Messages may arrive out of order; all consumers must be strictly idempotent | OrderPlaced event published to Kafka; consumed by Email, Shipping, and Analytics |
| Communication | API Gateway | Clients having to manage hundreds of service endpoints, protocols, and auth tokens | Single entry reverse proxy managing routing, SSL, authentication, and rate limiting | Single point of failure if not load-balanced; slight latency overhead | YARP / Ocelot / Kong routing client traffic to internal Kubernetes services |
| Communication | Backend for Frontend (BFF) | One generic API response overloading mobile devices with unnecessary payload fields | Dedicated gateway instance tailored specifically for Mobile vs Web vs Desktop | Code duplication across BFFs if common logic is not shared cleanly | iOSMobileBFF returning trimmed 2KB JSON payloads while Web gets full 15KB data |
| Discovery | Client-Side Service Discovery | Microservice container IP addresses changing constantly during autoscaling | Client queries a service registry (Consul, Eureka) and load-balances calls itself | Discovery and load-balancing logic must be implemented in every client language | Netflix Eureka + Ribbon client-side load balancing |
| Discovery | Server-Side Service Discovery | Simplifies client code; clients shouldnβt need to know about internal service registries | Client calls a stable load balancer/router which queries DNS/registry behind the scenes | Extra network hop; load balancer infrastructure must be highly available | Kubernetes Service with CoreDNS and kube-proxy routing |
| Reliability | Circuit Breaker | Cascading failures caused by repeatedly hitting an unresponsive or dead service | Wraps calls in a state machine: Closed (Normal), Open (Fail-fast), Half-Open (Testing) | Picking correct failure thresholds and cooldown durations | Polly Circuit Breaker tripping after 5 consecutive HTTP 500 errors |
| Reliability | Retry with Exponential Backoff | Transient network hiccups and temporary database connection blips | Retries failed calls with increasing delay intervals plus randomized jitter | Non-idempotent endpoints can cause duplicate charges if retried carelessly | Retrying failed HTTP calls after 500ms, 1s, 2s with randomized jitter |
| Reliability | Timeout Pattern | Threads hanging indefinitely waiting for slow downstream responses | Enforces a strict maximum duration deadline on outgoing network calls | Requires graceful handling of OperationCanceledException across all layers | Setting a 2.5-second cancellation token deadline on downstream HTTP calls |
| Reliability | Fallback Pattern | UI crashing or displaying blank screens when non-essential services fail | Returns cached data, default values, or simplified views when calls fail | Stale data served to clients; must be designed intentionally per feature | Showing static cached recommendations when RecommendationService fails |
| Reliability | Rate Limiting & Throttling | Malicious attacks, DDOS, noisy neighbors, and resource saturation | Restricts incoming requests per client/token using Token Bucket or Leaky Bucket | Requires distributed caching (Redis) for cluster-wide rate limiting | ASP.NET Core RateLimiter limiting public API keys to 100 requests/minute |
| Observability | Distributed Tracing | Inability to track a requestβs path across dozens of asynchronous microservices | Propagates a Trace-Id and Span-Id via W3C TraceContext HTTP headers | High storage costs for full trace capture; requires trace sampling policies | OpenTelemetry + Jaeger visualizing end-to-end latency across 8 services |
| Observability | Log Aggregation | Logs scattered across hundreds of ephemeral containers and nodes | Log forwarders (FluentBit, Promtail) ship structured JSON logs to a central store | Storage and indexing costs; requires consistent structured JSON logging standards | Elastic Stack (ELK) / Grafana Loki querying logs by TraceId and OrderId |
| Observability | Health Check API | Kubernetes sending user traffic to uninitialized or crashed pods | Exposes /health/live (process running) and /health/ready (DB & dependencies ready) | Complex readiness checks can hammer databases if polled every 5 seconds | ASP.NET Core MapHealthChecks() used by Kubernetes liveness probes |
| Observability | Application Metrics | Flying blind; no real-time visibility into throughput, latency, or error spikes | Exposes numeric counters, gauges, and histograms scraped by Prometheus | Overhead of defining and maintaining business metric naming conventions | Prometheus scraping /metrics and alerting Grafana on P99 latency > 500ms |
| Deployment | Service Instance per Container | Environmental drift (βworks on my machineβ), resource contention, slow provisioning | Packages each microservice with its runtime and dependencies into a Docker image | Requires container orchestration (Kubernetes, ECS) and image registries | Deploying .NET Core microservices as isolated Pods on AKS / EKS |
| Deployment | Sidecar Pattern | Cross-cutting operational code polluting application business logic | Runs a helper container alongside the primary application container in the same Pod | Slight memory and CPU overhead per Pod instance | Dapr or Envoy sidecar intercepting traffic for mTLS and telemetry |
| Deployment | Service Mesh | Complex network management, mTLS encryption, and traffic routing across services | Dedicated infrastructure layer managing service-to-service communication via proxies | Steep learning curve and added latency of proxy hops | Istio / Linkerd providing zero-trust mTLS and canary traffic splitting |
| Security | Access Token & Claims Propagation | Passing authenticated user identity safely across distributed microservices | Identity Provider issues signed JWT containing claims; services validate signature | Token revocation requires short expiration times or centralized blacklists | API Gateway validates OAuth2 JWT; downstream services read user claims |
| Security | Externalized Configuration & Secrets | Storing database passwords and API keys in source code repositories | Injects configuration values and encrypted secrets at application startup | Requires secure credential management for the secret provider itself | Azure Key Vault + App Configuration injected into ASP.NET Core IConfiguration |
| Testing | Consumer-Driven Contract Testing | Breaking API changes deployed unintentionally, breaking upstream consumers | Consumers write contract tests; providers verify contracts in CI before deploying | Requires team discipline and a shared contract repository (Pact Broker) | Mobile team defines Pact contract; API team pipeline fails if JSON schema changes |
Deep-Dive: Decomposition Patterns
Decomposing a system is where most teams get stuck. If you draw your service boundaries incorrectly, you end up with the worst architectural pattern in software engineering: the distributed monolith (all the network latency and deployment coordination of microservices, with all the coupling of a monolith).
Decompose by Business Capability vs Subdomain (DDD)
graph TD subgraph Monolith["Legacy Monolithic Enterprise System"] M1[Order Module] M2[Customer Module] M3[Billing Module] M4[Warehouse Module] end
subgraph DDD["Decomposed by Bounded Contexts (DDD)"] S1["Order Bounded Context<br/>(Core Subdomain)"] S2["Customer Bounded Context<br/>(Supporting Subdomain)"] S3["Payment & Billing<br/>(Generic Subdomain)"] S4["Fulfillment & Inventory<br/>(Supporting Subdomain)"] end
Monolith -->|Decompose using Event Storming| DDD- Decompose by Business Capability: Look at what the company actually does (e.g., Order Processing, Inventory Warehousing, Invoicing).
- Decompose by Subdomain (Domain-Driven Design): Classify your domains by strategic value:
- Core Subdomain: The unique software differentiator that makes your company money (e.g., proprietary matching algorithm).
- Supporting Subdomain: Custom business features that support the core domain (e.g., Customer Loyalty Portal).
- Generic Subdomain: Standard business functions you could theoretically buy off the shelf (e.g., Authentication, Email Notifications, Invoicing).
Deep-Dive: Data Management & Distributed Transactions
In microservices, you cannot use SQL joins across services. You must manage distributed consistency.
1. The Saga Pattern (Orchestration vs Choreography)
When an e-commerce order is placed, three distinct services must commit transactions:
OrderService(Create order inPendingstate)PaymentService(Authorize and charge credit card)InventoryService(Reserve stock)
sequenceDiagram autonumber actor Customer participant Gateway as API Gateway participant Orchestrator as Order Saga Orchestrator participant OrderSvc as Order Service participant PaymentSvc as Payment Service participant InventorySvc as Inventory Service
Customer->>Gateway: POST /api/orders (Checkout) Gateway->>Orchestrator: Start OrderSaga Orchestrator->>OrderSvc: 1. CreatePendingOrder() OrderSvc-->>Orchestrator: OrderCreated (Pending)
Orchestrator->>PaymentSvc: 2. ProcessPayment() alt Payment Succeeded PaymentSvc-->>Orchestrator: PaymentCaptured Orchestrator->>InventorySvc: 3. ReserveInventory() alt Inventory Succeeded InventorySvc-->>Orchestrator: InventoryReserved Orchestrator->>OrderSvc: 4. CompleteOrder() OrderSvc-->>Orchestrator: OrderConfirmed Orchestrator-->>Gateway: 200 OK (Order Placed) else Out of Stock (Failure) InventorySvc-->>Orchestrator: OutOfStockError Note over Orchestrator,PaymentSvc: Compensating Transaction Triggered! Orchestrator->>PaymentSvc: 4a. RefundPayment() PaymentSvc-->>Orchestrator: PaymentRefunded Orchestrator->>OrderSvc: 4b. CancelOrder() OrderSvc-->>Orchestrator: OrderCancelled Orchestrator-->>Gateway: 400 Bad Request (Order Failed) end else Payment Declined (Failure) PaymentSvc-->>Orchestrator: PaymentFailed Orchestrator->>OrderSvc: CancelOrder() Orchestrator-->>Gateway: 402 Payment Required end2. The Transactional Outbox Pattern + Change Data Capture (CDC)
When updating a database and publishing an event, the Dual-Write Problem occurs if your database update succeeds but the network blips before publishing to Kafka.
The Transactional Outbox Pattern solves this by writing the entity update and the event record into the same database within a single local ACID transaction.
flowchart LR subgraph OrderServicePod["Order Microservice"] API[API Controller] -->|1. Local DB Transaction| DB[(Private SQL Database)] subgraph DB["Private Database"] T1[Orders Table] T2[Outbox Table] end end
subgraph Relay["Message Relay Engine"] Debezium["CDC Debezium / Background Worker"] end
subgraph Broker["Event Streaming Platform"] Kafka["Apache Kafka Topic: order.events"] end
subgraph Downstream["Downstream Services"] EmailSvc["Email Notification Service"] AnalyticsSvc["CQRS Read Model"] end
DB -->|2. Transaction Log / Polling| Debezium Debezium -->|3. Guaranteed At-Least-Once Delivery| Kafka Kafka -->|4. Consume & Process| EmailSvc Kafka -->|4. Project Read Model| AnalyticsSvcDeep-Dive: Reliability & Resiliency Patterns
In distributed systems, network partitions and service outages are expected events. Resiliency patterns ensure that failures remain isolated.
The Circuit Breaker State Machine
stateDiagram-v2 [*] --> Closed
Closed --> Open: Failure rate exceeds threshold<br/>(e.g., 5 consecutive 500 errors) note right of Closed Normal Operation: All requests pass through to downstream service. Success and failure counters tracked. end note
Open --> HalfOpen: Cooldown duration expires<br/>(e.g., after 30 seconds) note right of Open Failing Fast: Requests immediately return Fallback or 503. No network calls made downstream. end note
HalfOpen --> Closed: Trial requests succeed<br/>(Downstream is healthy) HalfOpen --> Open: Any trial request fails<br/>(Downstream still broken) note right of HalfOpen Testing Health: Limited trial requests allowed through. end notePractical .NET Core Code Examples
1. Transactional Outbox Pattern in ASP.NET Core & EF Core
Outbox Message Entity & DbContext Setup
using System.Text.Json;using Microsoft.EntityFrameworkCore;
public class OutboxMessage{ public Guid Id { get; set; } = Guid.NewGuid(); public string EventType { get; set; } = string.Empty; public string Payload { get; set; } = string.Empty; public DateTime CreatedOnUtc { get; set; } = DateTime.UtcNow; public DateTime? ProcessedOnUtc { get; set; } public string? ErrorMessage { get; set; }}
public class OrderDbContext : DbContext{ public OrderDbContext(DbContextOptions<OrderDbContext> options) : base(options) { }
public DbSet<Order> Orders => Set<Order>(); public DbSet<OutboxMessage> OutboxMessages => Set<OutboxMessage>();
public async Task SaveOrderWithOutboxAsync<TEvent>(Order order, TEvent domainEvent) { // 1. Add the domain entity Orders.Add(order);
// 2. Add the outbox record in the EXACT same local ACID transaction OutboxMessages.Add(new OutboxMessage { EventType = typeof(TEvent).Name, Payload = JsonSerializer.Serialize(domainEvent) });
// 3. Commit both atomically await SaveChangesAsync(); }}Outbox Background Publisher Worker
using Microsoft.Extensions.Hosting;using Microsoft.Extensions.Logging;using Microsoft.EntityFrameworkCore;
public class OutboxPublisherWorker : BackgroundService{ private readonly IServiceProvider _serviceProvider; private readonly IMessageProducer _messageProducer; private readonly ILogger<OutboxPublisherWorker> _logger;
public OutboxPublisherWorker( IServiceProvider serviceProvider, IMessageProducer messageProducer, ILogger<OutboxPublisherWorker> logger) { _serviceProvider = serviceProvider; _messageProducer = messageProducer; _logger = logger; }
protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { using var scope = _serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService<OrderDbContext>();
// Fetch unprocessed outbox messages in batches var pendingMessages = await dbContext.OutboxMessages .Where(m => m.ProcessedOnUtc == null) .OrderBy(m => m.CreatedOnUtc) .Take(20) .ToListAsync(stoppingToken);
foreach (var message in pendingMessages) { try { // Publish to Kafka / RabbitMQ await _messageProducer.PublishAsync(message.EventType, message.Payload, stoppingToken); message.ProcessedOnUtc = DateTime.UtcNow; } catch (Exception ex) { _logger.LogError(ex, "Failed to publish Outbox message {Id}", message.Id); message.ErrorMessage = ex.Message; } }
if (pendingMessages.Count > 0) { await dbContext.SaveChangesAsync(stoppingToken); } } catch (Exception ex) { _logger.LogError(ex, "Error processing outbox messages"); }
// Polling interval await Task.Delay(TimeSpan.FromSeconds(2), stoppingToken); } }}2. Resiliency Pipeline (Polly v8 in .NET Core)
Configure a resilient HTTP client combining Retry with Exponential Backoff + Circuit Breaker + Timeout:
using Polly;using Polly.CircuitBreaker;using Polly.Retry;using Polly.Timeout;
var builder = WebApplication.CreateBuilder(args);
// Register HttpClient with Polly Resilience Pipelinebuilder.Services.AddHttpClient("PaymentClient", client =>{ client.BaseAddress = new Uri("https://payment-service.internal/");}).AddResilienceHandler("custom-resilience-pipeline", pipelineBuilder =>{ // 1. Timeout Policy (Outer: Limit overall operation time) pipelineBuilder.AddTimeout(new TimeoutStrategyOptions { Timeout = TimeSpan.FromSeconds(5) });
// 2. Retry Policy with Exponential Backoff and Jitter pipelineBuilder.AddRetry(new RetryStrategyOptions<HttpResponseMessage> { MaxRetryAttempts = 3, BackoffType = DelayBackoffType.Exponential, UseJitter = true, Delay = TimeSpan.FromMilliseconds(500), ShouldHandle = new PredicateBuilder<HttpResponseMessage>() .Handle<HttpRequestException>() .Handle<TimeoutRejectedException>() .HandleResult(response => (int)response.StatusCode >= 500) });
// 3. Circuit Breaker Policy pipelineBuilder.AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions { FailureRatio = 0.5, // Break if 50% of requests fail SamplingDuration = TimeSpan.FromSeconds(10), MinimumThroughput = 8, BreakDuration = TimeSpan.FromSeconds(30), OnOpened = args => { Console.WriteLine($"[ALERT] Circuit Breaker opened for {args.BreakDuration.TotalSeconds}s!"); return ValueTask.CompletedTask; }, OnClosed = _ => { Console.WriteLine("[INFO] Circuit Breaker closed. Traffic restored."); return ValueTask.CompletedTask; } });});3. YARP (Yet Another Reverse Proxy) API Gateway Configuration
ASP.NET Coreβs production-grade reverse proxy configuration in appsettings.json:
{ "ReverseProxy": { "Routes": { "orders-route": { "ClusterId": "orders-cluster", "Match": { "Path": "/api/orders/{**catch-all}" }, "Transforms": [ { "PathPattern": "{**catch-all}" } ] }, "payments-route": { "ClusterId": "payments-cluster", "Match": { "Path": "/api/payments/{**catch-all}" } } }, "Clusters": { "orders-cluster": { "Destinations": { "order-instance-1": { "Address": "http://orders-svc-1:8080" }, "order-instance-2": { "Address": "http://orders-svc-2:8080" } }, "LoadBalancingPolicy": "RoundRobin" }, "payments-cluster": { "Destinations": { "payment-instance-1": { "Address": "http://payments-svc:8080" } } } } }}Interactive Architecture Checklist
Use this checklist to track pattern implementation across your microservices architecture:
Microservice Architecture Design Patterns Checklist
Decision Framework & Best Practices
When designing microservices, follow these core architectural rules:
- Start with a Modular Monolith First: Unless your team size exceeds 20-30 engineers or you have distinct scaling requirements, starting with a well-structured modular monolith is often the safest path.
- Never Share Databases Between Services: A shared database couples services, breaking independent deployability and invalidating microservice benefits.
- Embrace Asynchronous Communication by Default: Use asynchronous events (Kafka, RabbitMQ) for inter-service communication whenever immediate response data is not needed.
- Make Every Message Consumer Idempotent: In distributed systems with retries, duplicate messages will arrive. Ensure consumers handle duplicate deliveries safely using unique message IDs.
- Enforce Observability on Day One: Standardize OpenTelemetry traces (
traceparent), structured JSON logs, and Prometheus metrics across all services before going to production. - Prefer Saga Orchestration for Complex Business Workflows: For multi-step workflows with strict auditability and compensating actions, orchestration simplifies tracking and error recovery.




