Deep Dive

Enterprise Integration Platform: Azure API Management + Logic Apps + Service Bus + Event Grid

Enterprise Integration Platform: Azure API Management + Logic Apps + Service Bus + Event Grid

Introduction: The Integration Challenge

The average enterprise uses 200+ SaaS applications, maintains legacy on-premises systems, and builds custom solutions — all of which need to exchange data reliably. Point-to-point integrations create brittle spaghetti architectures. This deep dive builds a centralized integration platform on Azure that manages APIs, orchestrates complex workflows, reliably delivers messages, and reacts to events across the entire organization. The platform handles 50M+ API calls daily with 99.99% availability.

Enterprise Integration Platform

Prerequisites

  • Azure subscription with Contributor access
  • Azure API Management (Premium tier for multi-region)
  • Azure Logic Apps (Standard for stateful workflows)
  • Azure Service Bus (Premium for mission-critical messaging)
  • Azure Event Grid (with system and custom topics)
  • Familiarity with REST APIs, OAuth 2.0, and messaging patterns

Phase 1: API Management Gateway

APIM Infrastructure with Bicep

param location string = resourceGroup().location

resource apim 'Microsoft.ApiManagement/service@2023-09-01-preview' = {
  name: 'apim-integration-prod'
  location: location
  sku: {
    name: 'Premium'
    capacity: 2
  }
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    publisherEmail: 'api-team@contoso.com'
    publisherName: 'Contoso Integration Platform'
    virtualNetworkType: 'Internal'
    virtualNetworkConfiguration: {
      subnetResourceId: apimSubnet.id
    }
    certificates: [
      {
        storeName: 'Root'
        encodedCertificate: loadFileAsBase64('certs/root-ca.pfx')
      }
    ]
    customProperties: {
      'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls11': 'false'
      'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls10': 'false'
      'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls11': 'false'
      'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls10': 'false'
      'Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Ciphers.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA': 'false'
    }
  }
}

// API versioning and revision management
resource ordersApi 'Microsoft.ApiManagement/service/apis@2023-09-01-preview' = {
  parent: apim
  name: 'orders-api'
  properties: {
    displayName: 'Orders API'
    path: 'orders'
    protocols: ['https']
    apiVersion: 'v2'
    apiVersionSetId: ordersVersionSet.id
    subscriptionRequired: true
    serviceUrl: 'https://orders-backend.azurewebsites.net'
    authenticationSettings: {
      oAuth2AuthenticationSettings: [
        {
          authorizationServerId: 'entra-id-oauth'
          scope: 'api://orders-api/.default'
        }
      ]
    }
  }
}

resource ordersVersionSet 'Microsoft.ApiManagement/service/apiVersionSets@2023-09-01-preview' = {
  parent: apim
  name: 'orders-version-set'
  properties: {
    displayName: 'Orders API'
    versioningScheme: 'Header'
    versionHeaderName: 'Api-Version'
  }
}

Advanced API Policies

<!-- Global policy: Rate limiting, JWT validation, logging -->
<policies>
    <inbound>
        <!-- Validate JWT token from Entra ID -->
        <validate-azure-ad-token tenant-id="{{tenantId}}" 
            header-name="Authorization" 
            failed-validation-httpcode="401"
            failed-validation-error-message="Unauthorized">
            <client-application-ids>
                <application-id>{{clientAppId1}}</application-id>
                <application-id>{{clientAppId2}}</application-id>
            </client-application-ids>
            <required-claims>
                <claim name="roles" match="any">
                    <value>Orders.Read</value>
                    <value>Orders.Write</value>
                </claim>
            </required-claims>
        </validate-azure-ad-token>

        <!-- Rate limiting by subscription -->
        <rate-limit-by-key calls="100" 
            renewal-period="60" 
            counter-key="@(context.Subscription?.Key ?? context.Request.IpAddress)" 
            increment-condition="@(context.Response.StatusCode >= 200 && context.Response.StatusCode < 400)" />
        
        <!-- Quota by product tier -->
        <quota-by-key calls="10000" 
            bandwidth="50000" 
            renewal-period="86400" 
            counter-key="@(context.Subscription.Key)" />

        <!-- Request correlation -->
        <set-header name="X-Correlation-Id" exists-action="skip">
            <value>@(Guid.NewGuid().ToString())</value>
        </set-header>

        <!-- Cache responses for GET requests -->
        <cache-lookup vary-by-developer="false" 
            vary-by-developer-groups="false"
            caching-type="prefer-external"
            downstream-caching-type="none">
            <vary-by-header>Accept</vary-by-header>
            <vary-by-header>Api-Version</vary-by-header>
        </cache-lookup>

        <!-- Request transformation -->
        <set-body>@{
            var body = context.Request.Body.As<JObject>();
            body["_processedAt"] = DateTime.UtcNow.ToString("o");
            body["_apiVersion"] = context.Api.Version;
            body["_correlationId"] = context.Request.Headers
                .GetValueOrDefault("X-Correlation-Id", "");
            return body.ToString();
        }</set-body>
    </inbound>
    <backend>
        <!-- Circuit breaker pattern -->
        <retry condition="@(context.Response.StatusCode >= 500)" 
            count="3" interval="1" 
            max-interval="10" 
            delta="2" 
            first-fast-retry="true">
            <forward-request buffer-request-body="true" 
                timeout="30" 
                follow-redirects="false" />
        </retry>
    </backend>
    <outbound>
        <!-- Cache store -->
        <cache-store duration="300" />

        <!-- Response transformation: Remove internal headers -->
        <set-header name="X-Powered-By" exists-action="delete" />
        <set-header name="X-AspNet-Version" exists-action="delete" />
        <set-header name="Server" exists-action="delete" />

        <!-- Add security headers -->
        <set-header name="Strict-Transport-Security" exists-action="override">
            <value>max-age=31536000; includeSubDomains</value>
        </set-header>
        <set-header name="X-Content-Type-Options" exists-action="override">
            <value>nosniff</value>
        </set-header>
    </outbound>
    <on-error>
        <!-- Structured error response -->
        <set-body>@{
            var error = new JObject();
            error["error"] = new JObject();
            error["error"]["code"] = context.Response.StatusCode.ToString();
            error["error"]["message"] = context.LastError?.Message ?? "An unexpected error occurred";
            error["error"]["correlationId"] = context.Request.Headers
                .GetValueOrDefault("X-Correlation-Id", "unknown");
            error["error"]["timestamp"] = DateTime.UtcNow.ToString("o");
            return error.ToString();
        }</set-body>

        <!-- Log to Application Insights -->
        <trace source="APIM-Error" severity="error">
            <message>@($"API Error: {context.LastError?.Message}")</message>
            <metadata name="correlationId" 
                value="@(context.Request.Headers.GetValueOrDefault("X-Correlation-Id",""))" />
            <metadata name="statusCode" value="@(context.Response.StatusCode.ToString())" />
        </trace>
    </on-error>
</policies>

API Management Gateway

Phase 2: Logic Apps Workflow Orchestration

Order-to-Cash Integration Workflow

