Manikandan β€” Manikandan
Updated on23 min readManikandanArchitecture

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.

Microservices Architecture: Types, Subtypes, and Design Patterns Matrix - Manikandan

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:

  1. What microservices actually look like in real production systems
  2. The distinct types and subtypes of services you will build
  3. A comprehensive master table showing exactly which design pattern solves which distributed systems headache
  4. Concrete .NET Core code examples and architecture diagrams
  5. 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:

  1. 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).
  2. 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.
  3. 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.
  4. 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.
  5. Independent Deployability: If releasing a new feature in the Order Service requires you to simultaneously deploy the Payment Service and Inventory Service, your boundaries are wrong. Services must be deployable independently at any time.
  6. 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 DimensionTraditional MonolithModular MonolithMicroservices Architecture
Codebase LayoutSingle solution where everything can reference everythingSingle solution with strictly enforced domain project boundariesMultiple independent repositories or monorepo with isolated deployables
Database ModelOne big shared database schema with heavy cross-table SQL joinsSingle database, but each module strictly owns its isolated tables/schemasDatabase per service: completely isolated database instances
Deployment SpeedSlow: any single line change forces testing and deploying the whole appModerate: still deployed as one unit, but domain logic is clean and decoupledFast & autonomous: ship changes to one service in 5 minutes with zero downtime
Scaling GranularityCoarse: horizontally scaling spins up full copies of the entire serverCoarse: you scale the whole application togetherFine-grained: spin up 30 instances of PaymentService and leave Reports at 2
Fault Blast RadiusHigh: an unhandled exception or memory leak crashes everythingModerate: shared process, but cleaner memory boundariesLow: if Recommendations goes down, users can still search and checkout
Team StructureHigh coordination overhead, merge conflicts, shared release calendarsGood: teams can own specific modules within the shared repositoryAutonomous: cross-functional squads (5–8 engineers) own a service end-to-end
Operational OverheadVery low (simple CI/CD pipeline, minimal infrastructure)Low to medium (straightforward infrastructure, clean code)High (Kubernetes, distributed tracing, service mesh, API gateway, monitoring)
Transaction ModelACID transactions (instant consistency with simple rollbacks)ACID transactions (shared DB transaction across modules)BASE & Eventual Consistency (Saga orchestrations, Outbox patterns)
Call LatencyNanoseconds (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 & Metrics

1. 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.
  • 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.
  • 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).

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 CustomerDashboardAggregator that calls UserService (profile info), OrderService (recent 5 orders), and RewardsService (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).
  • 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).

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 iOSMobileBFF might return lightweight, compact JSON payloads to save battery and mobile bandwidth, while your WebAdminBFF returns rich data tables.
  • 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 Outbox table or tail database transaction logs (using Debezium) to reliably publish domain events to the message broker without risking dual-write inconsistencies.
  • 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:

