Microservices split a monolith into independently deployable services, each owning a business domain. .NET provides first-class support for HTTP APIs, gRPC, messaging, and containerized deployment — making it a strong choice for enterprise microservice architectures.

Monolith vs Microservices

  Monolith:                         Microservices:
┌──────────────────────┐         ┌────────┐  ┌────────┐  ┌────────┐
│ Users │ Orders │ Pay  │         │  User  │  │ Order  │  │Payment │
│   Shared Database    │         │ Service│  │Service │  │Service │
└──────────────────────┘         └────────┘  └────────┘  └────────┘
                                          ↑
                                    API Gateway (YARP)
  

Extract services when team size, deployment conflicts, or scaling needs justify the operational overhead. Start with a modular monolith and split along natural boundaries.

Service Boundaries

Decompose by business capability, not technical layer:

  ✅ UserService, OrderService, PaymentService, NotificationService
❌ ControllerService, RepositoryService, ValidationService
  

Each service:

  • Owns its database (no shared tables)
  • Exposes a well-defined API
  • Deploys independently
  • Can fail without cascading (with proper resilience)

Project Structure

  src/
├── Services/
│   ├── UserService/
│   │   ├── UserService.Api/
│   │   ├── UserService.Domain/
│   │   └── UserService.Infrastructure/
│   ├── OrderService/
│   └── PaymentService/
├── Gateway/
│   └── ApiGateway/
└── Shared/
    └── Contracts/          # DTOs, event schemas only
  

Avoid shared libraries with business logic — they create coupling. Share only contracts (DTOs, event definitions).

REST API Service

  // OrderService.Api/Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<OrderDbContext>(o =>
    o.UseNpgsql(builder.Configuration.GetConnectionString("Orders")));
builder.Services.AddScoped<IOrderRepository, OrderRepository>();

var app = builder.Build();

app.MapGet("/orders/{id:guid}", async (Guid id, IOrderRepository repo) =>
{
    var order = await repo.GetByIdAsync(id);
    return order is null ? Results.NotFound() : Results.Ok(order);
});

app.MapPost("/orders", async (CreateOrderRequest req, IOrderRepository repo) =>
{
    var order = await repo.CreateAsync(req);
    return Results.Created($"/orders/{order.Id}", order);
});

app.Run();
  

gRPC for Service-to-Service

gRPC is faster and strongly typed — ideal for internal communication:

  // orders.proto
syntax = "proto3";
service OrderService {
    rpc GetOrder (GetOrderRequest) returns (OrderResponse);
}
message GetOrderRequest { string id = 1; }
message OrderResponse { string id = 1; string status = 2; double total = 3; }
  
  // Client
var channel = GrpcChannel.ForAddress("https://order-service:5001");
var client = new OrderService.OrderServiceClient(channel);
var response = await client.GetOrderAsync(new GetOrderRequest { Id = orderId });
  

Enable HTTP/2 and TLS between services in production.

Message Bus with MassTransit

Decouple services with asynchronous events:

  // Shared contract
public record OrderPlaced(Guid OrderId, Guid CustomerId, decimal Total);

// Order Service — publish
await publishEndpoint.Publish(new OrderPlaced(order.Id, order.CustomerId, order.Total));

// Payment Service — consume
public class OrderPlacedConsumer : IConsumer<OrderPlaced>
{
    public async Task Consume(ConsumeContext<OrderPlaced> context)
    {
        var msg = context.Message;
        await paymentService.ProcessPayment(msg.OrderId, msg.Total);
    }
}
  

Configure RabbitMQ or Azure Service Bus as the transport. Consumers must be idempotent.

API Gateway with YARP

  // Gateway/Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

var app = builder.Build();
app.MapReverseProxy();
app.Run();
  
  {
  "ReverseProxy": {
    "Routes": {
      "orders": {
        "ClusterId": "order-cluster",
        "Match": { "Path": "/api/orders/{**catch-all}" }
      }
    },
    "Clusters": {
      "order-cluster": {
        "Destinations": {
          "order1": { "Address": "http://order-service:8080" }
        }
      }
    }
  }
}
  

YARP handles routing, load balancing, and transforms at the edge.

Resilience with Polly

  builder.Services.AddHttpClient<IOrderClient, OrderClient>()
    .AddStandardResilienceHandler(options =>
    {
        options.Retry.MaxRetryAttempts = 3;
        options.CircuitBreaker.SamplingDuration = TimeSpan.FromSeconds(30);
        options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(5);
    });
  

Patterns: retry with backoff, circuit breaker, timeout, bulkhead. Prevent cascading failures when a downstream service is unhealthy.

Health Checks

  builder.Services.AddHealthChecks()
    .AddNpgSql(connectionString)
    .AddRabbitMQ(rabbitConnectionString);

app.MapHealthChecks("/health");
app.MapHealthChecks("/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready"),
});
  

Kubernetes uses /health (liveness) and /ready (readiness) probes.

Containerization

  FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 8080

FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish OrderService.Api -c Release -o /app/publish

FROM base AS final
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "OrderService.Api.dll"]
  

Deploy to Kubernetes, Azure Container Apps, or AWS ECS.

Distributed Tracing

  builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddOtlpExporter());
  

Correlate requests across services with trace IDs. Tools: Jaeger, Application Insights, Grafana Tempo.

Data Management

  • Database per service — no cross-service JOINs
  • Saga pattern for distributed transactions (choreography via events or orchestration)
  • CQRS when read and write patterns differ significantly
  • Event sourcing for audit-heavy domains (optional, adds complexity)

Production Checklist

  • Clear service boundaries with owned databases
  • API gateway for external clients
  • gRPC or message bus for internal communication
  • Resilience policies (retry, circuit breaker, timeout)
  • Health checks and structured logging
  • Distributed tracing enabled
  • Container images with non-root user
  • CI/CD per service (independent deploy pipelines)
  • Contract tests between services

.NET microservices succeed when teams embrace autonomous services, async communication, and operational observability from day one.