{
  "definition": {
    "$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
    "triggers": {
      "When_new_order_received": {
        "type": "ServiceBus",
        "inputs": {
          "host": {
            "connection": { "name": "@parameters('$connections')['servicebus']['connectionId']" }
          },
          "method": "peeklock",
          "path": "/@{encodeURIComponent('new-orders')}/messages/head",
          "queries": { "sessionId": "None" }
        },
        "recurrence": { "frequency": "Second", "interval": 30 }
      }
    },
    "actions": {
      "Parse_order": {
        "type": "ParseJson",
        "inputs": {
          "content": "@{base64ToString(triggerBody()?['ContentData'])}",
          "schema": {
            "type": "object",
            "properties": {
              "orderId": { "type": "string" },
              "customerId": { "type": "string" },
              "items": { "type": "array" },
              "totalAmount": { "type": "number" },
              "currency": { "type": "string" }
            }
          }
        }
      },
      "Validate_customer_in_CRM": {
        "type": "ApiConnection",
        "runAfter": { "Parse_order": ["Succeeded"] },
        "inputs": {
          "host": { "connection": { "name": "@parameters('$connections')['dynamicscrm']['connectionId']" } },
          "method": "get",
          "path": "/v9.2/accounts",
          "queries": {
            "$filter": "accountnumber eq '@{body('Parse_order')?['customerId']}'",
            "$select": "accountid,name,creditlimit,creditonhold"
          }
        }
      },
      "Check_credit_limit": {
        "type": "If",
        "runAfter": { "Validate_customer_in_CRM": ["Succeeded"] },
        "expression": {
          "and": [
            { "equals": ["@body('Validate_customer_in_CRM')?['value']?[0]?['creditonhold']", false] },
            { "lessOrEquals": ["@body('Parse_order')?['totalAmount']", "@body('Validate_customer_in_CRM')?['value']?[0]?['creditlimit']"] }
          ]
        },
        "actions": {
          "Check_inventory_SAP": {
            "type": "Http",
            "inputs": {
              "method": "POST",
              "uri": "https://sap-gateway.contoso.com/sap/opu/odata/sap/ZMM_INVENTORY_SRV/CheckAvailability",
              "headers": {
                "Content-Type": "application/json",
                "Authorization": "Bearer @{body('Get_SAP_token')?['access_token']}"
              },
              "body": {
                "Items": "@body('Parse_order')?['items']",
                "Plant": "1000",
                "StorageLocation": "0001"
              }
            }
          },
          "Create_invoice_in_QuickBooks": {
            "type": "ApiConnection",
            "runAfter": { "Check_inventory_SAP": ["Succeeded"] },
            "inputs": {
              "host": { "connection": { "name": "@parameters('$connections')['quickbooks']['connectionId']" } },
              "method": "post",
              "path": "/v3/company/@{encodeURIComponent(parameters('qbCompanyId'))}/invoice",
              "body": {
                "CustomerRef": { "value": "@{body('Parse_order')?['customerId']}" },
                "Line": "@body('Parse_order')?['items']",
                "TotalAmt": "@body('Parse_order')?['totalAmount']",
                "CurrencyRef": { "value": "@body('Parse_order')?['currency']" }
              }
            }
          },
          "Send_order_confirmation": {
            "type": "ServiceBus",
            "runAfter": { "Create_invoice_in_QuickBooks": ["Succeeded"] },
            "inputs": {
              "host": { "connection": { "name": "@parameters('$connections')['servicebus']['connectionId']" } },
              "method": "send",
              "path": "/@{encodeURIComponent('order-confirmations')}/messages",
              "body": {
                "ContentData": "@{base64(json(concat('{\"orderId\":\"', body('Parse_order')?['orderId'], '\",\"status\":\"confirmed\",\"invoiceId\":\"', body('Create_invoice_in_QuickBooks')?['Id'], '\"}')))}",
                "ContentType": "application/json",
                "Label": "order-confirmed"
              }
            }
          }
        },
        "else": {
          "actions": {
            "Reject_order_credit_hold": {
              "type": "ServiceBus",
              "inputs": {
                "host": { "connection": { "name": "@parameters('$connections')['servicebus']['connectionId']" } },
                "method": "send",
                "path": "/@{encodeURIComponent('order-rejections')}/messages",
                "body": {
                  "ContentData": "@{base64(json(concat('{\"orderId\":\"', body('Parse_order')?['orderId'], '\",\"reason\":\"Credit limit exceeded or account on hold\"}')))}",
                  "ContentType": "application/json"
                }
              }
            }
          }
        }
      }
    }
  }
}

Phase 3: Service Bus Messaging Patterns

Topic/Subscription with Filters for Event Fan-Out

using Azure.Messaging.ServiceBus.Administration;

public class ServiceBusTopologyManager
{
    private readonly ServiceBusAdministrationClient _adminClient;

