Logo
Building an Event-Driven Order Processing System with Azure Services

Building an Event-Driven Order Processing System with Azure Services

Published on 11/13/2025

Introduction

In modern e-commerce systems, handling order processing reliably and at scale is critical. This article walks through building a production-ready, event-driven order processing system using Azure services, demonstrating cloud-native patterns that ensure reliability, scalability, and observability.

What We're Building

An enterprise-grade order processing system with:

  • Event-driven architecture using Azure Service Bus
  • Serverless backend with Azure Functions (TypeScript)
  • Modern frontend with Next.js and React
  • Complete observability with Application Insights
  • Infrastructure as Code using Bicep
  • Automated CI/CD with GitHub Actions

Tech Stack:

  • Backend: Azure Functions (Node.js), TypeScript
  • Frontend: Next.js, React, TailwindCSS, shadcn/ui
  • Infrastructure: Azure Service Bus, Application Insights, Storage, Bicep
  • Validation: Zod schemas
  • State Management: TanStack Query

Architecture Overview

The system implements the Queue-Based Load Leveling pattern with Competing Consumers, providing:

┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Next.js │─────▶│ Azure Function │─────▶│ Service Bus │
│ Frontend │ HTTP │ (HTTP Trigger) │ │ Queue │
└─────────────────┘ └──────────────────┘ └─────────────────┘
┌──────────────────┐ ┌─────────────────┐
│ Azure Function │◀─────│ Service Bus │
│ (Queue Trigger) │ │ Queue │
└──────────────────┘ └─────────────────┘
┌──────────────────┐
│ Application │
│ Insights │
└──────────────────┘

Key Benefits

  1. Decoupling: Frontend and backend processing are completely independent
  2. Reliability: Messages persist in Service Bus if processing fails
  3. Scalability: Multiple consumers can process messages in parallel
  4. Resilience: Failed messages automatically retry or move to dead letter queue
  5. Performance: Fast user response times with asynchronous processing

Part 1: Infrastructure as Code with Bicep

The infrastructure is defined entirely in Bicep, Azure's domain-specific language for deploying resources.

Resources Deployed

  1. Resource Group - Container for all resources
  2. Service Bus Namespace (Standard tier) with Queue
  3. Application Insights + Log Analytics Workspace
  4. Storage Account for Azure Functions
  5. Function App with pre-configured settings
  6. Static Web App for Next.js frontend

Project Structure

infrastructure/
├── main.bicep # Main orchestration
├── modules/
│ ├── servicebus.bicep # Queue configuration
│ ├── appinsights.bicep # Monitoring setup
│ ├── storage.bicep # Storage account
│ ├── functionapp.bicep # Serverless functions
│ └── staticwebapp.bicep # Frontend hosting
└── parameters/
├── dev.bicepparam # Development config
└── prod.bicepparam # Production config

Service Bus Configuration

The Service Bus queue is configured with production-ready settings:

resource queue 'Microsoft.ServiceBus/namespaces/queues@2021-11-01' = {
parent: namespace
name: queueName
properties: {
maxDeliveryCount: 10
lockDuration: 'PT5M' // 5 minutes
defaultMessageTimeToLive: 'P14D' // 14 days
deadLetteringOnMessageExpiration: true
enablePartitioning: false
duplicateDetectionHistoryTimeWindow: 'PT10M'
}
}

Deploying Infrastructure

cd infrastructure
# Deploy to development
az deployment sub create \
--name orderproc-infra-dev \
--location westeurope \
--template-file main.bicep \
--parameters parameters/dev.bicepparam
# Deployment takes 5-7 minutes

Cost Estimate: ~$11-23/month for development environment using free tiers where available.

Part 2: Backend - Azure Functions

The backend consists of four TypeScript Azure Functions:

1. Order Submit Function (HTTP Trigger)

Receives orders from the frontend and publishes to Service Bus:

app.http('OrderSubmit', {
methods: ['POST'],
authLevel: 'anonymous',
handler: async (request, context) => {
const body = await request.json();
// Validate with Zod
const validatedOrder = OrderSchema.parse(body);
// Add order ID and timestamp
const order = {
...validatedOrder,
orderId: uuidv4(),
orderDate: new Date().toISOString(),
status: 'submitted'
};
// Publish to Service Bus
const sender = serviceBusClient.createSender('orders-queue');
await sender.sendMessages({
body: order,
messageId: order.orderId
});
return {
status: 201,
jsonBody: { orderId: order.orderId }
};
}
});

2. Order Processor Function (Queue Trigger)

Processes orders asynchronously from the queue:

app.serviceBusQueue('OrderProcessor', {
connection: 'ServiceBusConnectionString',
queueName: 'orders-queue',
handler: async (message, context) => {
const order = message as Order;
context.log(`Processing order ${order.orderId}`);
try {
// Simulate processing (inventory check, payment, etc.)
await processOrder(order);
// Track success
appInsights.defaultClient.trackEvent({
name: 'OrderProcessed',
properties: {
orderId: order.orderId,
totalAmount: order.totalAmount
}
});
} catch (error) {
context.error(`Failed to process order: ${error}`);
throw error; // Triggers retry or dead letter
}
}
});

3. Dead Letter Processor Function

Handles messages that failed processing:

app.serviceBusQueue('DeadLetterProcessor', {
connection: 'ServiceBusConnectionString',
queueName: 'orders-queue/$deadletterqueue',
handler: async (message, context) => {
context.error(`Dead letter message: ${JSON.stringify(message)}`);
// Log to Application Insights
appInsights.defaultClient.trackException({
exception: new Error('Order failed after max retries'),
properties: {
orderId: message.orderId,
deliveryCount: message.deliveryCount
}
});
// Could trigger alert, email, or manual review process
}
});

4. Health Check Function

Provides system health status:

app.http('Health', {
methods: ['GET'],
authLevel: 'anonymous',
handler: async (request, context) => {
return {
jsonBody: {
status: 'healthy',
timestamp: new Date().toISOString(),
environment: process.env.ENVIRONMENT,
checks: {
serviceBus: process.env.ServiceBusConnectionString ? 'configured' : 'missing',
appInsights: process.env.APPLICATIONINSIGHTS_CONNECTION_STRING ? 'configured' : 'missing'
}
}
};
}
});

Data Validation with Zod

Type-safe validation ensures data integrity:

import { z } from 'zod';
const OrderItemSchema = z.object({
productId: z.string().uuid(),
productName: z.string().min(1),
quantity: z.number().int().positive(),
price: z.number().positive()
});
const OrderSchema = z.object({
customerId: z.string().min(1),
customerEmail: z.string().email(),
items: z.array(OrderItemSchema).min(1),
totalAmount: z.number().positive(),
currency: z.string().length(3), // ISO 4217
});

Part 3: Frontend - Next.js Application

Modern React application with App Router and TypeScript.

Features

  • Server Components for optimal performance
  • React Hook Form with Zod validation
  • TanStack Query for data fetching
  • shadcn/ui components
  • Dark mode support with next-themes

Order Form Component

'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMutation } from '@tanstack/react-query';
import { OrderSchema } from '@/lib/schemas';
export function OrderForm() {
const form = useForm({
resolver: zodResolver(OrderSchema),
defaultValues: {
customerEmail: '',
items: [{ productName: '', quantity: 1, price: 0 }]
}
});
const mutation = useMutation({
mutationFn: async (data) => {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/orders`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (!response.ok) throw new Error('Failed to submit order');
return response.json();
},
onSuccess: (data) => {
toast.success(`Order submitted! ID: ${data.orderId}`);
form.reset();
}
});
return (
<form onSubmit={form.handleSubmit((data) => mutation.mutate(data))}>
{/* Form fields */}
</form>
);
}

API Integration Layer

// lib/api.ts
export class OrdersAPI {
private baseUrl: string;
constructor() {
this.baseUrl = process.env.NEXT_PUBLIC_API_URL || '';
}
async submitOrder(order: Order) {
const response = await fetch(`${this.baseUrl}/api/orders`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(order)
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
return response.json();
}
async checkHealth() {
const response = await fetch(`${this.baseUrl}/api/health`);
return response.json();
}
}

Part 4: Observability with Application Insights

Comprehensive monitoring and diagnostics built-in.

Custom Events Tracked

// Order submitted
appInsights.defaultClient.trackEvent({
name: 'OrderSubmitted',
properties: {
orderId: order.orderId,
customerId: order.customerId,
itemCount: order.items.length,
totalAmount: order.totalAmount
}
});
// Order processed
appInsights.defaultClient.trackEvent({
name: 'OrderProcessed',
properties: {
orderId: order.orderId,
processingDuration: duration
}
});
// Processing failed
appInsights.defaultClient.trackException({
exception: error,
properties: {
orderId: order.orderId,
errorType: error.name
}
});

Key Metrics Dashboard

Using KQL (Kusto Query Language) to analyze data:

// Order submission rate
customEvents
| where name == "OrderSubmitted"
| summarize count() by bin(timestamp, 1h)
| render timechart
// Average processing time
customEvents
| where name == "OrderProcessed"
| extend duration = todouble(customProperties.processingDuration)
| summarize avg(duration) by bin(timestamp, 5m)
// Error rate
exceptions
| where operation_Name contains "OrderProcessor"
| summarize errorCount = count() by bin(timestamp, 5m)

Alerts Configuration

Set up proactive monitoring:

  • High Error Rate: Alert when error rate > 5%
  • Slow Processing: Alert when avg processing time > 30 seconds
  • Dead Letter Queue: Alert on any messages in DLQ
  • Queue Depth: Alert when queue depth > 1000 messages

Part 5: CI/CD with GitHub Actions

Automated deployment pipelines for infrastructure and applications.

Infrastructure Deployment Workflow

name: Deploy Infrastructure
on:
push:
branches: [main, master]
paths:
- 'infrastructure/**'
workflow_dispatch:
inputs:
environment:
type: choice
options: [dev, staging, prod]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Deploy Bicep
run: |
az deployment sub create \
--location westeurope \
--template-file infrastructure/main.bicep \
--parameters infrastructure/parameters/${{ inputs.environment }}.bicepparam

Backend Deployment Workflow

name: Deploy Backend
on:
push:
branches: [main, master]
paths:
- 'backend/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Build Functions
working-directory: backend/functions
run: |
npm ci
npm run build
- name: Deploy to Azure Functions
run: |
func azure functionapp publish ${{ env.FUNCTION_APP_NAME }}

Part 6: Error Handling & Resilience

Retry Strategy

Service Bus automatically retries failed messages:

  1. Initial attempt - Message delivered to function
  2. Retry on failure - Up to 10 attempts with exponential backoff
  3. Dead letter queue - After max retries, message moves to DLQ
  4. DLQ processor - Separate function monitors and logs failures

Error Types Handled

Transient Errors (automatic retry):

  • Network timeouts
  • Service unavailability
  • Rate limiting

Permanent Errors (move to DLQ):

  • Invalid data format
  • Business rule violations
  • External service failures

Circuit Breaker Pattern

For external service calls:

class CircuitBreaker {
private failures = 0;
private lastFailureTime?: Date;
private state: 'closed' | 'open' | 'half-open' = 'closed';
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
if (this.shouldAttemptReset()) {
this.state = 'half-open';
} else {
throw new Error('Circuit breaker is open');
}
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failures = 0;
this.state = 'closed';
}
private onFailure() {
this.failures++;
this.lastFailureTime = new Date();
if (this.failures >= 5) {
this.state = 'open';
}
}
private shouldAttemptReset(): boolean {
if (!this.lastFailureTime) return false;
const elapsed = Date.now() - this.lastFailureTime.getTime();
return elapsed > 60000; // 1 minute
}
}

Part 7: Testing Strategy

Unit Tests

describe('OrderProcessor', () => {
it('should process valid order', async () => {
const order = {
orderId: '123',
items: [{ productId: 'p1', quantity: 2, price: 10 }],
totalAmount: 20
};
await processOrder(order);
expect(appInsights.trackEvent).toHaveBeenCalledWith(
expect.objectContaining({
name: 'OrderProcessed'
})
);
});
it('should throw on invalid order', async () => {
const invalidOrder = { items: [] };
await expect(processOrder(invalidOrder))
.rejects.toThrow('Invalid order');
});
});

Integration Tests

describe('End-to-End Order Flow', () => {
it('should process order from submission to completion', async () => {
// Submit order via HTTP
const response = await fetch(`${API_URL}/api/orders`, {
method: 'POST',
body: JSON.stringify(validOrder)
});
expect(response.status).toBe(201);
const { orderId } = await response.json();
// Wait for processing
await waitFor(() => {
const logs = getApplicationInsightsLogs();
return logs.some(log =>
log.name === 'OrderProcessed' &&
log.properties.orderId === orderId
);
}, { timeout: 30000 });
});
});

Part 8: Performance Optimization

Function App Settings

{
"FUNCTIONS_WORKER_RUNTIME": "node",
"FUNCTIONS_WORKER_PROCESS_COUNT": "3",
"WEBSITE_RUN_FROM_PACKAGE": "1",
"WEBSITE_NODE_DEFAULT_VERSION": "~20",
"FUNCTIONS_EXTENSION_VERSION": "~4"
}

Service Bus Batching

Process multiple messages efficiently:

app.serviceBusQueue('OrderProcessorBatch', {
connection: 'ServiceBusConnectionString',
queueName: 'orders-queue',
cardinality: 'many',
maxMessageBatchSize: 100,
handler: async (messages, context) => {
// Process messages in parallel
await Promise.all(
messages.map(message => processOrder(message))
);
}
});

Frontend Performance

  • Static generation for landing pages
  • Server components for data fetching
  • Optimistic updates with TanStack Query
  • Code splitting with dynamic imports
  • Image optimization with Next.js Image

Part 9: Security Best Practices

Authentication & Authorization

// Function-level authentication
app.http('OrderSubmit', {
authLevel: 'function', // Requires function key
handler: async (request, context) => {
// Verify custom JWT token
const token = request.headers.get('Authorization');
const user = await verifyToken(token);
// Process with user context
}
});

Data Validation

  • Input validation with Zod schemas
  • Output sanitization to prevent XSS
  • SQL injection prevention (if using database)
  • Rate limiting on API endpoints

Secrets Management

# Store in Azure Key Vault
az keyvault secret set \
--vault-name kv-orderproc \
--name ServiceBusConnectionString \
--value "<connection-string>"
# Reference in Function App
az functionapp config appsettings set \
--name func-orderproc \
--settings ServiceBusConnectionString="@Microsoft.KeyVault(SecretUri=https://kv-orderproc.vault.azure.net/secrets/ServiceBusConnectionString/)"

Network Security

  • HTTPS only enforced on all services
  • CORS configured to allow specific origins
  • Private endpoints for production (optional)
  • Managed Identity for service-to-service auth

Deployment

One-Command Deployment

# Clone the repository
git clone https://github.com/yourusername/event-driven-order-processing-system
cd event-driven-order-processing-system
# Deploy everything to development
./scripts/deploy-local.sh dev

The script will:

  1. Deploy infrastructure (5-7 minutes)
  2. Build and deploy backend functions (2-3 minutes)
  3. Deploy frontend (if configured)
  4. Display all URLs and connection strings

Verify Deployment

# Check health endpoint
curl https://func-orderproc-dev-<suffix>.azurewebsites.net/api/health
# Submit test order
curl -X POST https://func-orderproc-dev-<suffix>.azurewebsites.net/api/orders \
-H "Content-Type: application/json" \
-d '{
"customerId": "test-123",
"customerEmail": "test@example.com",
"items": [{
"productId": "prod-001",
"productName": "Test Product",
"quantity": 2,
"price": 29.99
}],
"totalAmount": 59.98,
"currency": "USD"
}'

Lessons Learned

What Worked Well

  1. Bicep for IaC: Declarative, type-safe, and Azure-native
  2. Service Bus: Reliable message delivery with minimal configuration
  3. Consumption Plan: Cost-effective for variable workloads
  4. TypeScript: Type safety across frontend and backend
  5. Application Insights: Rich telemetry with minimal code

Challenges & Solutions

Cold start latency: Pre-warmed instances, smaller bundle sizes

Message duplicate detection: Enabled duplicate detection on Service Bus

CORS configuration: Explicit origin allowlist, dynamic updates

Secret management: Azure Key Vault integration

Testing Service Bus locally: Azurite emulator for local development

Cost Breakdown

Monthly Costs (Development):

  • Service Bus (Standard): ~$10
  • Function App (Consumption): ~$0-5 (1M executions free)
  • Application Insights: ~$0-5 (5GB free)
  • Storage Account: ~$1
  • Static Web App (Free tier): $0
  • Total: ~$11-21/month

Scaling to Production:

  • Premium Functions for dedicated instances: +$150/month
  • Service Bus Premium for higher throughput: +$672/month
  • Static Web App Standard: +$9/month

Future Enhancements

  • [ ] Add Cosmos DB for order persistence
  • [ ] Implement SignalR for real-time order status updates
  • [ ] Add Azure API Management for rate limiting and API gateway
  • [ ] Implement saga pattern for complex multi-step workflows
  • [ ] Add Azure Front Door for global distribution
  • [ ] Implement feature flags with Azure App Configuration
  • [ ] Add automated load testing with Azure Load Testing

Conclusion

This project demonstrates how to build a production-ready, event-driven system using Azure services. The combination of serverless functions, message queuing, and modern frontend frameworks creates a scalable, maintainable architecture suitable for real-world e-commerce applications.

Key takeaways:

  • Event-driven architecture provides resilience and scalability
  • Infrastructure as Code ensures reproducible deployments
  • Observability from day one simplifies debugging and monitoring
  • TypeScript across the stack improves developer experience
  • Serverless reduces operational overhead and costs

Want a quick overview? Check out this project in my portfolio

Resources