diff --git a/lambda-durable-order-processing-sam/.gitignore b/lambda-durable-order-processing-sam/.gitignore new file mode 100644 index 000000000..38d4d358f --- /dev/null +++ b/lambda-durable-order-processing-sam/.gitignore @@ -0,0 +1,16 @@ +# SAM Build Artifacts +.aws-sam/ +samconfig.toml +*.zip + +# Node +node_modules/ +*.log + +# Temporary Files +checkpoint-policy.json +DEPLOYMENT.md + +# OS +.DS_Store +Thumbs.db diff --git a/lambda-durable-order-processing-sam/README.md b/lambda-durable-order-processing-sam/README.md new file mode 100644 index 000000000..038ce1d22 --- /dev/null +++ b/lambda-durable-order-processing-sam/README.md @@ -0,0 +1,494 @@ +# Order Processing Workflow with AWS Lambda Durable Functions + +This pattern demonstrates a multi-step order processing workflow using AWS Lambda Durable Functions. The workflow handles order validation, payment processing, inventory checking, and shipping arrangement with automatic checkpointing and state persistence across long-running operations. + +**Important:** Lambda Durable Functions are currently available in the **us-east-2 (Ohio)** region only. + +## Architecture + +![Architecture Diagram](architecture.png) + +The solution uses a dual-function architecture: +- **Durable Function**: Handles async order processing with 17 steps and automatic checkpointing +- **Status Function**: Provides real-time order status via synchronous API calls + +### Order Processing Workflow (17 Steps) + +The workflow consists of 17 steps organized into 5 phases: + +**Phase 1: Validation (Steps 1-3)** +1. Validate Order - Check order data and customer information +2. Check Inventory - Verify item availability +3. Process Payment - Process payment transaction + +**Phase 2: Risk Assessment (Steps 4-6)** +4. Reserve Inventory - Lock inventory for order +5. Fraud Check - Run fraud detection +6. Credit Check - Verify credit (for orders > $1000) + +**Phase 3: Invoice (Step 7)** +7. Generate Invoice - Create customer invoice + +**Phase 4: Fulfillment (Steps 8-12)** +- *Wait 5 minutes for warehouse processing (no compute cost)* +8. Pick Items - Warehouse picks items +9. Quality Check - Inspect items +10. Package Order - Package for shipping +11. Generate Shipping Label - Create shipping label + +**Phase 5: Shipping & Completion (Steps 13-17)** +- *Wait 3 minutes for carrier pickup (no compute cost)* +12. Ship Order - Hand off to carrier +13. Send Notifications - Email/SMS to customer +14. Update Loyalty Points - Award loyalty points +15. Complete Order - Mark order as complete + +Each step is automatically checkpointed, allowing the workflow to resume from the last successful step if interrupted. + +## Key Features + +- ✅ **Automatic Checkpointing** - Each step is checkpointed automatically +- ✅ **Failure Recovery** - Resumes from last checkpoint on failure +- ✅ **Compensation Logic** - Rolls back on errors +- ✅ **Wait States** - Efficient waiting without compute charges +- ✅ **State Persistence** - Order status stored in DynamoDB +- ✅ **API Integration** - REST API for order submission and status checking + +## Prerequisites + +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) installed +* [Node.js 18+](https://nodejs.org/) installed + +### Required IAM Permissions + +Your AWS CLI user/role needs the following permissions for deployment and testing: +- **CloudFormation**: `cloudformation:DescribeStacks`, `cloudformation:DeleteStack` +- **Lambda**: `lambda:CreateFunction`, `lambda:InvokeFunction`, `lambda:GetFunction` +- **DynamoDB**: `dynamodb:Scan`, `dynamodb:GetItem` +- **CloudWatch Logs**: `logs:DescribeLogGroups`, `logs:FilterLogEvents`, `logs:GetLogEvents`, `logs:TailLogEvents` +- **API Gateway**: `apigateway:GET` +- **IAM**: `iam:CreateRole`, `iam:AttachRolePolicy`, `iam:PassRole` + + +## Deployment + +1. Navigate to the pattern directory: + ```bash + cd lambda-durable-order-processing-sam + ``` + +2. Install dependencies: + ```bash + cd src && npm install && cd .. + ``` + +3. Build the SAM application: + ```bash + sam build + ``` + +4. Deploy the application (must use us-east-2 region): + ```bash + sam deploy --guided --region us-east-2 + ``` + + During the guided deployment: + - Stack Name: `lambda-durable-order-processing` + - AWS Region: `us-east-2` + - Confirm changes: `N` + - Allow SAM CLI IAM role creation: `Y` + - Disable rollback: `N` + - Save arguments to config file: `Y` + +5. Note the `OrderApiEndpoint` from the outputs. + +## Testing + +### Step 1: Get Your API Endpoint + +Retrieve your API endpoint from the CloudFormation stack: + +```bash +API_ENDPOINT=$(aws cloudformation describe-stacks \ + --stack-name lambda-durable-order-processing \ + --region us-east-2 \ + --query 'Stacks[0].Outputs[?OutputKey==`OrderApiEndpoint`].OutputValue' \ + --output text) + +echo "API Endpoint: $API_ENDPOINT" +``` + +### Step 2: Create Test Orders + +**Test 1: Low-value order (< $1000)** +```bash +curl -X POST ${API_ENDPOINT}/orders \ + -H "Content-Type: application/json" \ + -d '{ + "customerId": "CUST-001", + "customerEmail": "customer1@example.com", + "items": [ + {"productId": "BOOK-001", "name": "Programming Book", "quantity": 2, "price": 45.99} + ] + }' +``` + +Expected response: +```json +{ + "message": "Order processing initiated", + "orderId": "order-1764821208592" +} +``` + +**Test 2: High-value order (> $1000, triggers credit check)** +```bash +curl -X POST ${API_ENDPOINT}/orders \ + -H "Content-Type: application/json" \ + -d '{ + "customerId": "CUST-002", + "customerEmail": "customer2@example.com", + "items": [ + {"productId": "SERVER-001", "name": "Enterprise Server", "quantity": 1, "price": 3500.00} + ] + }' +``` + +**Test 3: Multiple items order** +```bash +curl -X POST ${API_ENDPOINT}/orders \ + -H "Content-Type: application/json" \ + -d '{ + "customerId": "CUST-003", + "customerEmail": "customer3@example.com", + "items": [ + {"productId": "LAPTOP-001", "name": "Gaming Laptop", "quantity": 1, "price": 1299.99}, + {"productId": "MOUSE-001", "name": "Wireless Mouse", "quantity": 2, "price": 29.99}, + {"productId": "KEYBOARD-001", "name": "Mechanical Keyboard", "quantity": 1, "price": 149.99} + ] + }' +``` + +**Test 4: Edge case - Empty items (should fail validation)** +```bash +curl -X POST ${API_ENDPOINT}/orders \ + -H "Content-Type: application/json" \ + -d '{ + "customerId": "CUST-004", + "customerEmail": "customer4@example.com", + "items": [] + }' +``` + +### Step 3: Check Order Status + +Wait 10 seconds, then check the order status using the order ID from the response: + +```bash +ORDER_ID="order-1764821208592" # Replace with your order ID +curl ${API_ENDPOINT}/orders/${ORDER_ID} | jq '.' +``` + +Expected response: +```json +{ + "orderId": "order-1764821208592", + "status": "awaiting-warehouse", + "customerId": "CUST-001", + "customerEmail": "customer1@example.com", + "total": 91.98, + "items": [ + { + "name": "Programming Book", + "quantity": 2, + "productId": "BOOK-001", + "price": 45.99 + } + ], + "createdAt": "2025-12-04T04:06:48.964Z", + "lastUpdated": "2025-12-04T04:06:50.621Z" +} +``` + +### Step 4: Track Status Progression + +Poll the order status to see it progress through the workflow: + +```bash +# Check status every 30 seconds +for i in {1..10}; do + echo "=== Check $i at $(date +%H:%M:%S) ===" + curl -s ${API_ENDPOINT}/orders/${ORDER_ID} | jq '{status, lastUpdated}' + sleep 30 +done +``` + +You'll see the status progress through: +- `validated` → `inventory-checked` → `payment-processed` → `inventory-reserved` → `fraud-checked` → `invoice-generated` → `awaiting-warehouse` (5 min wait) → `items-picked` → `quality-checked` → `packaged` → `awaiting-pickup` (3 min wait) → `shipped` → `completed` + +### Step 5: Monitor Lambda Logs + +View real-time Lambda execution logs: + +```bash +# Get function name +FUNCTION_NAME=$(aws cloudformation describe-stack-resources \ + --stack-name lambda-durable-order-processing \ + --region us-east-2 \ + --query 'StackResources[?LogicalResourceId==`OrderProcessingFunction`].PhysicalResourceId' \ + --output text) + +# Tail logs +aws logs tail /aws/lambda/${FUNCTION_NAME} \ + --follow \ + --format short \ + --region us-east-2 +``` + +Look for checkpoint and step execution messages: +``` +Starting order processing { orderId: 'order-1764821208592' } +Validating order { orderId: 'order-1764821208592' } +Checking inventory { orderId: 'order-1764821208592' } +Processing payment { orderId: 'order-1764821208592', amount: 91.98 } +Waiting for warehouse processing { orderId: 'order-1764821208592' } +``` + +### Step 6: Verify Amazon DynamoDB Storage + +Check orders stored in DynamoDB: + +```bash +TABLE_NAME=$(aws cloudformation describe-stacks \ + --stack-name lambda-durable-order-processing \ + --region us-east-2 \ + --query 'Stacks[0].Outputs[?OutputKey==`OrdersTableName`].OutputValue' \ + --output text) + +# Scan all orders +aws dynamodb scan \ + --table-name ${TABLE_NAME} \ + --region us-east-2 \ + --max-items 5 | jq '.Items[] | {orderId: .orderId.S, status: .status.S, total: .total.N}' +``` + +### Step 7: Test Concurrent Orders + +Create multiple orders simultaneously to test concurrency: + +```bash +for i in {1..5}; do + curl -X POST ${API_ENDPOINT}/orders \ + -H "Content-Type: application/json" \ + -d "{ + \"customerId\": \"CONCURRENT-$i\", + \"customerEmail\": \"concurrent$i@example.com\", + \"items\": [{\"productId\": \"PROD-$i\", \"name\": \"Product $i\", \"quantity\": 1, \"price\": $((100 + i * 10))}] + }" & +done +wait +echo "All 5 orders submitted concurrently" +``` + +### Expected Test Results + +- ✅ **Low-value orders**: Complete all 17 steps (no credit check) +- ✅ **High-value orders**: Include credit check step (Step 6) +- ✅ **Multi-item orders**: Calculate total correctly +- ✅ **Invalid orders**: Fail validation and mark as failed +- ✅ **Status API**: Returns real-time order status +- ✅ **Concurrent orders**: All process independently +- ✅ **Wait periods**: 8 minutes total (5 min + 3 min) with no compute cost + + + +## How It Works + +### Durable Execution + +The order processing function uses the `@aws/durable-execution-sdk-js` to create checkpoints at each step: + +```javascript +import { withDurableExecution } from '@aws/durable-execution-sdk-js'; + +export const handler = withDurableExecution(async (event, context) => { + // Each step is automatically checkpointed + await context.step('validate-order', async () => { + console.log('Validating order'); + // Validation logic + }); + + // Wait 5 seconds (simulating external API call) + await context.wait({ seconds: 5 }); + + await context.step('process-payment', async () => { + console.log('Processing payment'); + // Payment logic + }); + + // More steps... +}); +``` + +### Checkpoint Behavior + +When a durable function executes: +1. Each `context.step()` creates a checkpoint before execution +2. If the function is interrupted, Lambda saves the checkpoint state +3. On retry, the function replays from the beginning +4. Completed steps are skipped using stored checkpoint results +5. Execution continues from the last incomplete step + +You'll see the same order ID appear multiple times in logs - this is the durable execution resuming from checkpoints! + +### State Persistence + +Final order state is saved to DynamoDB after all steps complete: + +```javascript +await context.step('save-order', async () => { + await dynamodb.putItem({ + TableName: process.env.ORDERS_TABLE, + Item: { orderId, status: 'completed', ... } + }); +}); +``` + +## Configuration + +### Durable Execution Settings + +The durable function must be created with durable configuration (cannot be added to existing functions): + +```bash +aws lambda create-function \ + --function-name my-durable-function \ + --runtime nodejs22.x \ + --durable-config '{"ExecutionTimeout":86400,"RetentionPeriodInDays":7}' \ + ... +``` + +- **ExecutionTimeout**: 86400 seconds (24 hours) +- **RetentionPeriodInDays**: 7 days + +### IAM Permissions + +The function requires these permissions: + +```yaml +Policies: + - Statement: + - Effect: Allow + Action: + - lambda:CheckpointDurableExecution + - lambda:GetDurableExecutionState + Resource: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${FunctionName}:*/durable-execution/*' + - DynamoDBCrudPolicy: + TableName: !Ref OrdersTable +``` + +## Customization + +### Adjust Wait Time + +Modify the wait duration in `src/index.js`: + +```javascript +await context.wait({ seconds: 3600 }); // Wait 1 hour +``` + +### Add More Steps + +Add additional processing steps: + +```javascript +await context.step('send-notification', async () => { + console.log('Sending notification'); + // Send email/SMS notification +}); +``` + +### Integrate Real Services + +Replace simulation code with actual service calls: + +```javascript +await context.step('process-payment', async () => { + const stripe = require('stripe')(process.env.STRIPE_KEY); + return await stripe.charges.create({ + amount: order.total * 100, + currency: 'usd', + customer: order.customerId + }); +}); +``` + +### Modify Execution Timeout + +Update the durable configuration when creating the function: + +```yaml +DurableConfig: + ExecutionTimeout: 172800 # 48 hours + RetentionPeriodInDays: 14 +``` + +## Monitoring + +### CloudWatch Metrics + +Monitor durable execution metrics: +- `DurableExecutionStarted` +- `DurableExecutionCompleted` +- `DurableExecutionFailed` +- `DurableExecutionCheckpointCreated` + +### CloudWatch Logs + +Look for log entries with `[DURABLE_EXECUTION]` prefix to track: +- Checkpoint creation +- Replay events +- Step execution + +### X-Ray Tracing + +Enable X-Ray tracing in the SAM template: + +```yaml +Tracing: Active +``` + +## Cleanup + +Delete the stack: + +```bash +sam delete --region us-east-2 +``` + +Or via AWS CLI: + +```bash +aws cloudformation delete-stack --stack-name lambda-durable-order-processing --region us-east-2 +``` + +## Cost Considerations + +- **Lambda**: Pay per invocation and execution time +- **DynamoDB**: Pay-per-request pricing +- **API Gateway**: Pay per API call +- **Durable Execution**: Checkpoint storage costs (minimal) +- **Wait States**: No compute charges during waits + +## Learn More + +- [Lambda Durable Functions Documentation](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html) +- [Durable Execution SDK (JavaScript)](https://github.com/aws/aws-durable-execution-sdk-js) +- [AWS SAM Documentation](https://docs.aws.amazon.com/serverless-application-model/) + +--- + +Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/lambda-durable-order-processing-sam/architecture.png b/lambda-durable-order-processing-sam/architecture.png new file mode 100644 index 000000000..e8f7635c9 Binary files /dev/null and b/lambda-durable-order-processing-sam/architecture.png differ diff --git a/lambda-durable-order-processing-sam/example-pattern.json b/lambda-durable-order-processing-sam/example-pattern.json new file mode 100644 index 000000000..7c3748b10 --- /dev/null +++ b/lambda-durable-order-processing-sam/example-pattern.json @@ -0,0 +1,68 @@ +{ + "title": "Order Processing Workflow with Lambda Durable Functions", + "description": "Production-ready 17-step order processing workflow using Lambda Durable Functions with automatic checkpointing, long-running waits, and state persistence", + "language": "Node.js", + "level": "300", + "framework": "AWS SAM", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern demonstrates a production-ready order processing workflow with 17 steps using Lambda Durable Functions.", + "The workflow includes validation, payment processing, fraud checks, credit checks (for high-value orders), inventory management, and shipping coordination.", + "Durable execution enables long-running waits (5 minutes for warehouse processing, 3 minutes for carrier pickup) without consuming compute resources.", + "Each step is automatically checkpointed, allowing the workflow to survive interruptions and resume from the last successful step.", + "The pattern uses a dual-function architecture: async durable function for order processing and sync non-durable function for real-time status queries.", + "Order state is persisted in DynamoDB with real-time status updates throughout the 17-step workflow." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-durable-order-processing-sam", + "templateURL": "serverless-patterns/lambda-durable-order-processing-sam", + "projectFolder": "lambda-durable-order-processing-sam", + "templateFile": "template.yaml" + } + }, + "resources": { + "bullets": [ + { + "text": "Lambda Durable Functions Documentation", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" + }, + { + "text": "Durable Execution SDK for JavaScript", + "link": "https://github.com/aws/aws-durable-execution-sdk-js" + }, + { + "text": "AWS Blog: Build multi-step applications with Lambda durable functions", + "link": "https://aws.amazon.com/blogs/aws/build-multi-step-applications-and-ai-workflows-with-aws-lambda-durable-functions/" + } + ] + }, + "deploy": { + "text": [ + "Note: Lambda Durable Functions are currently available in us-east-2 (Ohio) region only.", + "cd src && npm install && cd ..", + "sam build", + "sam deploy --guided --region us-east-2" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: sam delete --region us-east-2." + ] + }, + "authors": [ + { + "name": "Abhishek Agawane", + "image": "https://drive.google.com/file/d/1E-5koDaKEaMUtOctX32I9TLwfh3kgpAq/view?usp=drivesdk", + "bio": "Abhishek Agawane is a Security Consultant at Amazon Web Services with more than 8 years of industry experience. He helps organizations architect resilient, secure, and efficient cloud environments, guiding them through complex challenges and large-scale infrastructure transformations. He has helped numerous organizations enhance their cloud operations through targeted optimizations, robust architectures, and best-practice implementations.", + "linkedin": "https://www.linkedin.com/in/agawabhi/" + } + ] +} diff --git a/lambda-durable-order-processing-sam/lambda-durable-order-processing-sam.json b/lambda-durable-order-processing-sam/lambda-durable-order-processing-sam.json new file mode 100644 index 000000000..c5b3aed6a --- /dev/null +++ b/lambda-durable-order-processing-sam/lambda-durable-order-processing-sam.json @@ -0,0 +1,96 @@ +{ + "title": "Order Processing with AWS Lambda Durable Functions", + "description": "Order processing workflow using Lambda Durable Functions with automatic checkpointing, long-running waits, and state persistence", + "language": "Node.js", + "level": "300", + "framework": "AWS SAM", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern demonstrates an order processing workflow using Lambda Durable Functions.", + "The workflow includes validation, payment processing, fraud checks, credit checks (for high-value orders), inventory management, and shipping coordination.", + "Durable execution enables long-running waits (5 minutes for warehouse processing, 3 minutes for carrier pickup) without consuming compute resources.", + "Each step is automatically checkpointed, allowing the workflow to survive interruptions and resume from the last successful step.", + "The pattern uses a dual-function architecture: async durable function for order processing and sync non-durable function for real-time status queries.", + "Order state is persisted in Amazon DynamoDB with real-time status updates throughout the 17-step workflow." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-durable-order-processing-sam", + "templateURL": "serverless-patterns/lambda-durable-order-processing-sam", + "projectFolder": "lambda-durable-order-processing-sam", + "templateFile": "template.yaml" + } + }, + "resources": { + "bullets": [ + { + "text": "Lambda Durable Functions Documentation", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" + }, + { + "text": "Durable Execution SDK for JavaScript", + "link": "https://github.com/aws/aws-durable-execution-sdk-js" + }, + { + "text": "AWS Blog: Build multi-step applications with Lambda durable functions", + "link": "https://aws.amazon.com/blogs/aws/build-multi-step-applications-and-ai-workflows-with-aws-lambda-durable-functions/" + } + ] + }, + "deploy": { + "text": [ + "Note: Lambda Durable Functions are currently available in us-east-2 (Ohio) region only.", + "cd src && npm install && cd ..", + "sam build", + "sam deploy --guided --region us-east-2" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "Delete the stack: sam delete --region us-east-2." + ] + }, + "authors": [ + { + "name": "Abhishek Agawane", + "image": "https://drive.google.com/file/d/1E-5koDaKEaMUtOctX32I9TLwfh3kgpAq/view?usp=drivesdk", + "bio": "Abhishek Agawane is a Security Consultant at Amazon Web Services with more than 8 years of industry experience. He helps organizations architect resilient, secure, and efficient cloud environments, guiding them through complex challenges and large-scale infrastructure transformations. He has helped numerous organizations enhance their cloud operations through targeted optimizations, robust architectures, and best-practice implementations.", + "linkedin": "agawabhi" + } + ], + "patternArch": { + "icon1": { + "x": 20, + "y": 50, + "service": "apigw", + "label": "API Gateway REST API" + }, + "icon2": { + "x": 50, + "y": 50, + "service": "lambda", + "label": "AWS Lambda" + }, + "line1": { + "from": "icon1", + "to": "icon2" + }, + "icon3": { + "x": 80, + "y": 50, + "service": "dynamodb", + "label": "Amazon DynamoDB" + }, + "line2": { + "from": "icon2", + "to": "icon3" + } + } +} diff --git a/lambda-durable-order-processing-sam/src/index.js b/lambda-durable-order-processing-sam/src/index.js new file mode 100644 index 000000000..d30fabb27 --- /dev/null +++ b/lambda-durable-order-processing-sam/src/index.js @@ -0,0 +1,408 @@ +const { withDurableExecution } = require('@aws/durable-execution-sdk-js'); +const { DynamoDBClient } = require('@aws-sdk/client-dynamodb'); +const { DynamoDBDocumentClient, PutCommand, UpdateCommand } = require('@aws-sdk/lib-dynamodb'); + +const dynamoClient = new DynamoDBClient({}); +const docClient = DynamoDBDocumentClient.from(dynamoClient); +const ORDERS_TABLE = process.env.ORDERS_TABLE; + +/** + * Order Processing Durable Function + * + * This function demonstrates a multi-step order processing workflow: + * 1. Validate order + * 2. Check inventory + * 3. Process payment + * 4. Reserve inventory + * 5. Wait for fulfillment preparation + * 6. Ship order + * 7. Send confirmation + */ +exports.handler = withDurableExecution(async (event, context) => { + console.log('Starting order processing', { event }); + + // Parse order data - orderId comes from API Gateway template + const orderId = event.orderId || `order-${Date.now()}`; + const body = typeof event.body === 'string' ? JSON.parse(event.body) : event.body; + const order = { ...body, orderId }; + + try { + // Step 1: Validate Order + const validatedOrder = await context.step('validate-order', async () => { + console.log('Validating order', { orderId }); + + // Simulate validation logic + if (!order.items || order.items.length === 0) { + throw new Error('Order must contain at least one item'); + } + + if (!order.customerId) { + throw new Error('Customer ID is required'); + } + + // Calculate total + const total = order.items.reduce((sum, item) => { + return sum + (item.price * item.quantity); + }, 0); + + const validated = { + ...order, + orderId, + total, + status: 'validated', + validatedAt: new Date().toISOString() + }; + + // Save to DynamoDB + await docClient.send(new PutCommand({ + TableName: ORDERS_TABLE, + Item: validated + })); + + return validated; + }); + + // Step 2: Check Inventory + const inventoryCheck = await context.step('check-inventory', async () => { + console.log('Checking inventory', { orderId }); + + // Simulate inventory check + const available = validatedOrder.items.every(item => { + // Simulate: 90% chance items are in stock + return Math.random() > 0.1; + }); + + if (!available) { + throw new Error('Insufficient inventory'); + } + + await updateOrderStatus(orderId, 'inventory-checked'); + + return { + available: true, + checkedAt: new Date().toISOString() + }; + }); + + // Step 3: Process Payment + const paymentResult = await context.step('process-payment', async () => { + console.log('Processing payment', { orderId, amount: validatedOrder.total }); + + // Simulate payment processing + // In real scenario, this would call a payment gateway + const paymentId = `pay-${Date.now()}`; + + // Simulate: 95% success rate + if (Math.random() < 0.05) { + throw new Error('Payment declined'); + } + + await updateOrderStatus(orderId, 'payment-processed'); + + return { + paymentId, + amount: validatedOrder.total, + status: 'success', + processedAt: new Date().toISOString() + }; + }); + + // Step 4: Reserve Inventory + const reservation = await context.step('reserve-inventory', async () => { + console.log('Reserving inventory', { orderId }); + + // Simulate inventory reservation + const reservationId = `res-${Date.now()}`; + + await updateOrderStatus(orderId, 'inventory-reserved'); + + return { + reservationId, + items: validatedOrder.items, + reservedAt: new Date().toISOString() + }; + }); + + // Step 5: Fraud Check (parallel with credit check) + const fraudCheck = await context.step('fraud-check', async () => { + console.log('Running fraud detection', { orderId }); + + // Simulate fraud detection analysis + const riskScore = Math.random() * 100; + const isFraudulent = riskScore > 95; + + if (isFraudulent) { + throw new Error('Order flagged as fraudulent'); + } + + await updateOrderStatus(orderId, 'fraud-checked'); + + return { + riskScore, + status: 'passed', + checkedAt: new Date().toISOString() + }; + }); + + // Step 6: Credit Check + const creditCheck = await context.step('credit-check', async () => { + console.log('Checking customer credit', { orderId }); + + // Simulate credit check for high-value orders + if (validatedOrder.total > 1000) { + const creditScore = Math.floor(Math.random() * 300) + 500; + + if (creditScore < 600) { + throw new Error('Insufficient credit score'); + } + + return { + creditScore, + approved: true, + checkedAt: new Date().toISOString() + }; + } + + return { skipped: true, reason: 'Order value below threshold' }; + }); + + // Step 7: Generate Invoice + const invoice = await context.step('generate-invoice', async () => { + console.log('Generating invoice', { orderId }); + + const invoiceId = `INV-${Date.now()}`; + const invoiceData = { + invoiceId, + orderId, + customerId: validatedOrder.customerId, + items: validatedOrder.items, + subtotal: validatedOrder.total, + tax: validatedOrder.total * 0.08, + total: validatedOrder.total * 1.08, + generatedAt: new Date().toISOString() + }; + + await updateOrderStatus(orderId, 'invoice-generated'); + + return invoiceData; + }); + + // Step 8: Wait for warehouse processing (showcases long wait without compute charges) + console.log('Waiting for warehouse processing', { orderId }); + await updateOrderStatus(orderId, 'awaiting-warehouse'); + await context.wait({ seconds: 300 }); // 5 minutes - in production could be hours + + // Step 9: Pick Items from Warehouse + const pickingResult = await context.step('pick-items', async () => { + console.log('Picking items from warehouse', { orderId }); + + const pickedItems = validatedOrder.items.map(item => ({ + ...item, + binLocation: `BIN-${Math.floor(Math.random() * 1000)}`, + pickedBy: `PICKER-${Math.floor(Math.random() * 50)}`, + pickedAt: new Date().toISOString() + })); + + await updateOrderStatus(orderId, 'items-picked'); + + return { + items: pickedItems, + completedAt: new Date().toISOString() + }; + }); + + // Step 10: Quality Check + const qualityCheck = await context.step('quality-check', async () => { + console.log('Performing quality check', { orderId }); + + // Simulate quality inspection + const passedQC = Math.random() > 0.02; // 98% pass rate + + if (!passedQC) { + throw new Error('Quality check failed - items damaged'); + } + + await updateOrderStatus(orderId, 'quality-checked'); + + return { + passed: true, + inspector: `QC-${Math.floor(Math.random() * 20)}`, + checkedAt: new Date().toISOString() + }; + }); + + // Step 11: Package Order + const packaging = await context.step('package-order', async () => { + console.log('Packaging order', { orderId }); + + const packageId = `PKG-${Date.now()}`; + const weight = validatedOrder.items.reduce((sum, item) => sum + (item.quantity * 2), 0); + + await updateOrderStatus(orderId, 'packaged'); + + return { + packageId, + weight: `${weight} lbs`, + dimensions: '12x10x8 inches', + packagedBy: `PACKER-${Math.floor(Math.random() * 30)}`, + packagedAt: new Date().toISOString() + }; + }); + + // Step 12: Generate Shipping Label + const shippingLabel = await context.step('generate-shipping-label', async () => { + console.log('Generating shipping label', { orderId }); + + const trackingNumber = `TRK-${Date.now()}-${Math.random().toString(36).substring(2, 11).toUpperCase()}`; + const carrier = validatedOrder.total > 500 ? 'ExpressShip' : 'StandardShip'; + + return { + trackingNumber, + carrier, + labelUrl: `https://shipping.example.com/labels/${trackingNumber}`, + generatedAt: new Date().toISOString() + }; + }); + + // Step 13: Wait for carrier pickup (another long wait) + console.log('Waiting for carrier pickup', { orderId }); + await updateOrderStatus(orderId, 'awaiting-pickup'); + await context.wait({ seconds: 180 }); // 3 minutes - in production could be hours + + // Step 14: Ship Order + const shipment = await context.step('ship-order', async () => { + console.log('Order shipped', { orderId }); + + await updateOrderStatus(orderId, 'shipped'); + + return { + ...shippingLabel, + estimatedDelivery: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000).toISOString(), + shippedAt: new Date().toISOString() + }; + }); + + // Step 15: Send Customer Notifications (parallel notifications) + const notifications = await context.step('send-notifications', async () => { + console.log('Sending customer notifications', { orderId }); + + // Simulate sending multiple notifications + const emailNotification = { + type: 'email', + recipient: order.customerEmail || 'customer@example.com', + subject: `Order ${orderId} Shipped - Tracking: ${shippingLabel.trackingNumber}`, + sentAt: new Date().toISOString() + }; + + const smsNotification = { + type: 'sms', + recipient: order.customerPhone || '+1234567890', + message: `Your order ${orderId} has shipped! Track: ${shippingLabel.trackingNumber}`, + sentAt: new Date().toISOString() + }; + + return { + email: emailNotification, + sms: smsNotification + }; + }); + + // Step 16: Update Loyalty Points + const loyaltyUpdate = await context.step('update-loyalty-points', async () => { + console.log('Updating loyalty points', { orderId }); + + const pointsEarned = Math.floor(validatedOrder.total * 0.1); // 10% back in points + + return { + customerId: validatedOrder.customerId, + pointsEarned, + newBalance: pointsEarned, // In production, would fetch and add to existing balance + updatedAt: new Date().toISOString() + }; + }); + + // Step 17: Complete Order + const completion = await context.step('complete-order', async () => { + console.log('Completing order', { orderId }); + + await updateOrderStatus(orderId, 'completed'); + + return { + completedAt: new Date().toISOString(), + totalProcessingTime: Date.now() - parseInt(orderId.split('-')[1]) + }; + }); + + // Return comprehensive result + const result = { + orderId, + status: 'completed', + order: validatedOrder, + payment: paymentResult, + fraudCheck, + creditCheck, + invoice, + picking: pickingResult, + qualityCheck, + packaging, + shipment, + notifications, + loyaltyPoints: loyaltyUpdate, + completion, + summary: { + totalSteps: 17, + totalWaitTime: '8 minutes (480 seconds)', + processingTime: `${completion.totalProcessingTime}ms` + } + }; + + console.log('Order processing completed successfully', { orderId, totalSteps: 17 }); + return result; + + } catch (error) { + console.error('Order processing failed', { orderId, error: error.message }); + + // Compensation logic - rollback changes + await context.step('compensate', async () => { + console.log('Running compensation logic', { orderId }); + + await updateOrderStatus(orderId, 'failed', error.message); + + // In production, you would: + // - Refund payment if processed + // - Release inventory reservation + // - Notify customer of failure + + return { + compensated: true, + reason: error.message + }; + }); + + throw error; + } +}); + +/** + * Helper function to update order status in DynamoDB + */ +async function updateOrderStatus(orderId, status, errorMessage = null) { + const updateExpression = errorMessage + ? 'SET #status = :status, errorMessage = :error, updatedAt = :updatedAt' + : 'SET #status = :status, updatedAt = :updatedAt'; + + const expressionAttributeValues = errorMessage + ? { ':status': status, ':error': errorMessage, ':updatedAt': new Date().toISOString() } + : { ':status': status, ':updatedAt': new Date().toISOString() }; + + await docClient.send(new UpdateCommand({ + TableName: ORDERS_TABLE, + Key: { orderId }, + UpdateExpression: updateExpression, + ExpressionAttributeNames: { + '#status': 'status' + }, + ExpressionAttributeValues: expressionAttributeValues + })); +} + diff --git a/lambda-durable-order-processing-sam/src/package.json b/lambda-durable-order-processing-sam/src/package.json new file mode 100644 index 000000000..ac34e40e5 --- /dev/null +++ b/lambda-durable-order-processing-sam/src/package.json @@ -0,0 +1,24 @@ +{ + "name": "lambda-durable-order-processing", + "version": "1.0.0", + "description": "Order processing workflow using Lambda Durable Functions", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@aws/durable-execution-sdk-js": "^1.0.0", + "@aws-sdk/client-dynamodb": "^3.0.0", + "@aws-sdk/lib-dynamodb": "^3.0.0", + "@aws-sdk/client-lambda": "^3.0.0" + }, + "keywords": [ + "aws", + "lambda", + "durable-functions", + "serverless", + "order-processing" + ], + "author": "", + "license": "MIT-0" +} diff --git a/lambda-durable-order-processing-sam/src/status.js b/lambda-durable-order-processing-sam/src/status.js new file mode 100644 index 000000000..326728332 --- /dev/null +++ b/lambda-durable-order-processing-sam/src/status.js @@ -0,0 +1,70 @@ +const { DynamoDBClient } = require('@aws-sdk/client-dynamodb'); +const { DynamoDBDocumentClient, GetCommand } = require('@aws-sdk/lib-dynamodb'); + +const dynamoClient = new DynamoDBClient({}); +const docClient = DynamoDBDocumentClient.from(dynamoClient); +const ORDERS_TABLE = process.env.ORDERS_TABLE; + +/** + * Order Status Function (Non-Durable) + * + * This is a separate, regular Lambda function for checking order status. + * It queries DynamoDB directly without invoking the durable function. + */ +exports.handler = async (event) => { + console.log('Checking order status', { event }); + + try { + const orderId = event.pathParameters?.orderId; + + if (!orderId) { + return { + statusCode: 400, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ error: 'Order ID is required' }) + }; + } + + const result = await docClient.send(new GetCommand({ + TableName: ORDERS_TABLE, + Key: { orderId } + })); + + if (!result.Item) { + return { + statusCode: 404, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + error: 'Order not found', + orderId + }) + }; + } + + return { + statusCode: 200, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + orderId: result.Item.orderId, + status: result.Item.status, + customerId: result.Item.customerId, + customerEmail: result.Item.customerEmail, + total: result.Item.total, + items: result.Item.items, + createdAt: result.Item.validatedAt || result.Item.updatedAt, + lastUpdated: result.Item.updatedAt, + errorMessage: result.Item.errorMessage + }) + }; + } catch (error) { + console.error('Error getting order status', { error }); + return { + statusCode: 500, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + error: 'Failed to get order status', + message: error.message + }) + }; + } +}; diff --git a/lambda-durable-order-processing-sam/template.yaml b/lambda-durable-order-processing-sam/template.yaml new file mode 100644 index 000000000..29bdac282 --- /dev/null +++ b/lambda-durable-order-processing-sam/template.yaml @@ -0,0 +1,148 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Order Processing Workflow using AWS Lambda Durable Functions (uksb-1tthgi812) (tag:lambda-durable-order-processing-sam) + +Globals: + Function: + Timeout: 900 + Runtime: nodejs22.x + Architectures: + - arm64 + Environment: + Variables: + ORDERS_TABLE: !Ref OrdersTable + +Resources: + # DynamoDB Table for Orders + OrdersTable: + Type: AWS::DynamoDB::Table + Properties: + TableName: !Sub '${AWS::StackName}-orders' + BillingMode: PAY_PER_REQUEST + AttributeDefinitions: + - AttributeName: orderId + AttributeType: S + KeySchema: + - AttributeName: orderId + KeyType: HASH + StreamSpecification: + StreamViewType: NEW_AND_OLD_IMAGES + + # Order Processing Durable Function + OrderProcessingFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: src/ + Handler: index.handler + Timeout: 120 + AutoPublishAlias: live + DurableConfig: + ExecutionTimeout: 86400 # 24 hours + RetentionPeriodInDays: 7 + Policies: + - DynamoDBCrudPolicy: + TableName: !Ref OrdersTable + - Statement: + - Effect: Allow + Action: + - lambda:CheckpointDurableExecution + Resource: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${AWS::StackName}-OrderProcessingFunction-*' + + # Order Status Function (Non-Durable) + OrderStatusFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: src/ + Handler: status.handler + Timeout: 30 + Policies: + - DynamoDBReadPolicy: + TableName: !Ref OrdersTable + + # API Gateway + OrderApi: + Type: AWS::Serverless::Api + Properties: + StageName: prod + Cors: + AllowMethods: "'POST, GET, OPTIONS'" + AllowHeaders: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'" + AllowOrigin: "'*'" + DefinitionBody: + openapi: 3.0.1 + paths: + /orders: + post: + responses: + '202': + description: Order accepted + x-amazon-apigateway-integration: + type: aws + httpMethod: POST + uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OrderProcessingFunction.Arn}:live/invocations' + requestParameters: + integration.request.header.X-Amz-Invocation-Type: "'Event'" + requestTemplates: + application/json: | + #set($orderId = "order-$context.requestTimeEpoch") + { + "orderId": "$orderId", + "body": $input.json('$') + } + responses: + default: + statusCode: '202' + responseTemplates: + application/json: | + #set($orderId = "order-$context.requestTimeEpoch") + { + "message": "Order processing initiated", + "orderId": "$orderId" + } + passthroughBehavior: never + /orders/{orderId}: + get: + parameters: + - name: orderId + in: path + required: true + schema: + type: string + responses: + '200': + description: Order status + x-amazon-apigateway-integration: + type: aws_proxy + httpMethod: POST + uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OrderStatusFunction.Arn}/invocations' + passthroughBehavior: when_no_match + + # Lambda Permissions for API Gateway + OrderProcessingFunctionInvokePermission: + Type: AWS::Lambda::Permission + Properties: + FunctionName: !Ref OrderProcessingFunction.Alias + Action: lambda:InvokeFunction + Principal: apigateway.amazonaws.com + SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${OrderApi}/*/POST/orders' + + OrderStatusFunctionInvokePermission: + Type: AWS::Lambda::Permission + Properties: + FunctionName: !Ref OrderStatusFunction + Action: lambda:InvokeFunction + Principal: apigateway.amazonaws.com + SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${OrderApi}/*/GET/orders/*' + +Outputs: + OrderApiEndpoint: + Description: API Gateway endpoint URL for order processing + Value: !Sub 'https://${OrderApi}.execute-api.${AWS::Region}.amazonaws.com/prod' + + OrderProcessingFunctionArn: + Description: Order Processing Durable Function ARN + Value: !GetAtt OrderProcessingFunction.Arn + + OrdersTableName: + Description: DynamoDB table name for orders + Value: !Ref OrdersTable diff --git a/lambda-durable-order-processing-sam/test-orders.json b/lambda-durable-order-processing-sam/test-orders.json new file mode 100644 index 000000000..5cb93e0c4 --- /dev/null +++ b/lambda-durable-order-processing-sam/test-orders.json @@ -0,0 +1,50 @@ +{ + "lowValueOrder": { + "customerId": "CUST-001", + "customerEmail": "customer1@example.com", + "items": [ + { + "productId": "BOOK-001", + "name": "Programming Book", + "quantity": 2, + "price": 45.99 + } + ] + }, + "highValueOrder": { + "customerId": "CUST-002", + "customerEmail": "customer2@example.com", + "items": [ + { + "productId": "SERVER-001", + "name": "Enterprise Server", + "quantity": 1, + "price": 3500.00 + } + ] + }, + "multiItemOrder": { + "customerId": "CUST-003", + "customerEmail": "customer3@example.com", + "items": [ + { + "productId": "LAPTOP-001", + "name": "Gaming Laptop", + "quantity": 1, + "price": 1299.99 + }, + { + "productId": "MOUSE-001", + "name": "Wireless Mouse", + "quantity": 2, + "price": 29.99 + }, + { + "productId": "KEYBOARD-001", + "name": "Mechanical Keyboard", + "quantity": 1, + "price": 149.99 + } + ] + } +}