CategoryPattern NameProblem / Issue SolvedHow It Works in PracticeWhat’s the Catch? (Tradeoffs)Real-World Example
DecompositionDecompose by Business CapabilityGiant monolith where 50 engineers constantly collide in the same repositorySplits services along business departments (Orders, Billing, Shipping, Inventory)Requires a stable business model; organizational reorgs can cause service reshufflingE-Commerce splitting into Order, Billing, and Shipping services
DecompositionDecompose by Subdomain (DDD)Unclear boundaries, spaghetti dependencies, shared domain modelsUses Domain-Driven Design Bounded Contexts to isolate Core, Supporting, and Generic domainsRequires deep domain knowledge and event storming sessionsHealthcare app separating PatientCare (Core) from Billing (Generic)
DecompositionStrangler FigHigh risk, long delays, and failure rate of β€œBig-Bang” monolith rewritesPlaces an API gateway in front of the monolith and gradually migrates endpoints one by oneMonolith and microservices must coexist for months; data synchronization requiredRouting /api/orders to a new microservice while keeping /api/users on the monolith
DecompositionBulkhead PatternOne misbehaving or slow dependency consuming all CPU/threads and crashing the whole serverIsolates resources (thread pools, memory, connection pools) into distinct compartmentsResource overhead of managing and tuning individual pool capacitiesGiving PaymentService a dedicated thread pool isolated from ReportExport
Data ManagementDatabase per ServiceDatabase lock contention, schema coupling, services blocking each other’s migrationsEach microservice exclusively owns its private database instance or schemaDirect SQL joins across services are impossible; requires eventual consistencyOrderService uses SQL Server; CatalogService uses MongoDB
Data ManagementShared Database (Tactical)Need to break code into services before databases can be physically separatedServices share a physical database instance but strictly access isolated tables/schemasHigh risk of accidental table joins and connection pool exhaustionTransitional stepping stone during early Strangler Fig migration
Data ManagementSaga (Choreography)Multi-service distributed transactions without introducing a central coordinatorServices publish domain events; downstream services react and execute local transactionsHard to visualize the overall workflow; risk of circular event dependenciesOrderCreated $\rightarrow$ PaymentCharged $\rightarrow$ StockReserved via Kafka topics
Data ManagementSaga (Orchestration)Complex multi-step distributed workflows requiring centralized tracking and rollbacksA central orchestrator service directs participants via commands and executes compensationsCoordinator logic can become complex; must be clustered for high availabilityMassTransit State Machine managing Order $\rightarrow$ Payment $\rightarrow$ Inventory $\rightarrow$ Shipping
Data ManagementCQRSRead vs write performance mismatch; complex UI queries slowing down transactional writesSeparates the Command model (write-optimized ACID) from the Query model (read-optimized)Eventual consistency delay; code duplication between read and write modelsStoring Orders in PostgreSQL (Write), projecting denormalized JSON to Redis (Read)
Data ManagementEvent SourcingAudit trail loss, inability to see historical state changes, write lock contentionState is stored as an immutable, append-only log of domain events rather than current stateHigh storage overhead; schema evolution for events requires careful versioningBanking Ledger storing AccountOpened, MoneyDeposited, MoneyWithdrawn
Data ManagementTransactional 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 transactionRequires a background worker or CDC process to poll and publish outbox recordsEF Core saving Order and OutboxMessage in one SaveChangesAsync()
Data ManagementChange Data Capture (CDC)Polling outbox tables creates database query load and latency spikesTails the database transaction log directly (WAL) to extract and publish committed eventsRequires database-specific infrastructure like Debezium and Kafka ConnectDebezium tailing PostgreSQL WAL and publishing events to Kafka
Data ManagementAPI CompositionNeed to aggregate data across multiple microservices to render a single UI screenAn aggregator service calls downstream services in parallel and merges the responsesHigher latency if downstream calls are slow; requires partial failure fallbacksMobileBFF calling OrderService, PaymentService, and UserService in parallel
CommunicationSynchronous REST / HTTPNeed a simple, human-readable, universal request-response protocolStandard HTTP methods (GET, POST, PUT, DELETE) with JSON payloadsBlocking I/O overhead; higher latency and tight temporal couplingPublic REST API endpoints consumed by Angular and React frontends
CommunicationSynchronous gRPCHigh CPU overhead, bandwidth consumption, and JSON parsing latency in internal callsBinary serialization using Protocol Buffers over HTTP/2 multiplexed connectionsHarder to test with standard browser tools; requires shared .proto contractsHigh-throughput internal RPC between OrderService and InventoryService
CommunicationAsynchronous Pub/SubHigh temporal coupling; services crashing because downstream dependencies are downPublisher emits an event to a topic; multiple subscribers process it independentlyMessages may arrive out of order; all consumers must be strictly idempotentOrderPlaced event published to Kafka; consumed by Email, Shipping, and Analytics
CommunicationAPI GatewayClients having to manage hundreds of service endpoints, protocols, and auth tokensSingle entry reverse proxy managing routing, SSL, authentication, and rate limitingSingle point of failure if not load-balanced; slight latency overheadYARP / Ocelot / Kong routing client traffic to internal Kubernetes services
CommunicationBackend for Frontend (BFF)One generic API response overloading mobile devices with unnecessary payload fieldsDedicated gateway instance tailored specifically for Mobile vs Web vs DesktopCode duplication across BFFs if common logic is not shared cleanlyiOSMobileBFF returning trimmed 2KB JSON payloads while Web gets full 15KB data
DiscoveryClient-Side Service DiscoveryMicroservice container IP addresses changing constantly during autoscalingClient queries a service registry (Consul, Eureka) and load-balances calls itselfDiscovery and load-balancing logic must be implemented in every client languageNetflix Eureka + Ribbon client-side load balancing
DiscoveryServer-Side Service DiscoverySimplifies client code; clients shouldn’t need to know about internal service registriesClient calls a stable load balancer/router which queries DNS/registry behind the scenesExtra network hop; load balancer infrastructure must be highly availableKubernetes Service with CoreDNS and kube-proxy routing
ReliabilityCircuit BreakerCascading failures caused by repeatedly hitting an unresponsive or dead serviceWraps calls in a state machine: Closed (Normal), Open (Fail-fast), Half-Open (Testing)Picking correct failure thresholds and cooldown durationsPolly Circuit Breaker tripping after 5 consecutive HTTP 500 errors
ReliabilityRetry with Exponential BackoffTransient network hiccups and temporary database connection blipsRetries failed calls with increasing delay intervals plus randomized jitterNon-idempotent endpoints can cause duplicate charges if retried carelesslyRetrying failed HTTP calls after 500ms, 1s, 2s with randomized jitter
ReliabilityTimeout PatternThreads hanging indefinitely waiting for slow downstream responsesEnforces a strict maximum duration deadline on outgoing network callsRequires graceful handling of OperationCanceledException across all layersSetting a 2.5-second cancellation token deadline on downstream HTTP calls
ReliabilityFallback PatternUI crashing or displaying blank screens when non-essential services failReturns cached data, default values, or simplified views when calls failStale data served to clients; must be designed intentionally per featureShowing static cached recommendations when RecommendationService fails
ReliabilityRate Limiting & ThrottlingMalicious attacks, DDOS, noisy neighbors, and resource saturationRestricts incoming requests per client/token using Token Bucket or Leaky BucketRequires distributed caching (Redis) for cluster-wide rate limitingASP.NET Core RateLimiter limiting public API keys to 100 requests/minute
ObservabilityDistributed TracingInability to track a request’s path across dozens of asynchronous microservicesPropagates a Trace-Id and Span-Id via W3C TraceContext HTTP headersHigh storage costs for full trace capture; requires trace sampling policiesOpenTelemetry + Jaeger visualizing end-to-end latency across 8 services
ObservabilityLog AggregationLogs scattered across hundreds of ephemeral containers and nodesLog forwarders (FluentBit, Promtail) ship structured JSON logs to a central storeStorage and indexing costs; requires consistent structured JSON logging standardsElastic Stack (ELK) / Grafana Loki querying logs by TraceId and OrderId
ObservabilityHealth Check APIKubernetes sending user traffic to uninitialized or crashed podsExposes /health/live (process running) and /health/ready (DB & dependencies ready)Complex readiness checks can hammer databases if polled every 5 secondsASP.NET Core MapHealthChecks() used by Kubernetes liveness probes
ObservabilityApplication MetricsFlying blind; no real-time visibility into throughput, latency, or error spikesExposes numeric counters, gauges, and histograms scraped by PrometheusOverhead of defining and maintaining business metric naming conventionsPrometheus scraping /metrics and alerting Grafana on P99 latency > 500ms
DeploymentService Instance per ContainerEnvironmental drift (β€œworks on my machine”), resource contention, slow provisioningPackages each microservice with its runtime and dependencies into a Docker imageRequires container orchestration (Kubernetes, ECS) and image registriesDeploying .NET Core microservices as isolated Pods on AKS / EKS
DeploymentSidecar PatternCross-cutting operational code polluting application business logicRuns a helper container alongside the primary application container in the same PodSlight memory and CPU overhead per Pod instanceDapr or Envoy sidecar intercepting traffic for mTLS and telemetry
DeploymentService MeshComplex network management, mTLS encryption, and traffic routing across servicesDedicated infrastructure layer managing service-to-service communication via proxiesSteep learning curve and added latency of proxy hopsIstio / Linkerd providing zero-trust mTLS and canary traffic splitting
SecurityAccess Token & Claims PropagationPassing authenticated user identity safely across distributed microservicesIdentity Provider issues signed JWT containing claims; services validate signatureToken revocation requires short expiration times or centralized blacklistsAPI Gateway validates OAuth2 JWT; downstream services read user claims
SecurityExternalized Configuration & SecretsStoring database passwords and API keys in source code repositoriesInjects configuration values and encrypted secrets at application startupRequires secure credential management for the secret provider itselfAzure Key Vault + App Configuration injected into ASP.NET Core IConfiguration
TestingConsumer-Driven Contract TestingBreaking API changes deployed unintentionally, breaking upstream consumersConsumers write contract tests; providers verify contracts in CI before deployingRequires 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:
    1. Core Subdomain: The unique software differentiator that makes your company money (e.g., proprietary matching algorithm).
    2. Supporting Subdomain: Custom business features that support the core domain (e.g., Customer Loyalty Portal).
    3. 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:

  1. OrderService (Create order in Pending state)
  2. PaymentService (Authorize and charge credit card)
  3. 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
end

2. 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| AnalyticsSvc

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

Practical .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 Pipeline
builder.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

0 of 0 items completed0%

Decision Framework & Best Practices

When designing microservices, follow these core architectural rules:

  1. 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.
  2. Never Share Databases Between Services: A shared database couples services, breaking independent deployability and invalidating microservice benefits.
  3. Embrace Asynchronous Communication by Default: Use asynchronous events (Kafka, RabbitMQ) for inter-service communication whenever immediate response data is not needed.
  4. Make Every Message Consumer Idempotent: In distributed systems with retries, duplicate messages will arrive. Ensure consumers handle duplicate deliveries safely using unique message IDs.
  5. Enforce Observability on Day One: Standardize OpenTelemetry traces (traceparent), structured JSON logs, and Prometheus metrics across all services before going to production.
  6. Prefer Saga Orchestration for Complex Business Workflows: For multi-step workflows with strict auditability and compensating actions, orchestration simplifies tracking and error recovery.

Interactive Architectural Roadmaps

Explore Complete Roadmaps & Pattern Checklists

Track your learning with interactive checklists for all 23 Gang of Four patterns and modern Microservice architecture patterns.

Share:
Back to Blog

Related Posts

View All Posts Β»