Azure Functions Real World Project
This topic builds a complete, real-world Order Processing System. The project combines HTTP triggers, queue triggers, timer triggers, output bindings, error handling, configuration management, and monitoring into a working application.
Project Overview
┌──────────────────────────────────────────────────────────────┐ │ ORDER PROCESSING SYSTEM │ │ │ │ Customer │ │ │ POST /api/orders (place order) │ │ ▼ │ │ [1] PlaceOrder Function (HTTP Trigger) │ │ │ Validates order, saves to Cosmos DB │ │ │ Puts message on "orders" queue │ │ ▼ │ │ [2] ProcessOrder Function (Queue Trigger) │ │ │ Reads from "orders" queue │ │ │ Charges payment, updates order status │ │ │ Puts message on "notifications" queue │ │ ▼ │ │ [3] SendNotification Function (Queue Trigger) │ │ │ Reads from "notifications" queue │ │ │ Sends confirmation email to customer │ │ ▼ │ │ [4] DailyReport Function (Timer Trigger) │ │ │ Runs at 8:00 AM daily │ │ │ Queries all orders from last 24 hours │ │ │ Saves summary report to Blob Storage │ └──────────────────────────────────────────────────────────────┘
Project Folder Structure
order-processing-app/
├── host.json
├── local.settings.json
├── package.json
├── services/
│ ├── orderService.js ← Order validation and database logic
│ ├── paymentService.js ← Payment processing logic
│ └── emailService.js ← Email sending logic
├── PlaceOrder/
│ ├── function.json ← HTTP trigger
│ └── index.js
├── ProcessOrder/
│ ├── function.json ← Queue trigger (orders queue)
│ └── index.js
├── SendNotification/
│ ├── function.json ← Queue trigger (notifications queue)
│ └── index.js
└── DailyReport/
├── function.json ← Timer trigger (daily at 8 AM)
└── index.js
Function 1 – PlaceOrder (HTTP Trigger)
PlaceOrder/function.json
{
"bindings": [
{
"type": "httpTrigger",
"direction": "in",
"name": "req",
"authLevel": "function",
"methods": ["post"],
"route": "orders"
},
{
"type": "http",
"direction": "out",
"name": "res"
},
{
"type": "queue",
"direction": "out",
"name": "ordersQueue",
"queueName": "orders",
"connection": "AzureWebJobsStorage"
}
]
}
PlaceOrder/index.js
const OrderService = require("../services/orderService");
module.exports = async function (context, req) {
context.log("PlaceOrder function triggered");
const orderData = req.body;
// Step 1: Validate the incoming order data
const validationError = OrderService.validate(orderData);
if (validationError) {
context.res = { status: 400, body: { error: validationError } };
return;
}
try {
// Step 2: Generate order ID and add metadata
const order = {
id: `ORD-${Date.now()}`,
customerId: orderData.customerId,
productId: orderData.productId,
quantity: orderData.quantity,
unitPrice: orderData.unitPrice,
total: orderData.quantity * orderData.unitPrice,
status: "pending",
email: orderData.email,
createdAt: new Date().toISOString()
};
// Step 3: Save order to database (Cosmos DB in production)
await OrderService.save(order);
context.log(`Order saved: ${order.id}`);
// Step 4: Put order on queue for async processing
// Output binding sends this to the "orders" queue automatically
context.bindings.ordersQueue = JSON.stringify(order);
context.log(`Order queued for processing: ${order.id}`);
// Step 5: Return 201 Created with the new order
context.res = {
status: 201,
body: {
orderId: order.id,
status: order.status,
total: order.total,
message: "Order placed successfully. Processing in progress."
}
};
} catch (err) {
context.log.error("PlaceOrder failed:", err.message);
context.res = { status: 500, body: { error: "Failed to place order" } };
}
};
Function 2 – ProcessOrder (Queue Trigger)
ProcessOrder/function.json
{
"bindings": [
{
"type": "queueTrigger",
"direction": "in",
"name": "orderMessage",
"queueName": "orders",
"connection": "AzureWebJobsStorage"
},
{
"type": "queue",
"direction": "out",
"name": "notificationsQueue",
"queueName": "notifications",
"connection": "AzureWebJobsStorage"
}
]
}
ProcessOrder/index.js
const PaymentService = require("../services/paymentService");
const OrderService = require("../services/orderService");
module.exports = async function (context, orderMessage) {
context.log(`ProcessOrder triggered for order: ${orderMessage.id}`);
// Idempotency check — prevent double-processing
const alreadyProcessed = await OrderService.isProcessed(orderMessage.id);
if (alreadyProcessed) {
context.log(`Order ${orderMessage.id} already processed. Skipping.`);
return;
}
try {
// Step 1: Charge the payment
context.log(`Charging payment for order ${orderMessage.id}...`);
const paymentResult = await PaymentService.charge({
amount: orderMessage.total,
customerId: orderMessage.customerId,
orderId: orderMessage.id
});
if (!paymentResult.success) {
throw new Error(`Payment failed: ${paymentResult.message}`);
}
context.log(`Payment successful for order ${orderMessage.id}`);
// Step 2: Update order status to "confirmed"
await OrderService.updateStatus(orderMessage.id, "confirmed");
// Step 3: Mark as processed (idempotency)
await OrderService.markProcessed(orderMessage.id);
// Step 4: Send to notifications queue
context.bindings.notificationsQueue = JSON.stringify({
type: "order_confirmed",
orderId: orderMessage.id,
customerEmail: orderMessage.email,
total: orderMessage.total
});
context.log(`Order ${orderMessage.id} confirmed and notification queued`);
} catch (err) {
context.log.error(`ProcessOrder failed for ${orderMessage.id}:`, err.message);
// Throwing the error causes Azure to retry (up to maxDequeueCount times)
throw err;
}
};
Function 3 – SendNotification (Queue Trigger)
SendNotification/function.json
{
"bindings": [
{
"type": "queueTrigger",
"direction": "in",
"name": "notificationMessage",
"queueName": "notifications",
"connection": "AzureWebJobsStorage"
}
]
}
SendNotification/index.js
const EmailService = require("../services/emailService");
module.exports = async function (context, notificationMessage) {
context.log(`SendNotification triggered: ${notificationMessage.type}`);
if (notificationMessage.type === "order_confirmed") {
await EmailService.sendOrderConfirmation({
to: notificationMessage.customerEmail,
orderId: notificationMessage.orderId,
total: notificationMessage.total
});
context.log(`Confirmation email sent to ${notificationMessage.customerEmail}`);
}
};
Function 4 – DailyReport (Timer Trigger)
DailyReport/function.json
{
"bindings": [
{
"type": "timerTrigger",
"direction": "in",
"name": "myTimer",
"schedule": "0 0 8 * * *"
},
{
"type": "blob",
"direction": "out",
"name": "reportBlob",
"path": "reports/{DateTime:yyyy-MM-dd}-daily-report.json",
"connection": "AzureWebJobsStorage"
}
]
}
DailyReport/index.js
const OrderService = require("../services/orderService");
module.exports = async function (context, myTimer) {
if (myTimer.isPastDue) {
context.log.warn("DailyReport timer is running late");
}
context.log("DailyReport function started");
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
yesterday.setHours(0, 0, 0, 0);
const today = new Date();
today.setHours(0, 0, 0, 0);
// Fetch all orders from last 24 hours
const orders = await OrderService.getOrdersByDateRange(yesterday, today);
const report = {
reportDate: yesterday.toISOString().split("T")[0],
generatedAt: new Date().toISOString(),
totalOrders: orders.length,
totalRevenue: orders.reduce((sum, o) => sum + o.total, 0),
confirmedOrders: orders.filter(o => o.status === "confirmed").length,
failedOrders: orders.filter(o => o.status === "failed").length,
orders: orders
};
// Output binding saves this report to blob storage automatically
context.bindings.reportBlob = JSON.stringify(report, null, 2);
context.log(`Daily report generated: ${report.totalOrders} orders, ₹${report.totalRevenue} revenue`);
};
Services Layer
services/orderService.js
const OrderService = {
validate: (order) => {
if (!order.customerId) return "customerId is required";
if (!order.productId) return "productId is required";
if (!order.quantity || order.quantity < 1) return "quantity must be at least 1";
if (!order.unitPrice || order.unitPrice <= 0) return "unitPrice must be positive";
if (!order.email) return "email is required";
return null; // null means valid
},
save: async (order) => {
// In production: save to Cosmos DB or SQL Database
console.log("Saving order:", order.id);
},
updateStatus: async (orderId, status) => {
console.log(`Order ${orderId} status updated to: ${status}`);
},
isProcessed: async (orderId) => {
// In production: check a "processed orders" record in database
return false;
},
markProcessed: async (orderId) => {
console.log(`Order ${orderId} marked as processed`);
},
getOrdersByDateRange: async (from, to) => {
// In production: query database for orders between from and to
return [];
}
};
module.exports = OrderService;
local.settings.json for This Project
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "node",
"COSMOS_DB_CONNECTION": "AccountEndpoint=https://...",
"SENDGRID_API_KEY": "SG.your_key_here",
"PAYMENT_GATEWAY_URL": "https://sandbox.paymentgateway.com",
"PAYMENT_GATEWAY_KEY": "sandbox_key_here",
"WEBSITE_TIME_ZONE": "India Standard Time"
}
}
Testing the Complete Flow
Step 1 – Start Azurite
azurite --silent
Step 2 – Start the Function App
func start
Step 3 – Place a Test Order
curl -X POST http://localhost:7071/api/orders \
-H "Content-Type: application/json" \
-d '{
"customerId": "CUST-001",
"productId": "PROD-42",
"quantity": 2,
"unitPrice": 1500,
"email": "ravi@example.com"
}'
Expected Response
{
"orderId": "ORD-1711234567890",
"status": "pending",
"total": 3000,
"message": "Order placed successfully. Processing in progress."
}
Step 4 – Watch the Queue Processing
After the HTTP response, watch the terminal. Within a few seconds, the ProcessOrder function and SendNotification function trigger automatically as they read from the queues. The full flow completes end-to-end without any further manual action.
What This Project Demonstrates
| Concept | Where Used |
|---|---|
| HTTP Trigger | PlaceOrder function |
| Queue Trigger | ProcessOrder and SendNotification functions |
| Timer Trigger | DailyReport function |
| Queue Output Binding | PlaceOrder → orders queue, ProcessOrder → notifications queue |
| Blob Output Binding | DailyReport → blob storage |
| Event-Driven Architecture | Functions communicate through queues, not direct calls |
| Idempotency | ProcessOrder checks before processing |
| Error Handling | try/catch in all functions, retry via queue |
| Configuration | All secrets in local.settings.json |
| Separation of Concerns | Services layer separate from function handlers |
| Logging | context.log at each key step |
This project is a strong foundation for any real-world serverless application on Azure. Extend it by adding Azure Cosmos DB for persistence, Application Insights for monitoring, Azure Key Vault for secrets, and GitHub Actions for automated deployment.
