Back to Blog
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.

References

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.