6 min read
Microservices Patterns on Azure
Introduction
Microservices architecture decomposes applications into small, independently deployable services. Azure provides numerous services that support microservices patterns. This guide explores key patterns and their implementations using Azure services.
Service Decomposition
Domain-Driven Design Approach
// Bounded Context: Orders
namespace OrderService.Domain
{
public class Order : AggregateRoot
{
public Guid Id { get; private set; }
public Guid CustomerId { get; private set; }
public OrderStatus Status { get; private set; }
public Money Total { get; private set; }
private readonly List<OrderItem> _items = new();
public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
public static Order Create(Guid customerId)
{
var order = new Order
{
Id = Guid.NewGuid(),
CustomerId = customerId,
Status = OrderStatus.Draft
};
order.AddDomainEvent(new OrderCreatedEvent(order.Id, customerId));
return order;
}
public void AddItem(Guid productId, int quantity, Money price)
{
var item = new OrderItem(productId, quantity, price);
_items.Add(item);
RecalculateTotal();
}
public void Submit()
{
if (!_items.Any())
throw new DomainException("Cannot submit empty order");
Status = OrderStatus.Submitted;
AddDomainEvent(new OrderSubmittedEvent(Id, CustomerId, Total));
}
}
}
// Bounded Context: Inventory
namespace InventoryService.Domain
{
public class InventoryItem : AggregateRoot
{
public Guid ProductId { get; private set; }
public int QuantityOnHand { get; private set; }
public int ReservedQuantity { get; private set; }
public int AvailableQuantity => QuantityOnHand - ReservedQuantity;
public void Reserve(int quantity)
{
if (quantity > AvailableQuantity)
throw new InsufficientInventoryException(ProductId, quantity, AvailableQuantity);
ReservedQuantity += quantity;
AddDomainEvent(new InventoryReservedEvent(ProductId, quantity));
}
public void ReleaseReservation(int quantity)
{
ReservedQuantity = Math.Max(0, ReservedQuantity - quantity);
AddDomainEvent(new InventoryReleasedEvent(ProductId, quantity));
}
}
}
Service Communication Patterns
Synchronous Communication via HTTP
// Resilient HTTP client configuration
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddMicroserviceClients(
this IServiceCollection services,
IConfiguration configuration)
{
// Inventory Service Client
services.AddHttpClient<IInventoryClient, InventoryClient>(client =>
{
client.BaseAddress = new Uri(configuration["Services:Inventory:BaseUrl"]);
})
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetCircuitBreakerPolicy());
// Payment Service Client
services.AddHttpClient<IPaymentClient, PaymentClient>(client =>
{
client.BaseAddress = new Uri(configuration["Services:Payment:BaseUrl"]);
})
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetCircuitBreakerPolicy());
return services;
}
private static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
}
private static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
}
}
Asynchronous Communication via Service Bus
// Message publisher
public class ServiceBusPublisher : IEventPublisher
{
private readonly ServiceBusSender _sender;
public ServiceBusPublisher(ServiceBusClient client, string topicName)
{
_sender = client.CreateSender(topicName);
}
public async Task PublishAsync<T>(T @event) where T : IntegrationEvent
{
var message = new ServiceBusMessage(
JsonSerializer.SerializeToUtf8Bytes(@event))
{
MessageId = @event.Id.ToString(),
Subject = typeof(T).Name,
ContentType = "application/json",
CorrelationId = Activity.Current?.Id
};
// Add metadata for routing
message.ApplicationProperties["EventType"] = typeof(T).FullName;
message.ApplicationProperties["Timestamp"] = @event.Timestamp;
await _sender.SendMessageAsync(message);
}
}
// Message consumer (Azure Function)
public class OrderEventsConsumer
{
private readonly IOrderService _orderService;
private readonly ILogger<OrderEventsConsumer> _logger;
[Function("ProcessInventoryReserved")]
public async Task ProcessInventoryReserved(
[ServiceBusTrigger("inventory-events", "order-service")]
ServiceBusReceivedMessage message,
ServiceBusMessageActions messageActions)
{
try
{
var eventType = message.ApplicationProperties["EventType"]?.ToString();
if (eventType == typeof(InventoryReservedEvent).FullName)
{
var @event = JsonSerializer.Deserialize<InventoryReservedEvent>(
message.Body.ToArray());
await _orderService.HandleInventoryReservedAsync(@event);
}
await messageActions.CompleteMessageAsync(message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing message {MessageId}", message.MessageId);
if (message.DeliveryCount >= 3)
{
await messageActions.DeadLetterMessageAsync(message,
deadLetterReason: "ProcessingFailed",
deadLetterErrorDescription: ex.Message);
}
else
{
throw; // Retry
}
}
}
}
API Gateway Pattern
Azure API Management Configuration
// api-management.bicep
resource apim 'Microsoft.ApiManagement/service@2021-08-01' = {
name: 'apim-microservices'
location: location
sku: {
name: 'Developer'
capacity: 1
}
properties: {
publisherEmail: 'admin@company.com'
publisherName: 'Company'
}
}
// Orders API
resource ordersApi 'Microsoft.ApiManagement/service/apis@2021-08-01' = {
parent: apim
name: 'orders-api'
properties: {
displayName: 'Orders API'
path: 'orders'
protocols: ['https']
serviceUrl: 'https://order-service.azurewebsites.net'
subscriptionRequired: true
}
}
// Rate limiting policy
resource rateLimitPolicy 'Microsoft.ApiManagement/service/policies@2021-08-01' = {
parent: apim
name: 'policy'
properties: {
format: 'xml'
value: '''
<policies>
<inbound>
<rate-limit calls="100" renewal-period="60" />
<quota calls="10000" renewal-period="86400" />
<cors allow-credentials="true">
<allowed-origins>
<origin>https://app.company.com</origin>
</allowed-origins>
</cors>
</inbound>
</policies>
'''
}
}
Service Discovery
Using Azure Service Bus with Dapr
# dapr-components/pubsub.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: order-pubsub
spec:
type: pubsub.azure.servicebus
version: v1
metadata:
- name: connectionString
secretKeyRef:
name: servicebus-secret
key: connectionString
---
# dapr-components/statestore.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
spec:
type: state.azure.cosmosdb
version: v1
metadata:
- name: url
value: "https://mycosmosdb.documents.azure.com:443/"
- name: masterKey
secretKeyRef:
name: cosmos-secret
key: masterKey
- name: database
value: "microservices"
- name: collection
value: "state"
Dapr Service Invocation
public class OrderService
{
private readonly DaprClient _daprClient;
public OrderService(DaprClient daprClient)
{
_daprClient = daprClient;
}
public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
{
// Check inventory via service invocation
var inventoryCheck = await _daprClient.InvokeMethodAsync<InventoryCheckRequest, InventoryCheckResponse>(
"inventory-service",
"check",
new InventoryCheckRequest { Items = request.Items });
if (!inventoryCheck.AllAvailable)
{
throw new InsufficientInventoryException(inventoryCheck.UnavailableItems);
}
// Create order
var order = Order.Create(request.CustomerId);
foreach (var item in request.Items)
{
order.AddItem(item.ProductId, item.Quantity, item.Price);
}
// Save state
await _daprClient.SaveStateAsync("statestore", order.Id.ToString(), order);
// Publish event
await _daprClient.PublishEventAsync("order-pubsub", "orders", new OrderCreatedEvent
{
OrderId = order.Id,
CustomerId = order.CustomerId,
Items = order.Items.ToList()
});
return order;
}
}
Database Per Service Pattern
Service-Specific Data Stores
// databases.bicep
// Order Service - Azure SQL
resource ordersSql 'Microsoft.Sql/servers@2021-05-01-preview' = {
name: 'sql-orders'
location: location
properties: {
administratorLogin: sqlAdminLogin
administratorLoginPassword: sqlAdminPassword
}
resource database 'databases' = {
name: 'orders-db'
location: location
sku: {
name: 'S1'
tier: 'Standard'
}
}
}
// Inventory Service - Cosmos DB
resource inventoryCosmos 'Microsoft.DocumentDB/databaseAccounts@2021-06-15' = {
name: 'cosmos-inventory'
location: location
kind: 'GlobalDocumentDB'
properties: {
databaseAccountOfferType: 'Standard'
consistencyPolicy: {
defaultConsistencyLevel: 'Session'
}
locations: [
{
locationName: location
failoverPriority: 0
}
]
}
resource database 'sqlDatabases' = {
name: 'inventory-db'
properties: {
resource: {
id: 'inventory-db'
}
}
}
}
// Product Catalog - Azure Cognitive Search
resource searchService 'Microsoft.Search/searchServices@2021-04-01-preview' = {
name: 'search-products'
location: location
sku: {
name: 'standard'
}
}
Health Monitoring
Distributed Health Checks
// Aggregated health check endpoint
public class HealthCheckService
{
private readonly HttpClient _httpClient;
private readonly IConfiguration _configuration;
public async Task<SystemHealthReport> GetSystemHealthAsync()
{
var services = _configuration.GetSection("Services").GetChildren();
var tasks = services.Select(async service =>
{
var healthUrl = $"{service["BaseUrl"]}/health";
try
{
var response = await _httpClient.GetAsync(healthUrl);
return new ServiceHealth
{
Name = service.Key,
Status = response.IsSuccessStatusCode ? "Healthy" : "Unhealthy",
ResponseTime = response.Headers.GetValues("X-Response-Time").FirstOrDefault()
};
}
catch (Exception ex)
{
return new ServiceHealth
{
Name = service.Key,
Status = "Unhealthy",
Error = ex.Message
};
}
});
var results = await Task.WhenAll(tasks);
return new SystemHealthReport
{
Timestamp = DateTime.UtcNow,
Services = results.ToList(),
OverallStatus = results.All(s => s.Status == "Healthy") ? "Healthy" : "Degraded"
};
}
}
Distributed Tracing
Application Insights Integration
// Program.cs
builder.Services.AddApplicationInsightsTelemetry();
builder.Services.AddServiceProfiler();
// Custom telemetry
public class OrderTelemetry
{
private readonly TelemetryClient _telemetryClient;
public void TrackOrderProcessing(Order order, TimeSpan duration)
{
var properties = new Dictionary<string, string>
{
["OrderId"] = order.Id.ToString(),
["CustomerId"] = order.CustomerId.ToString(),
["ItemCount"] = order.Items.Count.ToString()
};
var metrics = new Dictionary<string, double>
{
["OrderTotal"] = (double)order.Total.Amount,
["ProcessingDurationMs"] = duration.TotalMilliseconds
};
_telemetryClient.TrackEvent("OrderProcessed", properties, metrics);
}
public IOperationHolder<DependencyTelemetry> StartDependencyOperation(
string dependencyName,
string target)
{
return _telemetryClient.StartOperation<DependencyTelemetry>(
$"Call {dependencyName}");
}
}
Conclusion
Microservices patterns on Azure require careful consideration of service decomposition, communication strategies, and data management. By leveraging Azure services like Service Bus, API Management, and Cosmos DB, you can build scalable, resilient microservices architectures. Start simple and evolve your architecture as requirements demand.