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
- Decoupling: Frontend and backend processing are completely independent
- Reliability: Messages persist in Service Bus if processing fails
- Scalability: Multiple consumers can process messages in parallel
- Resilience: Failed messages automatically retry or move to dead letter queue
- 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
- Resource Group - Container for all resources
- Service Bus Namespace (Standard tier) with Queue
- Application Insights + Log Analytics Workspace
- Storage Account for Azure Functions
- Function App with pre-configured settings
- 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: namespacename: queueNameproperties: {maxDeliveryCount: 10lockDuration: 'PT5M' // 5 minutesdefaultMessageTimeToLive: 'P14D' // 14 daysdeadLetteringOnMessageExpiration: trueenablePartitioning: falseduplicateDetectionHistoryTimeWindow: 'PT10M'}}
Deploying Infrastructure
cd infrastructure# Deploy to developmentaz 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 Zodconst validatedOrder = OrderSchema.parse(body);// Add order ID and timestampconst order = {...validatedOrder,orderId: uuidv4(),orderDate: new Date().toISOString(),status: 'submitted'};// Publish to Service Busconst 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 successappInsights.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 InsightsappInsights.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.tsexport 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 submittedappInsights.defaultClient.trackEvent({name: 'OrderSubmitted',properties: {orderId: order.orderId,customerId: order.customerId,itemCount: order.items.length,totalAmount: order.totalAmount}});// Order processedappInsights.defaultClient.trackEvent({name: 'OrderProcessed',properties: {orderId: order.orderId,processingDuration: duration}});// Processing failedappInsights.defaultClient.trackException({exception: error,properties: {orderId: order.orderId,errorType: error.name}});
Key Metrics Dashboard
Using KQL (Kusto Query Language) to analyze data:
// Order submission ratecustomEvents| where name == "OrderSubmitted"| summarize count() by bin(timestamp, 1h)| render timechart// Average processing timecustomEvents| where name == "OrderProcessed"| extend duration = todouble(customProperties.processingDuration)| summarize avg(duration) by bin(timestamp, 5m)// Error rateexceptions| 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 Infrastructureon:push:branches: [main, master]paths:- 'infrastructure/**'workflow_dispatch:inputs:environment:type: choiceoptions: [dev, staging, prod]jobs:deploy:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- name: Azure Loginuses: azure/login@v1with:creds: ${{ secrets.AZURE_CREDENTIALS }}- name: Deploy Biceprun: |az deployment sub create \--location westeurope \--template-file infrastructure/main.bicep \--parameters infrastructure/parameters/${{ inputs.environment }}.bicepparam
Backend Deployment Workflow
name: Deploy Backendon:push:branches: [main, master]paths:- 'backend/**'jobs:deploy:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- name: Setup Node.jsuses: actions/setup-node@v4with:node-version: '20'- name: Build Functionsworking-directory: backend/functionsrun: |npm cinpm run build- name: Deploy to Azure Functionsrun: |func azure functionapp publish ${{ env.FUNCTION_APP_NAME }}
Part 6: Error Handling & Resilience
Retry Strategy
Service Bus automatically retries failed messages:
- Initial attempt - Message delivered to function
- Retry on failure - Up to 10 attempts with exponential backoff
- Dead letter queue - After max retries, message moves to DLQ
- 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 HTTPconst 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 processingawait 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 parallelawait 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 authenticationapp.http('OrderSubmit', {authLevel: 'function', // Requires function keyhandler: async (request, context) => {// Verify custom JWT tokenconst 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 Vaultaz keyvault secret set \--vault-name kv-orderproc \--name ServiceBusConnectionString \--value "<connection-string>"# Reference in Function Appaz 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 repositorygit clone https://github.com/yourusername/event-driven-order-processing-systemcd event-driven-order-processing-system# Deploy everything to development./scripts/deploy-local.sh dev
The script will:
- Deploy infrastructure (5-7 minutes)
- Build and deploy backend functions (2-3 minutes)
- Deploy frontend (if configured)
- Display all URLs and connection strings
Verify Deployment
# Check health endpointcurl https://func-orderproc-dev-<suffix>.azurewebsites.net/api/health# Submit test ordercurl -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
- Bicep for IaC: Declarative, type-safe, and Azure-native
- Service Bus: Reliable message delivery with minimal configuration
- Consumption Plan: Cost-effective for variable workloads
- TypeScript: Type safety across frontend and backend
- 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
- Source Code: GitHub Repository
- Azure Documentation:



