1 min read
Microservices Patterns on Azure
I wrote “2021-06-21-microservices-patterns-azure” to share practical, production-minded guidance on this topic.
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\n\n## Takeaways\n\n*Add a concise, personal takeaway and recommended next steps here.*\n