BlogCloud Native

Building Cloud-Native Microservices on Azure

Most systems start as monoliths.

Author

Artan Ajredini

Artan Ajredini

CEO & Cloud Architect

5 min read
7 April 2025

From Monolith to Microservices

Most systems start as monoliths. A single deployable unit, a single database, a single team. For a long time, this is the right architecture — simple to reason about, easy to test, and fast to ship. The monolith becomes a problem when it starts fighting you: deployments take an hour, a bug in the payment module breaks the entire application, and ten teams are all stepping on each other in the same codebase.

Microservices decompose that monolith into independently deployable services, each owning its own data and communicating over well-defined APIs or messages. The goal is not to have many small services — it is to have the right boundaries so teams can move independently.

Do not start with microservices. Start with a well-structured monolith, find the seams where teams and domains naturally separate, then extract services along those boundaries. Decomposing prematurely creates a distributed monolith — all the complexity of microservices with none of the benefits.

The Strangler Fig pattern

The safest way to decompose a monolith is the Strangler Fig pattern: incrementally extract functionality into new services while the monolith continues to run. A facade (often an API gateway or Azure Application Gateway) routes traffic — new requests go to the new service, old requests go to the monolith. Over time, the monolith shrinks and the new services grow until the monolith can be retired.

  1. Identify a bounded context — a domain with clear ownership and minimal coupling to other modules (e.g. order management, user accounts, notifications).
  2. Build the new service independently, with its own database and deployment pipeline.
  3. Put a routing layer (API Gateway or Azure Front Door) in front of both the monolith and the new service.
  4. Migrate traffic incrementally — start with read traffic, then writes, then retire the monolith module.
  5. Repeat for the next bounded context.

Architecture Design on Azure

A cloud-native microservices architecture on Azure is built around three core decisions: where services run, how they communicate, and how each service manages its data.

Cloud-native architecture components on Azure

Azure Kubernetes Service (AKS) as the runtime

AKS is the most common runtime for microservices on Azure. It provides container orchestration, service discovery, health management, rolling deployments, and horizontal scaling. Each microservice is packaged as a Docker image and deployed as a Kubernetes Deployment with its own service, resource limits, and health probes.

yaml
# k8s/order-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
        - name: order-service
          image: myregistry.azurecr.io/order-service:1.0.0
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 256Mi
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 15
          readinessProbe:
            httpGet:
              path: /ready
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10

Per-service data isolation with Azure Cosmos DB

Each microservice owns its data. No shared databases — this is the most important rule. Shared databases create invisible coupling: one service's schema change breaks another service's queries. Use Azure Cosmos DB for services that need globally distributed, low-latency reads, or Azure SQL / PostgreSQL Flexible Server for relational workloads.

  • One database (or database account) per service — never share a database between two services.
  • If service A needs data owned by service B, it calls service B's API or subscribes to service B's events.
  • Cosmos DB's partitioning model aligns well with microservice access patterns — partition by the entity your service most frequently queries (e.g. orderId, customerId).
  • Use Cosmos DB change feed to publish events when data changes — other services subscribe to these events rather than polling.

Asynchronous messaging with Azure Service Bus

Synchronous HTTP calls between services create tight coupling and cascade failures. If the payment service is slow, the order service becomes slow too. Use Azure Service Bus for asynchronous, decoupled communication between services — especially for operations that do not need an immediate response.

csharp
// Publishing an event to Service Bus from the Order service
public class OrderService
{
    private readonly ServiceBusSender _sender;

    public async Task PlaceOrderAsync(Order order)
    {
        await _orderRepository.SaveAsync(order);

        var message = new ServiceBusMessage(
            JsonSerializer.Serialize(new OrderPlacedEvent
            {
                OrderId = order.Id,
                CustomerId = order.CustomerId,
                TotalAmount = order.Total,
                PlacedAt = DateTimeOffset.UtcNow
            }))
        {
            Subject = "order.placed",
            ContentType = "application/json"
        };

        await _sender.SendMessageAsync(message);
    }
}

Distributed Tracing and the Outbox Pattern

When a request spans five services, a log in one service tells you nothing on its own. You need distributed tracing — a way to follow a single request across every service it touches and see where time was spent or where it failed.

OpenTelemetry and Azure Monitor

OpenTelemetry is the open standard for distributed tracing and metrics. Instrument your services once with the OpenTelemetry SDK and export traces to Azure Monitor (Application Insights). Every cross-service call propagates a trace context — a correlation ID that links all the spans from a single user request into one end-to-end trace.

csharp
// Program.cs — wire up OpenTelemetry in .NET
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddEntityFrameworkCoreInstrumentation()
        .AddAzureMonitorTraceExporter(options =>
        {
            options.ConnectionString = builder.Configuration
                ["ApplicationInsights:ConnectionString"];
        }))
    .WithMetrics(metrics => metrics
        .AddAspNetCoreInstrumentation()
        .AddRuntimeInstrumentation()
        .AddAzureMonitorMetricExporter());

In Azure Monitor, use the Application Map to see the topology of your services and where latency or failures are occurring. Use Transaction Search to drill into a specific failing request and see every span across every service.

The Outbox Pattern for reliable event publishing

A common bug in event-driven microservices: a service saves data to its database and then publishes an event to Service Bus. If the service crashes between those two steps, the data is saved but the event is never published — downstream services never know the order was placed.

The Outbox Pattern solves this by writing the event to an outbox table in the same database transaction as the business data. A background process (the outbox relay) reads from the outbox table and publishes to Service Bus, marking events as published once confirmed. The event is guaranteed to be published exactly once, even if the service crashes mid-operation.

