Microservices Architecture
Microservices architecture structures an application as a collection of loosely coupled, independently deployable services. Each service is owned by a small team and focuses on a specific business capability.
Core Principles
Single Responsibility
Each microservice should do one thing well:
// Order Service - Only handles order lifecycle
public class OrderService
{
private readonly IEventPublisher _eventPublisher;
private readonly IOrderRepository _orderRepository;
public async Task<Order> CreateOrderAsync(CreateOrderCommand command)
{
var order = Order.Create(
command.CustomerId,
command.Items,
command.ShippingAddress
);
await _orderRepository.SaveAsync(order);
// Publish event for other services to react
await _eventPublisher.PublishAsync(new OrderCreatedEvent
{
OrderId = order.Id,
CustomerId = order.CustomerId,
Items = order.Items.Select(i => new OrderItemDto
{
ProductId = i.ProductId,
Quantity = i.Quantity
}).ToList(),
TotalAmount = order.TotalAmount,
CreatedAt = order.CreatedAt
});
return order;
}
}
Independent Deployability
# kubernetes/order-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
labels:
app: order-service
version: v2.1.0
spec:
replicas: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
version: v2.1.0
spec:
containers:
- name: order-service
image: myregistry/order-service:v2.1.0
ports:
- containerPort: 8080
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: order-db-secret
key: connection-string
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
Service Decomposition Strategies
By Business Capability
By Subdomain (DDD Approach)
// Bounded Context: Ordering
namespace Ordering.Domain
{
public class Order : AggregateRoot
{
public OrderId Id { get; private set; }
public CustomerId CustomerId { get; private set; }
public OrderStatus Status { get; private set; }
public Money TotalAmount { get; private set; }
private readonly List<OrderItem> _items = new();
// Order has its own view of Customer (just the ID)
// Order has its own view of Product (OrderItem with snapshot data)
}
public class OrderItem : Entity
{
public ProductId ProductId { get; private set; }
public string ProductName { get; private set; } // Snapshot at order time
public Money UnitPrice { get; private set; } // Snapshot at order time
public int Quantity { get; private set; }
}
}
// Bounded Context: Catalog (separate service)
namespace Catalog.Domain
{
public class Product : AggregateRoot
{
public ProductId Id { get; private set; }
public string Name { get; private set; }
public string Description { get; private set; }
public Money Price { get; private set; }
public Category Category { get; private set; }
public List<ProductImage> Images { get; private set; }
// Rich product model with full lifecycle
}
}
Strangler Fig Pattern for Migration
// API Gateway routing during migration
public class StranglerFigRouter
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IFeatureFlagService _featureFlags;
public async Task<HttpResponseMessage> RouteRequestAsync(
HttpRequest request,
string feature)
{
var useNewService = await _featureFlags.IsEnabledAsync(
$"use-new-{feature}-service");
var client = _httpClientFactory.CreateClient(
useNewService ? $"new-{feature}-service" : "legacy-monolith");
var targetPath = useNewService
? MapToNewServicePath(request.Path, feature)
: request.Path;
return await ForwardRequestAsync(client, request, targetPath);
}
// Gradually migrate features
// 1. Build new service
// 2. Route percentage of traffic
// 3. Monitor and compare
// 4. Increase traffic
// 5. Decommission legacy
}
Inter-Service Communication
Synchronous (HTTP/gRPC)
// gRPC Service Definition
syntax = "proto3";
package inventory;
service InventoryService {
rpc CheckAvailability(CheckAvailabilityRequest)
returns (CheckAvailabilityResponse);
rpc ReserveStock(ReserveStockRequest)
returns (ReserveStockResponse);
}
message CheckAvailabilityRequest {
repeated string product_ids = 1;
}
message CheckAvailabilityResponse {
repeated ProductAvailability items = 1;
}
message ProductAvailability {
string product_id = 1;
int32 available_quantity = 2;
bool is_available = 3;
}
// gRPC Client with Resilience
public class InventoryClient : IInventoryClient
{
private readonly InventoryService.InventoryServiceClient _client;
private readonly ILogger<InventoryClient> _logger;
private static readonly AsyncRetryPolicy<CheckAvailabilityResponse> RetryPolicy =
Policy<CheckAvailabilityResponse>
.Handle<RpcException>(ex =>
ex.StatusCode == StatusCode.Unavailable ||
ex.StatusCode == StatusCode.DeadlineExceeded)
.WaitAndRetryAsync(
3,
retryAttempt => TimeSpan.FromMilliseconds(
Math.Pow(2, retryAttempt) * 100));
public async Task<CheckAvailabilityResponse> CheckAvailabilityAsync(
IEnumerable<string> productIds,
CancellationToken ct = default)
{
return await RetryPolicy.ExecuteAsync(async () =>
{
var deadline = DateTime.UtcNow.AddSeconds(5);
return await _client.CheckAvailabilityAsync(
new CheckAvailabilityRequest
{
ProductIds = { productIds }
},
deadline: deadline,
cancellationToken: ct);
});
}
}
Asynchronous (Message-Based)
// Event-Driven Communication
public class OrderCreatedHandler : IMessageHandler<OrderCreatedEvent>
{
private readonly IInventoryService _inventoryService;
public async Task HandleAsync(
OrderCreatedEvent @event,
CancellationToken ct)
{
// Each service reacts independently
// This handler is in the Inventory service
await _inventoryService.ReserveStockAsync(
@event.OrderId,
@event.Items.Select(i => new StockReservation
{
ProductId = i.ProductId,
Quantity = i.Quantity
}),
ct);
}
}
Saga Pattern for Distributed Transactions
// Choreography-based Saga
public class OrderSaga :
ISaga,
InitiatedBy<OrderCreatedEvent>,
Orchestrates<PaymentProcessedEvent>,
Orchestrates<PaymentFailedEvent>,
Orchestrates<StockReservedEvent>,
Orchestrates<StockReservationFailedEvent>
{
public Guid CorrelationId { get; set; }
public OrderSagaState State { get; set; }
public Guid OrderId { get; set; }
public bool PaymentCompleted { get; set; }
public bool StockReserved { get; set; }
public async Task Consume(ConsumeContext<PaymentFailedEvent> context)
{
State = OrderSagaState.Failed;
// Compensating action - release reserved stock
if (StockReserved)
{
await context.Publish(new ReleaseStockCommand
{
OrderId = OrderId
});
}
await context.Publish(new OrderCancelledEvent
{
OrderId = OrderId,
Reason = "Payment failed"
});
}
public async Task Consume(ConsumeContext<StockReservationFailedEvent> context)
{
State = OrderSagaState.Failed;
// Compensating action - refund payment
if (PaymentCompleted)
{
await context.Publish(new RefundPaymentCommand
{
OrderId = OrderId
});
}
await context.Publish(new OrderCancelledEvent
{
OrderId = OrderId,
Reason = "Stock not available"
});
}
}
Service Discovery and Load Balancing
// Service Registration with Consul
public class ServiceRegistration : IHostedService
{
private readonly IConsulClient _consulClient;
private readonly ServiceConfig _config;
private string _registrationId;
public async Task StartAsync(CancellationToken ct)
{
_registrationId = $"{_config.ServiceName}-{Guid.NewGuid()}";
var registration = new AgentServiceRegistration
{
ID = _registrationId,
Name = _config.ServiceName,
Address = _config.ServiceHost,
Port = _config.ServicePort,
Tags = new[] { "api", "v1" },
Check = new AgentServiceCheck
{
HTTP = $"http://{_config.ServiceHost}:{_config.ServicePort}/health",
Interval = TimeSpan.FromSeconds(10),
Timeout = TimeSpan.FromSeconds(5),
DeregisterCriticalServiceAfter = TimeSpan.FromMinutes(1)
}
};
await _consulClient.Agent.ServiceRegister(registration, ct);
}
}
Observability - Distributed Tracing
// OpenTelemetry Configuration
public static class ObservabilityExtensions
{
public static IServiceCollection AddObservability(
this IServiceCollection services,
string serviceName)
{
services.AddOpenTelemetry()
.ConfigureResource(resource => resource
.AddService(serviceName))
.WithTracing(tracing => tracing
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddGrpcClientInstrumentation()
.AddSource(serviceName)
.AddOtlpExporter(options =>
{
options.Endpoint = new Uri("http://jaeger:4317");
}))
.WithMetrics(metrics => metrics
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddRuntimeInstrumentation()
.AddPrometheusExporter());
return services;
}
}
Key Takeaways
- Service Boundaries: Align with business capabilities and bounded contexts
- Communication: Prefer async messaging for loose coupling; use sync for queries
- Data Ownership: Each service owns its data; no shared databases
- Resilience: Implement retries, circuit breakers, and fallbacks
- Observability: Distributed tracing is essential for debugging