    public async Task CreateOrderProcessingTopologyAsync()
    {
        // Create topic for order events
        await _adminClient.CreateTopicAsync(new CreateTopicOptions("order-events")
        {
            MaxSizeInMegabytes = 5120,
            DefaultMessageTimeToLive = TimeSpan.FromDays(14),
            EnablePartitioning = true,
            RequiresDuplicateDetection = true,
            DuplicateDetectionHistoryTimeWindow = TimeSpan.FromMinutes(10)
        });

        // Subscription: Billing system (only confirmed orders > $1000)
        await _adminClient.CreateSubscriptionAsync(
            new CreateSubscriptionOptions("order-events", "billing-high-value")
            {
                MaxDeliveryCount = 10,
                LockDuration = TimeSpan.FromMinutes(5),
                DeadLetteringOnMessageExpiration = true,
                DefaultMessageTimeToLive = TimeSpan.FromDays(7)
            });

        await _adminClient.CreateRuleAsync("order-events", "billing-high-value",
            new CreateRuleOptions("HighValueOrders", new SqlRuleFilter(
                "status = 'confirmed' AND totalAmount > 1000"
            )));

        // Subscription: Warehouse (all confirmed orders)
        await _adminClient.CreateSubscriptionAsync(
            new CreateSubscriptionOptions("order-events", "warehouse-fulfillment")
            {
                MaxDeliveryCount = 15,
                LockDuration = TimeSpan.FromMinutes(2)
            });

        await _adminClient.CreateRuleAsync("order-events", "warehouse-fulfillment",
            new CreateRuleOptions("ConfirmedOrders", new SqlRuleFilter(
                "status = 'confirmed'"
            )));

        // Subscription: Analytics (all events, no filtering)
        await _adminClient.CreateSubscriptionAsync(
            new CreateSubscriptionOptions("order-events", "analytics-all")
            {
                MaxDeliveryCount = 3,
                DefaultMessageTimeToLive = TimeSpan.FromDays(1)
            });

        // Subscription: Notification service (with action to add routing info)
        await _adminClient.CreateSubscriptionAsync(
            new CreateSubscriptionOptions("order-events", "notifications")
            {
                MaxDeliveryCount = 5
            });

        await _adminClient.CreateRuleAsync("order-events", "notifications",
            new CreateRuleOptions("AddNotificationRouting",
                new SqlRuleFilter("status IN ('confirmed', 'shipped', 'delivered')"),
                new SqlRuleAction("SET notificationChannel = CASE WHEN totalAmount > 5000 THEN 'sms+email' ELSE 'email' END")
            ));
    }
}

Service Bus Messaging

Phase 4: Event Grid for Reactive Architecture

Custom Event Grid Topics and Event Handlers

resource customTopic 'Microsoft.EventGrid/topics@2024-06-01-preview' = {
  name: 'evgt-business-events'
  location: location
  properties: {
    inputSchema: 'CloudEventSchemaV1_0'
    publicNetworkAccess: 'Disabled'
    inboundIpRules: []
  }
  identity: {
    type: 'SystemAssigned'
  }
}

// Event subscription: Route order events to Logic Apps
resource orderSubscription 'Microsoft.EventGrid/eventSubscriptions@2024-06-01-preview' = {
  scope: customTopic
  name: 'sub-order-processing'
  properties: {
    destination: {
      endpointType: 'AzureFunction'
      properties: {
        resourceId: '${functionApp.id}/functions/ProcessOrderEvent'
        maxEventsPerBatch: 10
        preferredBatchSizeInKilobytes: 64
      }
    }
    filter: {
      subjectBeginsWith: '/orders/'
      includedEventTypes: [
        'Contoso.Orders.Created'
        'Contoso.Orders.Updated'
        'Contoso.Orders.Cancelled'
      ]
      enableAdvancedFilteringOnArrays: true
      advancedFilters: [
        {
          operatorType: 'NumberGreaterThan'
          key: 'data.totalAmount'
          value: 0
        }
      ]
    }
    retryPolicy: {
      maxDeliveryAttempts: 30
      eventTimeToLiveInMinutes: 1440
    }
    deadLetterDestination: {
      endpointType: 'StorageBlob'
      properties: {
        resourceId: storageAccount.id
        blobContainerName: 'deadletter-events'
      }
    }
  }
}

