Azure Functions Advanced Patterns and Best Practices
As Azure Functions applications grow from single functions into complex, multi-function systems, certain architectural patterns and coding practices become essential. This topic covers the most important advanced patterns that separate maintainable, production-grade function apps from fragile ones.
Pattern 1 – Event-Driven Architecture
Event-driven architecture means functions communicate through events rather than direct calls. One function fires an event and does not wait for another function to respond. The second function reacts to the event independently.
┌──────────────────────────────────────────────────────────────┐ │ EVENT-DRIVEN ARCHITECTURE │ │ │ │ WITHOUT Events (Tight Coupling): │ │ OrderFunction → directly calls → EmailFunction │ │ If EmailFunction fails, OrderFunction fails too │ │ │ │ WITH Events (Loose Coupling): │ │ OrderFunction → puts event on queue │ │ │ │ │ ├──► EmailFunction (reads queue) │ │ ├──► InventoryFunction (reads queue) │ │ └──► AnalyticsFunction (reads queue) │ │ │ │ OrderFunction does not know or care about the others. │ │ Each function works independently. │ └──────────────────────────────────────────────────────────────┘
Implementation
// OrderFunction: places event on queue after order saved
module.exports = async function (context, req) {
const order = req.body;
// Save order to database
const savedOrder = await saveOrder(order);
// Fire event — multiple consumers will react independently
context.bindings.orderCreatedQueue = JSON.stringify({
event: "OrderCreated",
orderId: savedOrder.id,
customerId: order.customerId,
total: order.total,
timestamp: new Date().toISOString()
});
context.res = { status: 201, body: { orderId: savedOrder.id } };
};
Pattern 2 – Retry Pattern with Exponential Backoff
External services (databases, APIs, queues) sometimes fail temporarily. A naive retry tries again immediately, which can overload the failing service. Exponential backoff waits progressively longer between each retry.
┌──────────────────────────────────────────────────────────────┐ │ EXPONENTIAL BACKOFF DIAGRAM │ │ │ │ Attempt 1: Fails → Wait 1 second │ │ Attempt 2: Fails → Wait 2 seconds │ │ Attempt 3: Fails → Wait 4 seconds │ │ Attempt 4: Fails → Wait 8 seconds │ │ Attempt 5: Success! │ │ │ │ Total wait: 15 seconds instead of hammering every 0.1s │ └──────────────────────────────────────────────────────────────┘
async function callWithRetry(fn, maxRetries = 4) {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn(); // Try the operation
} catch (err) {
lastError = err;
if (attempt < maxRetries - 1) {
// Wait 2^attempt seconds: 1s, 2s, 4s, 8s
const delay = Math.pow(2, attempt) * 1000;
console.log(`Attempt ${attempt + 1} failed. Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw new Error(`All ${maxRetries} attempts failed: ${lastError.message}`);
}
// Usage in function
module.exports = async function (context, req) {
const data = await callWithRetry(
() => callExternalPaymentAPI(req.body)
);
context.res = { status: 200, body: data };
};
Pattern 3 – Idempotency
An idempotent function produces the same result whether it runs once or ten times with the same input. This is critical when queue messages can be delivered more than once (Azure guarantees at-least-once delivery, not exactly-once).
┌──────────────────────────────────────────────────────────────┐ │ IDEMPOTENCY │ │ │ │ NOT Idempotent: │ │ Queue message arrives 3 times (network issue) │ │ → Function runs 3 times │ │ → Email sent 3 times → User complains │ │ │ │ Idempotent: │ │ Queue message arrives 3 times │ │ → Function runs 3 times │ │ → First run: records "email sent for orderId=101" │ │ → Second run: checks, sees already sent → skips │ │ → Third run: checks, sees already sent → skips │ │ → User receives 1 email ✓ │ └──────────────────────────────────────────────────────────────┘
module.exports = async function (context, orderMessage) {
const orderId = orderMessage.orderId;
// Check if this order was already processed
const alreadyProcessed = await checkProcessedFlag(orderId);
if (alreadyProcessed) {
context.log(`Order ${orderId} already processed. Skipping.`);
return; // Exit without processing again
}
// Process the order
await sendConfirmationEmail(orderMessage);
// Record that this order has been processed
await setProcessedFlag(orderId);
context.log(`Order ${orderId} processed successfully.`);
};
Pattern 4 – Separation of Concerns
Placing all logic directly inside the function handler creates functions that are difficult to test and maintain. Separating business logic into dedicated service modules keeps the function handler clean.
// BAD: All logic inside handler
module.exports = async function (context, req) {
const order = req.body;
if (!order.productId || !order.quantity) { ... } // validation
const product = await cosmosClient.query(...); // data access
order.total = product.price * order.quantity; // calculation
await sendEmail(order.email, ...); // email sending
context.res = { status: 201, body: order };
};
// GOOD: Logic separated into services
// services/orderService.js
const OrderService = {
validate: (order) => order.productId && order.quantity > 0,
calculate: (order, product) => ({ ...order, total: product.price * order.quantity }),
save: async (order) => { /* database logic */ },
notify: async (order) => { /* email logic */ }
};
module.exports = OrderService;
// functions/CreateOrder/index.js
const OrderService = require("../../services/orderService");
module.exports = async function (context, req) {
const order = req.body;
if (!OrderService.validate(order)) {
context.res = { status: 400, body: "Missing required fields" };
return;
}
const product = await getProduct(order.productId);
const fullOrder = OrderService.calculate(order, product);
await OrderService.save(fullOrder);
await OrderService.notify(fullOrder);
context.res = { status: 201, body: fullOrder };
};
Pattern 5 – Circuit Breaker
If an external service is down, retrying hundreds of times wastes resources and delays recovery. A circuit breaker detects repeated failures and stops calling the failing service for a cooling period, then tries again.
┌──────────────────────────────────────────────────────────────┐ │ CIRCUIT BREAKER STATES │ │ │ │ CLOSED (normal) → Calls pass through │ │ │ │ │ │ 5 failures in a row │ │ ▼ │ │ OPEN (broken) → All calls rejected immediately │ │ (No more calls to failing service — saves time/resources) │ │ │ │ │ │ After 30 seconds cooling period │ │ ▼ │ │ HALF-OPEN → One trial call allowed │ │ │ │ │ ├── Trial succeeds → Back to CLOSED ✓ │ │ └── Trial fails → Back to OPEN ✗ │ └──────────────────────────────────────────────────────────────┘
Best Practices Summary
Code Quality
| Practice | Why It Matters |
|---|---|
| One function, one responsibility | Easier to test, deploy, and scale independently |
| Async/await everywhere | Prevents blocking, improves throughput |
| Always handle errors with try/catch | Prevents unhandled rejections that crash the function |
| Validate all inputs | Returns clear 400 errors instead of cryptic 500 errors |
| Log start, end, and key decisions | Makes debugging in production far easier |
Architecture
| Practice | Why It Matters |
|---|---|
| Use queues to decouple functions | One failure does not cascade to other functions |
| Design for idempotency | Safe to retry without side effects |
| Use Managed Identity for Azure services | No passwords stored in configuration |
| Keep function execution time short | Reduces cost and timeout risk |
| Offload long work to Durable Functions | Handles long workflows reliably |
Testing
// Unit testing a function handler (using Jest)
const httpTrigger = require("./index");
test("returns 200 with greeting when name provided", async () => {
const context = { log: jest.fn(), res: {} };
const req = { query: { name: "Ravi" }, body: {} };
await httpTrigger(context, req);
expect(context.res.status).toBe(200);
expect(context.res.body).toContain("Ravi");
});
test("returns 400 when name is missing", async () => {
const context = { log: jest.fn(), res: {} };
const req = { query: {}, body: {} };
await httpTrigger(context, req);
expect(context.res.status).toBe(400);
});
Function App Project Structure – Recommended
my-function-app/
├── host.json
├── local.settings.json ← In .gitignore
├── package.json
├── .gitignore
├── services/ ← Business logic (testable)
│ ├── orderService.js
│ ├── emailService.js
│ └── paymentService.js
├── utils/ ← Shared helpers
│ ├── retry.js
│ └── validator.js
├── CreateOrder/ ← One folder per function
│ ├── function.json
│ └── index.js
├── GetOrder/
│ ├── function.json
│ └── index.js
└── ProcessOrderQueue/
├── function.json
└── index.js
