Microservices with .NET 9: Architecture Patterns for Enterprise Systems
Proven microservices patterns in .NET 9 — service boundaries, API gateways, async messaging, and resilience for enterprise teams.
Microservices remain the default architecture conversation for enterprise teams scaling beyond a single deployment unit. With .NET 9, Microsoft has shipped meaningful improvements to hosting, native AOT, and OpenTelemetry support that make the platform a serious contender for large-scale distributed systems. But the framework alone does not solve the hard problems — getting service boundaries wrong is still the number-one cause of microservices failure.
This post covers six architecture patterns we consistently apply on .NET 9 engagements, drawn from real projects with 20+ services in production.
1. Drawing Service Boundaries That Last
The most expensive mistake is splitting too early or along the wrong seam. We follow a rule: if two services cannot be deployed independently without coordinating a release, they are one service.
Practical heuristics:
- Align to business capabilities, not data entities. An "OrderService" that also manages inventory is a monolith in disguise.
- Start with a modular monolith. .NET 9's improved minimal APIs and Keyed DI make it straightforward to build internal module boundaries that can be extracted later.
- Use the "team ownership" test. If a single team owns both sides of a boundary, that boundary likely does not need a network call.
Tip: We sketch service boundaries on a whiteboard using Event Storming sessions before writing any code. A two-hour workshop saves months of refactoring.
2. API Gateway: Routing, Not Logic
The API gateway should be a thin routing layer — never put business logic here. We typically deploy YARP (Yet Another Reverse Proxy) on .NET 9 for its deep integration with the platform.
Key configuration decisions:
- Route aggregation for mobile/BFF scenarios: keep it to simple fan-out, no transformation.
- Authentication offloading: validate JWT tokens at the gateway, pass claims downstream via headers.
- Rate limiting: use .NET 9's built-in
RateLimitermiddleware at the gateway tier. Define policies per client tier (free, standard, premium). - Health-check routing: exclude unhealthy downstream services from the gateway's route table automatically.
// YARP route configuration with rate limiting (simplified)
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("standard", opt =>
{
opt.Window = TimeSpan.FromMinutes(1);
opt.PermitLimit = 100;
});
});Avoid the trap of building a "smart gateway" that orchestrates calls across services — that path leads to a distributed monolith.
3. Async Messaging: The Backbone of Decoupling
Synchronous HTTP calls between services create tight coupling and cascade failures. For anything that does not require an immediate response, default to asynchronous messaging.
Our standard stack on Azure:
- Azure Service Bus for command-style messages (one consumer) and pub/sub topics (multiple consumers).
- MassTransit as the .NET abstraction layer — it handles retries, dead-letter routing, and saga state machines out of the box.
Patterns we apply consistently:
- Outbox pattern: never publish a message and write to a database in the same operation without transactional outbox. EF Core 9 + MassTransit's outbox support makes this straightforward.
- Idempotent consumers: every handler must tolerate duplicate delivery. Use a
MessageId-based deduplication table. - Message versioning: include a
SchemaVersionproperty from day one. It costs nothing now and saves painful migrations later.
Warning: Do not use async messaging for queries. If service A needs data from service B to respond to a user request, use a synchronous call (with resilience) or maintain a local read model.
4. Distributed Tracing with OpenTelemetry
.NET 9 has first-class OpenTelemetry support via System.Diagnostics.Activity. This is non-negotiable for production microservices.
Our standard setup:
- Instrumentation: add
OpenTelemetry.Extensions.Hostingto every service. Instrument HTTP clients, EF Core, MassTransit, and custom spans. - Exporter: ship traces to Azure Monitor (Application Insights) or Jaeger, depending on the client's stack.
- Correlation: propagate
traceparentheaders through all HTTP and message-based communication. MassTransit does this automatically. - Custom spans: wrap every significant business operation in a named
Activityso traces tell a business story, not just an infrastructure story.
Practical checklist for every service:
- HTTP server instrumentation enabled
- HTTP client instrumentation enabled
- Database query instrumentation enabled
- Message broker instrumentation enabled
- Custom business operation spans added
- Trace sampling configured (do not trace 100% in production)
5. Health Checks: More Than a Liveness Probe
.NET 9's health check framework (Microsoft.Extensions.Diagnostics.HealthChecks) supports three probe types that map directly to Kubernetes:
- Liveness (
/healthz): is the process alive? Keep this minimal — no database calls. - Readiness (
/ready): can the service handle traffic? Check database connectivity, message broker connection, downstream dependencies. - Startup (
/startup): has the service finished initialization? Critical for services with warm-up caches or migration steps.
Design rules:
- Readiness checks must have timeouts. A health check that hangs on a slow database call will cause cascading pod restarts.
- Degrade gracefully. If a non-critical dependency is down, report
Degraded, notUnhealthy. Let the orchestrator decide. - Expose health check UI for operations teams —
AspNetCore.HealthChecks.UIprovides a dashboard that aggregates all services.
6. Resilience Patterns with Polly v8 and .NET 9
.NET 9 integrates Microsoft.Extensions.Resilience (built on Polly v8) directly into the HTTP client factory. This is a significant improvement over manually wiring Polly policies.
The four patterns every inter-service call needs:
- Retry with jitter: exponential backoff plus random jitter to avoid thundering herds. Three retries is a sensible default.
- Circuit breaker: trip after 5 consecutive failures, half-open after 30 seconds. Prevents a failing downstream from consuming all your threads.
- Timeout: set aggressive timeouts (2-5 seconds for inter-service calls). A slow response is worse than a fast failure.
- Bulkhead isolation: limit concurrent calls to any single downstream service. Prevents one slow dependency from exhausting your connection pool.
// .NET 9 resilience pipeline (simplified)
builder.Services.AddHttpClient("OrderService")
.AddStandardResilienceHandler(options =>
{
options.Retry.MaxRetryAttempts = 3;
options.CircuitBreaker.FailureRatio = 0.5;
options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(3);
});Layer these patterns in order: Bulkhead -> Timeout -> Retry -> Circuit Breaker (outermost to innermost).
Architecture Decision Checklist
Before you commit to a microservices architecture on .NET 9, answer these honestly:
- Do you have independent teams that need to deploy independently?
- Is your domain well-understood enough to draw stable boundaries?
- Do you have the operational maturity for distributed tracing, centralized logging, and container orchestration?
- Can you afford the latency of network calls between services?
If you answered "no" to any of these, a modular monolith on .NET 9 may serve you better — and the migration path to microservices remains open.
At CC Conceptualise, we help enterprise teams make this architecture decision with confidence and implement it with production-grade patterns. If you are evaluating microservices for your .NET landscape, get in touch.