Event Publisher with CloudEvents

using Azure.Messaging.EventGrid;
using CloudNative.CloudEvents;

public class BusinessEventPublisher
{
    private readonly EventGridPublisherClient _client;

    public async Task PublishOrderCreatedAsync(Order order)
    {
        var cloudEvent = new CloudEvent
        {
            Source = new Uri("https://contoso.com/orders"),
            Type = "Contoso.Orders.Created",
            Subject = $"/orders/{order.Id}",
            Data = BinaryData.FromObjectAsJson(new
            {
                orderId = order.Id,
                customerId = order.CustomerId,
                totalAmount = order.TotalAmount,
                currency = order.Currency,
                itemCount = order.Items.Count,
                region = order.ShippingRegion,
                priority = order.TotalAmount > 5000 ? "high" : "standard"
            }),
            DataContentType = "application/json",
            Time = DateTimeOffset.UtcNow
        };

        await _client.SendEventAsync(cloudEvent);
    }
}

Event Grid Architecture

Integration Pattern Decision Guide

Pattern Technology When to Use
Request/Response APIM + Backend Synchronous API calls, real-time queries
Publish/Subscribe Service Bus Topics Fan-out events to multiple consumers
Queue Processing Service Bus Queues Load leveling, reliable async processing
Event Notification Event Grid React to Azure resource changes, lightweight events
Orchestration Logic Apps Multi-step workflows, SaaS-to-SaaS integration
Choreography Event Grid + Functions Loosely coupled microservice coordination
Saga Service Bus + Logic Apps Distributed transactions with compensation

Best Practices

  1. API-first design: Define OpenAPI specs before implementation — APIM imports them directly
  2. Dead letter everything: Every queue, subscription, and event handler needs DLQ configuration
  3. Idempotency is mandatory: Messages will be delivered at-least-once — design handlers accordingly
  4. Use correlation IDs end-to-end: Pass X-Correlation-Id through APIM → Logic Apps → Service Bus → Functions
  5. Separate concerns: APIM for API governance, Service Bus for reliable messaging, Event Grid for reactive events
  6. Monitor integration health: Track message throughput, dead letter counts, and API latency as SLIs

Architecture Decision and Tradeoffs

When designing integrated solutions solutions with Azure + Power Platform, consider these key architectural trade-offs:

Approach Best For Tradeoff
Managed / platform service Rapid delivery, reduced ops burden Less customisation, potential vendor lock-in
Custom / self-hosted Full control, advanced tuning Higher operational overhead and cost

Recommendation: Start with the managed approach for most workloads and move to custom only when specific requirements demand it.

Validation and Versioning

  • Last validated: April 2026
  • Validate examples against your tenant, region, and SKU constraints before production rollout.
  • Keep module, CLI, and SDK versions pinned in automation pipelines and review quarterly.

Security and Governance Considerations

  • Apply least-privilege access using RBAC roles and just-in-time elevation for admin tasks.
  • Store secrets in managed secret stores and avoid embedding credentials in scripts or source files.
  • Enable audit logging, data protection policies, and periodic access reviews for regulated workloads.

Cost and Performance Notes

  • Define budgets and alerts, then monitor usage and cost trends continuously after go-live.
  • Baseline performance with synthetic and real-user checks before and after major changes.
  • Scale resources with measured thresholds and revisit sizing after usage pattern changes.

Official Microsoft References

Public Examples from Official Sources

Key Takeaways

  • An enterprise integration platform requires multiple complementary services, not a single tool
  • API Management provides the governance, security, and observability layer for all API traffic
  • Logic Apps excels at orchestrating multi-step integrations between SaaS and on-premises systems
  • Service Bus ensures reliable message delivery with dead lettering and duplicate detection
  • Event Grid enables reactive architectures that respond to changes across the entire Azure estate

Further Reading

AI Assistant
AI Assistant

Article Assistant

Ask me about this article

AI
Hi! I'm here to help you understand this article. Ask me anything about the content, concepts, or implementation details.