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

ConceptWhere Used
HTTP TriggerPlaceOrder function
Queue TriggerProcessOrder and SendNotification functions
Timer TriggerDailyReport function
Queue Output BindingPlaceOrder → orders queue, ProcessOrder → notifications queue
Blob Output BindingDailyReport → blob storage
Event-Driven ArchitectureFunctions communicate through queues, not direct calls
IdempotencyProcessOrder checks before processing
Error Handlingtry/catch in all functions, retry via queue
ConfigurationAll secrets in local.settings.json
Separation of ConcernsServices layer separate from function handlers
Loggingcontext.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.

Leave a Comment