csharp
// Save order + outbox event in a single transaction
public async Task PlaceOrderAsync(Order order)
{
    await using var transaction = await _db.Database.BeginTransactionAsync();

    _db.Orders.Add(order);
    _db.OutboxMessages.Add(new OutboxMessage
    {
        Id = Guid.NewGuid(),
        Type = "order.placed",
        Payload = JsonSerializer.Serialize(new OrderPlacedEvent(order)),
        CreatedAt = DateTimeOffset.UtcNow,
        PublishedAt = null  // null = not yet published
    });

    await _db.SaveChangesAsync();
    await transaction.CommitAsync();
    // Background relay will pick up the outbox message and publish to Service Bus
}

Avoiding Common Pitfalls

Microservices introduce a class of problems that do not exist in a monolith. Teams that are not prepared for them end up with something worse than what they started with: a distributed monolith that is hard to deploy, hard to debug, and has all the operational complexity of microservices without the independence.

The distributed monolith anti-pattern

The most common microservices failure mode: services that are technically separate deployables but are tightly coupled at runtime. Service A calls Service B synchronously, which calls Service C, which queries Service A's database directly. The result: you cannot deploy A without also deploying B and C, latency is additive, and a single slow service degrades everything.

Signs you have a distributed monolith: services share a database, services cannot be deployed independently, a single business operation requires synchronous calls across 4+ services, and your integration test suite takes 40 minutes.

Handling distributed transactions with the Saga pattern

In a monolith, a database transaction guarantees atomicity: either all steps succeed or all are rolled back. In microservices, there is no cross-service transaction. Use the Saga pattern instead: define a sequence of local transactions, each publishing an event that triggers the next. If a step fails, compensating transactions undo the previous steps.

  • Choreography-based saga — each service listens for events and reacts. Simple but hard to follow the overall flow.
  • Orchestration-based saga — a central orchestrator (Azure Durable Functions works well here) coordinates the steps and handles compensations. Easier to reason about and debug.
  • Use Azure Durable Functions for the orchestrator: they are stateful, handle retries automatically, and the workflow code reads linearly despite being async and distributed.

NGINX Ingress with rate limiting and TLS

Expose your microservices through a single ingress controller — do not give each service its own public endpoint. NGINX Ingress on AKS handles TLS termination (cert-manager + Let's Encrypt or Azure Key Vault certificates), path-based routing, rate limiting, and CORS in one place.

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
  annotations:
    nginx.ingress.kubernetes.io/rate-limit: "100"
    nginx.ingress.kubernetes.io/rate-limit-window: "1m"
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts: [api.myapp.com]
      secretName: api-tls
  rules:
    - host: api.myapp.com
      http:
        paths:
          - path: /orders
            pathType: Prefix
            backend:
              service:
                name: order-service
                port: { number: 80 }
          - path: /payments
            pathType: Prefix
            backend:
              service:
                name: payment-service
                port: { number: 80 }

Want us to design your microservices architecture?

We help teams define service boundaries, design event-driven communication, and deploy production-grade microservices on AKS.

Schedule a call

Closing Thoughts

Microservices done right unlock independent deployability, targeted scaling, and genuine team autonomy. But the architecture only delivers on that promise when service boundaries are well-drawn, data is isolated, communication is asynchronous where possible, and observability is built in from the start.

Start with the Strangler Fig pattern if you are decomposing a monolith. Use AKS as your runtime, Service Bus for async messaging, and the Outbox Pattern to guarantee event delivery. Instrument everything with OpenTelemetry from day one — you will be glad you did when you are debugging a production incident at midnight.

More articles

View all
Azure Cost Optimisation: Cut Your Cloud Bill by 40%
about 1 year ago1 min read

Azure Cost Optimisation: Cut Your Cloud Bill by 40%

Cloud costs have a habit of growing faster than the business value they deliver. In our experience working with Azure customers across industries, most organisations have between 25% and 45% immediate savings available without any impact on performance or reliability. In this article, we walk through the most impactful cost reduction techniques: right-sizing virtual machines using Azure Advisor recommendations, converting pay-as-you-go workloads to Reserved Instances or Savings Plans, enabling auto-shutdown for non-production environments, replacing always-on VMs with Azure Container Apps or Functions for batch workloads, and deleting orphaned resources like unused disks and public IPs. We also show how to set up cost alerts and budgets in Azure Cost Management so that surprises are caught early, before they appear on the invoice.

Read article
Azure Infrastructure as Code (IaC) Guide: 10 Best Practices
11 months ago1 min read

Azure Infrastructure as Code (IaC) Guide: 10 Best Practices

Are you still deploying Azure resources manually in the Azure Portal? What starts as a quick setup often turns into inconsistencies across environments, undocumented changes, and errors that are hard to trace. The solution is Infrastructure as Code. This guide covers what IaC is, its benefits, how it works in Azure, the best tools (Bicep, ARM, Terraform, Pulumi), and 10 best practices to get you started.

Read article
Getting Started with Azure OpenAI Service
about 1 year ago1 min read

Getting Started with Azure OpenAI Service

Azure OpenAI Service brings powerful large language models — including GPT-4o, GPT-4 Turbo, and Embeddings — directly into your Azure environment, giving you enterprise-grade security, compliance, and regional data residency. In this guide, we walk through provisioning your first Azure OpenAI resource, deploying a model, and making your first API call from a .NET or Python application. We also cover key concepts like token limits, system prompts, temperature settings, and how to structure effective prompts for consistent results. Whether you are building a customer support chatbot, a document summarisation tool, or an internal knowledge assistant, this article gives you a solid foundation to start shipping AI features with confidence.

Read article