Insight AX
Back to Blog

Enterprise Integration Patterns for Modern ERP Systems

20 November 20253 min read
integrationerpazuredesign-patternsD365 F&O

Enterprise integration has evolved significantly over the past decade. Modern ERP systems like Dynamics 365 Finance & Operations demand integration approaches that are resilient, scalable, and maintainable. Having implemented these patterns across several D365 projects, here are the three I find most valuable.

The Challenge

Connecting an ERP system to cloud-native services introduces several challenges:

  • Data consistency across distributed systems
  • Latency tolerance for real-time vs batch processing
  • Error handling when upstream systems are unavailable
  • Schema evolution as business requirements change

Pattern 1: Event-Driven Integration

Rather than polling for changes, subscribe to business events published by the ERP system. D365 F&O supports this natively through Business Events — the platform publishes events such as sales order confirmations, invoice postings, and payment completions that external systems can subscribe to via Azure Service Bus, Event Grid, or direct HTTPS endpoints.

This reduces load on both systems and enables near-real-time data flow.

public class BusinessEventHandler : IBusinessEventHandler
{
    private readonly ILogger<BusinessEventHandler> _logger;
    private readonly IServiceProvider _serviceProvider;

    public async Task HandleAsync(BusinessEvent businessEvent)
    {
        var handler = businessEvent.EventType switch
        {
            "SalesOrder.Confirmed" => _serviceProvider.GetService<SalesOrderConfirmedHandler>(),
            "Invoice.Posted" => _serviceProvider.GetService<InvoicePostedHandler>(),
            "Payment.Completed" => _serviceProvider.GetService<PaymentCompletedHandler>(),
            _ => null
        };

        if (handler is null)
        {
            _logger.LogWarning("No handler registered for {EventType}", businessEvent.EventType);
            return;
        }

        await handler.ProcessAsync(businessEvent.Payload, businessEvent.CorrelationId);
    }
}

In practice, an Azure Function with a Service Bus trigger is a natural fit for consuming D365 Business Events — it scales automatically, handles retries, and integrates with Azure Monitor for observability.

Pattern 2: Outbox Pattern for Reliable Messaging

When you need guaranteed delivery, use the transactional outbox pattern. Write messages to an outbox table within the same database transaction as your business data, then publish them asynchronously. This ensures that messages are never lost, even if the message broker is temporarily unavailable.

This pattern is particularly relevant for D365 integrations where you are writing to an external database (e.g. via the Data Management Framework or a custom service) and need to guarantee that a downstream notification is sent for every record processed.

public class OutboxPublisher
{
    private readonly DbContext _dbContext;
    private readonly ServiceBusClient _serviceBusClient;
    private readonly ILogger<OutboxPublisher> _logger;

    public async Task ProcessOutboxAsync(CancellationToken cancellationToken)
    {
        var pendingMessages = await _dbContext.OutboxMessages
            .Where(m => m.Status == OutboxStatus.Pending)
            .OrderBy(m => m.CreatedAt)
            .Take(50)
            .ToListAsync(cancellationToken);

        var sender = _serviceBusClient.CreateSender("erp-integration-events");

        foreach (var message in pendingMessages)
        {
            var sbMessage = new ServiceBusMessage(message.Payload)
            {
                MessageId = message.Id.ToString(),
                CorrelationId = message.CorrelationId,
                Subject = message.EventType
            };

            await sender.SendMessageAsync(sbMessage, cancellationToken);
            message.Status = OutboxStatus.Published;
            message.PublishedAt = DateTimeOffset.UtcNow;
        }

        await _dbContext.SaveChangesAsync(cancellationToken);
    }
}

A scheduled Azure Function or Durable Function timer can poll the outbox table at short intervals, providing reliable delivery without adding complexity to the main transaction path.

Pattern 3: Circuit Breaker

Protect your integration layer from cascading failures by implementing circuit breakers around external service calls. This is essential when calling D365 F&O OData or custom service endpoints from external systems — if the ERP is undergoing a service update or experiencing throttling, you want to fail fast rather than queue up timeouts.

var circuitBreakerPolicy = Policy
    .Handle<HttpRequestException>()
    .OrResult<HttpResponseMessage>(r => r.StatusCode == HttpStatusCode.TooManyRequests)
    .CircuitBreakerAsync(
        handledEventsAllowedBeforeBreaking: 5,
        durationOfBreak: TimeSpan.FromSeconds(30),
        onBreak: (outcome, duration) =>
            logger.LogWarning("Circuit opened for {Duration}s — ERP endpoint unavailable", duration.TotalSeconds),
        onReset: () =>
            logger.LogInformation("Circuit closed — ERP endpoint recovered")
    );

var response = await circuitBreakerPolicy.ExecuteAsync(() =>
    httpClient.PostAsJsonAsync("/api/services/SalesOrderService/create", orderData)
);

The example above uses Polly, which integrates natively with HttpClientFactory in .NET — making it straightforward to apply across all outbound ERP calls. Note the explicit handling of 429 Too Many Requests, which is the response D365 F&O returns when API throttling is active.

Key Takeaways

  1. Prefer event-driven patterns over polling — D365 Business Events and Azure Service Bus provide a native foundation for this
  2. Use the outbox pattern when message delivery must be guaranteed, particularly for cross-system data synchronisation
  3. Always implement circuit breakers around external service calls, with explicit handling for ERP throttling responses
  4. Monitor integration health with distributed tracing — Application Insights provides end-to-end correlation across Azure Functions, Service Bus, and D365

These patterns have proven effective in production environments handling hundreds of thousands of transactions daily across multiple D365 legal entities.