Microservices stopped being a buzzword a decade ago — today they are simply one of the architectures you are expected to evaluate honestly. The hard part was never defining the style; it was knowing which patterns to reach for, and when the whole approach is overkill. This guide walks through the patterns that matter for architects in practice: how to split a system, how services should talk, how to keep data consistent without distributed transactions, and how to keep the whole thing observable and resilient.

What microservices actually promise
A microservice architecture builds one application as a set of small, independently deployable services, each owning a business capability and its own data, communicating over lightweight protocols — HTTP APIs, gRPC, or messages. The definition comes from James Lewis and Martin Fowler's 2014 write-up, and it has aged well because it centers on the two things that still matter:
- Independent deployability. A team can ship its service on Friday afternoon without coordinating a release train across the company.
- Business-aligned boundaries. Services map to capabilities — Orders, Payments, Catalog — not to technical layers.
Everything else — containers, Kubernetes, service meshes — is implementation detail in service of those two properties. If a design gives you neither, it is a distributed monolith: all the operational cost of microservices with none of the payoff.
Start with the honest question: do you need them?
A well-modularized monolith deploys in one piece, debugs in one process, and needs no network calls between its modules. For most teams under ~20 engineers, that is the faster, cheaper architecture — and modern .NET makes it a genuinely clean one, as we show in our clean architecture guide.
Microservices earn their complexity when you have:
- Multiple teams blocking each other's releases.
- Uneven scaling needs — search traffic is 100× checkout traffic, and scaling the whole monolith for one hot path wastes money.
- Isolation requirements — a failure or a risky rewrite in one capability must not take down the rest.
If none of those apply yet, build the monolith with clear module boundaries. Boundaries are the expensive thing to get right; extracting a well-bounded module into a service later is mechanical. We cover the operational pain you sign up for in more depth in microservice architecture challenges.
Decomposition patterns: where to cut
The most consequential decisions are the service boundaries.
Decompose by business capability. Model services on what the business does: order management, inventory, billing, notifications. These boundaries are stable — businesses rarely reorganize what they fundamentally do.
Decompose by subdomain (DDD). Domain-Driven Design's bounded contexts give you a more rigorous version of the same idea. Each bounded context has its own model and its own language — Product means something different to Catalog than it does to Shipping, and that is fine, because each service owns its meaning.
Strangler Fig for existing systems. Nobody should rewrite a working monolith big-bang. Put a routing facade in front of it, carve out one capability at a time into a new service, and shrink the old system gradually. This is the same incremental philosophy behind migrating legacy WCF services to CoreWCF or gRPC — replace at the edges, verify behavior, repeat.
The anti-pattern to avoid: decomposing by technical layer ("the API service, the business-logic service, the data service"). That guarantees every feature touches every service.
Communication patterns: sync where you must, async where you can
API Gateway. Clients should not talk to fifteen services directly. A gateway (YARP, Azure API Management, Kong) gives you one entry point for routing, authentication, rate limiting, and response shaping. Token validation belongs here or at each service edge — the mechanics are the same JWT authentication flow you would use in any API.
Synchronous calls (REST/gRPC) are fine for queries the caller genuinely has to wait for. But every synchronous hop couples your uptime to the callee's uptime. Chains of synchronous calls are how a slow dependency becomes a site-wide outage.
Asynchronous messaging is the default for anything that changes state across services. OrderPlaced goes onto a broker (Azure Service Bus, RabbitMQ, Kafka); Inventory, Notifications, and Analytics each consume it on their own schedule. The order service neither knows nor cares who is listening — that is the loose coupling that makes independent deployment real.
Circuit breakers and retries. When a downstream service degrades, callers should fail fast rather than pile up threads waiting. In .NET, Microsoft.Extensions.Http.Resilience (built on Polly) gives you retries with backoff, timeouts, and circuit breaking as a few lines of HttpClient configuration. Treat it as mandatory for every cross-service call.
Data patterns: the part that actually hurts
Database per service is non-negotiable if you want independence. The moment two services share tables, their schemas — and their deploys — are coupled forever. Each service owns its storage and exposes data only through its API or its events. This also frees each service to pick the storage that fits: relational for orders, a document store for a product catalog — even MongoDB in a container for services whose data is naturally document-shaped.
Saga pattern replaces distributed transactions. An order flow becomes a sequence of local transactions — reserve stock, charge payment, confirm order — coordinated either by events (choreography) or by a central orchestrator. Each step has a compensating action: if payment fails, a StockReservationCancelled event undoes the reservation. Sagas trade the comfort of ACID for availability, and force you to design for the failure cases explicitly — which, honestly, you should be doing anyway.
CQRS. Separate the write model from read models. Writes go through the owning service; read-heavy views (a dashboard joining data from five services) are served from a denormalized projection kept up to date by consuming events. Pair it with caching and most "we need cross-service joins" problems disappear.
Eventual consistency is the price of all this. The inventory count a user sees may be milliseconds stale. Design the UX for it (confirm asynchronously, notify on failure) instead of pretending strong consistency exists across service boundaries.
Operational patterns: deployment and observability
One service, one container. Containers make "runs on my machine" a contract — the same image moves through dev, staging, and production. If you are new to containerizing .NET services, start with our Docker for .NET guide, then let an orchestrator (Kubernetes, Azure Container Apps) handle scheduling, scaling, and restarts.
Automated pipelines are a prerequisite, not a nice-to-have. Fifteen services deployed manually is fifteen ways to have a bad weekend. Every service needs its own build-test-deploy pipeline — the pattern in our CI/CD with GitHub Actions guide scales from one service to fifty, and serverless pieces of the system can ride the same pipelines as Azure Functions.
Observability: logs, metrics, traces. In a monolith, a stack trace tells the story. In microservices, one user request may touch eight services, so you need:
- Structured, centralized logs with a correlation ID stamped at the gateway and propagated on every hop.
- Metrics per service — request rate, error rate, latency percentiles.
- Distributed traces (OpenTelemetry is the standard, and .NET emits it natively) so you can see the whole request path and find the slow hop in seconds.
We walk through the full .NET setup — Serilog, OpenTelemetry, health checks — in observability for .NET backends. Without this layer, every production incident becomes archaeology.
Health checks and self-healing. Every service exposes liveness and readiness endpoints; the orchestrator restarts what fails and routes traffic only to instances that are actually ready. Failures stop being pages at 3 a.m. and become log entries you review in the morning.
A pragmatic adoption path
- Build a modular monolith first with clean architecture and clear module boundaries — start here if you are setting one up.
- Extract your first service only when a real force demands it (team contention, scaling, isolation) — and pick the module with the fewest dependencies, not the most interesting one.
- Put the plumbing in before the second service: gateway, message broker, centralized logging, CI/CD per service.
- Grow by capability, applying database-per-service and sagas as data splits force the issue.
- Measure — if lead time to production and deployment frequency are not improving, stop extracting and fix the boundaries you have.
For a hands-on version of this journey — two .NET services, RabbitMQ messaging, Docker Compose, and an API gateway — work through building microservices in .NET, which turns the patterns in this article into running code.
Where AI fits into a microservice architecture
AI features are arriving in every roadmap, and microservices absorb them well — treat an AI capability as just another service with unusual dependencies. A summarization or recommendation feature becomes its own bounded service that wraps the model or LLM API, owns its prompts and evaluation data, and exposes a plain API to the rest of the system. That isolation matters more than usual here: models change weekly, costs need one place to be measured, and you will swap providers.
Two patterns earn their keep early. First, an AI gateway — the same idea as your API gateway, but for outbound model calls: one choke point for API keys, rate limits, cost tracking, prompt/response logging, and fallback between providers. Second, async by default — LLM calls are slow and occasionally fail; queue the request, process it like any other background job, and deliver the result by event or notification rather than holding an HTTP connection open. And the same discipline applies as everywhere else in this article: agentic AI systems are still software — boundaries, observability, and failure handling decide whether they ship reliably.
The architect's summary
Microservices are a trade: you accept distributed-systems complexity to buy team autonomy and independent scaling. The patterns above are how you keep the trade honest — capability-based boundaries so services stay independent, async messaging and sagas so they stay decoupled, database-per-service so they stay honest, and observability so you can still understand the system you built. Adopt them when the forces are real, in the order the pain arrives — and never all at once.
Comments (0)
No comments yet — be the first to share your thoughts.