diff --git a/aws-lambda-durable-functions-nodejs-saga-pattern/.gitignore b/aws-lambda-durable-functions-nodejs-saga-pattern/.gitignore new file mode 100644 index 000000000..f90cf609b --- /dev/null +++ b/aws-lambda-durable-functions-nodejs-saga-pattern/.gitignore @@ -0,0 +1,8 @@ +.aws-sam/ +*.pyc +__pycache__/ +.DS_Store +samconfig.toml +response.json +node_modules/ +package-lock.json diff --git a/aws-lambda-durable-functions-nodejs-saga-pattern/README.md b/aws-lambda-durable-functions-nodejs-saga-pattern/README.md new file mode 100644 index 000000000..1c62ab85d --- /dev/null +++ b/aws-lambda-durable-functions-nodejs-saga-pattern/README.md @@ -0,0 +1,184 @@ +# Saga Pattern with AWS Lambda durable functions (Node.js) + +This pattern demonstrates the Saga pattern for distributed transactions using AWS Lambda durable functions in Node.js. It coordinates a multi-step travel booking process (flight, hotel, car) with automatic compensating transactions on failure. + +## What is the Saga Pattern? + +The Saga pattern manages distributed transactions by breaking them into a sequence of local transactions. Each step can succeed or fail independently, and if any step fails, compensating transactions automatically undo previously completed steps to maintain data consistency. + +## Architecture + +This implementation uses a single Lambda durable function that: +1. Executes reservation steps sequentially (flight → hotel → car) +2. Tracks completed steps automatically via `context.step()` +3. Implements compensating transactions in reverse order on failure +4. Maintains state across retries without external storage + +## Key Features + +- **Automatic Checkpointing**: Each `context.step()` creates a checkpoint +- **Fault Tolerance**: Execution resumes from last checkpoint on failure +- **Compensating Transactions**: Automatic rollback in reverse order +- **No External State Store**: Durable functions handle state management +- **Failure Simulation**: Test different failure scenarios + +## How It Works + +### Success Flow +``` +Reserve Flight → Reserve Hotel → Reserve Car → SUCCESS +``` + +### Failure Flow (e.g., hotel fails) +``` +Reserve Flight → Reserve Hotel (FAILS) → Cancel Flight → ROLLBACK COMPLETE +``` + +### Failure Flow (e.g., car fails) +``` +Reserve Flight → Reserve Hotel → Reserve Car (FAILS) → Cancel Hotel → Cancel Flight → ROLLBACK COMPLETE +``` + +## Deployment + +### Prerequisites +- AWS CLI configured +- SAM CLI installed +- Node.js 22.x + +### Deploy +```bash +sam build +sam deploy --guided +``` + +Follow the prompts: +- Stack Name: `saga-pattern-demo` +- AWS Region: Your preferred region +- Confirm changes: Y +- Allow SAM CLI IAM role creation: Y +- Disable rollback: N +- Save arguments to configuration file: Y + +## Testing + +### Success Case +```bash +aws lambda invoke \ + --function-name :prod \ + --invocation-type Event \ + --payload '{"tripId":"trip-001","userId":"user-123"}' \ + --cli-binary-format raw-in-base64-out \ + response.json +``` + +### Simulate Flight Failure +```bash +aws lambda invoke \ + --function-name :prod \ + --invocation-type Event \ + --payload '{"tripId":"trip-002","userId":"user-123","simulateFailure":"flight"}' \ + --cli-binary-format raw-in-base64-out \ + response.json +``` + +### Simulate Hotel Failure (triggers flight cancellation) +```bash +aws lambda invoke \ + --function-name :prod \ + --invocation-type Event \ + --payload '{"tripId":"trip-003","userId":"user-123","simulateFailure":"hotel"}' \ + --cli-binary-format raw-in-base64-out \ + response.json +``` + +### Simulate Car Failure (triggers hotel and flight cancellation) +```bash +aws lambda invoke \ + --function-name :prod \ + --invocation-type Event \ + --payload '{"tripId":"trip-004","userId":"user-123","simulateFailure":"car"}' \ + --cli-binary-format raw-in-base64-out \ + response.json +``` + +## Viewing Logs + +```bash +sam logs --stack-name saga-pattern-demo --tail +``` + +Or view in CloudWatch Logs console. + +## Expected Output + +### Success Case +```json +{ + "status": "SUCCESS", + "message": "Trip booked successfully", + "tripId": "trip-001", + "userId": "user-123", + "reservations": { + "flight": { + "reservationId": "FL-1234567890", + "from": "SFO", + "to": "NYC", + "date": "2026-03-15", + "status": "CONFIRMED" + }, + "hotel": { + "reservationId": "HT-1234567891", + "name": "Grand Hotel NYC", + "checkIn": "2026-03-15", + "checkOut": "2026-03-18", + "status": "CONFIRMED" + }, + "car": { + "reservationId": "CR-1234567892", + "type": "SUV", + "pickupDate": "2026-03-15", + "returnDate": "2026-03-18", + "status": "CONFIRMED" + } + } +} +``` + +### Failure Case (hotel fails) +```json +{ + "status": "FAILED", + "message": "Trip booking failed, all reservations rolled back", + "tripId": "trip-003", + "userId": "user-123", + "error": "Hotel reservation failed - no rooms available", + "compensatedServices": ["flight"] +} +``` + +## How durable functions Enable Saga Pattern + +1. **State Management**: `context.step()` automatically creates checkpoints when completing the operation +2. **Idempotency**: Completed steps are executed exactly once, even on retries +3. **Compensation Tracking**: `completedSteps` array tracks what needs rollback +4. **Automatic Recovery**: Failed executions resume from last checkpoint +5. **No External Dependencies**: No DynamoDB or Step Functions needed + +## Cleanup + +```bash +sam delete --stack-name saga-pattern-demo +``` + +## Learn More + +- [AWS Lambda durable functions](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html) +- [Saga Pattern](https://microservices.io/patterns/data/saga.html) +- [Distributed Transactions](https://aws.amazon.com/blogs/compute/building-a-serverless-distributed-application-using-a-saga-orchestration-pattern/) + +--- + +© 2026 Amazon Web Services, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/aws-lambda-durable-functions-nodejs-saga-pattern/example-pattern.json b/aws-lambda-durable-functions-nodejs-saga-pattern/example-pattern.json new file mode 100644 index 000000000..4199eaecd --- /dev/null +++ b/aws-lambda-durable-functions-nodejs-saga-pattern/example-pattern.json @@ -0,0 +1,68 @@ +{ + "title": "Saga pattern with AWS Lambda durable functions in Node.js", + "description": "Implement the Saga pattern for distributed transactions using AWS Lambda durable functions with automatic compensating transactions", + "language": "Node.js", + "level": "200", + "framework": "AWS SAM", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern demonstrates the Saga pattern using AWS Lambda durable functions to coordinate distributed transactions across multiple services.", + "The orchestrator function executes a sequence of reservation steps (flight, hotel, car). Each step is checkpointed automatically using context.step().", + "If any step fails, compensating transactions execute in reverse order to rollback all completed operations, ensuring data consistency.", + "The durable function maintains execution state without requiring external storage like DynamoDB or Step Functions." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/aws-lambda-durable-functions-nodejs-saga-pattern", + "templateURL": "serverless-patterns/aws-lambda-durable-functions-nodejs-saga-pattern", + "projectFolder": "aws-lambda-durable-functions-nodejs-saga-pattern", + "templateFile": "template.yaml" + } + }, + "resources": { + "bullets": [ + { + "text": "AWS Lambda durable functions", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" + }, + { + "text": "Lambda Durable Execution SDK", + "link": "https://github.com/aws/aws-durable-execution-sdk-js" + }, + { + "text": "Saga Pattern", + "link": "https://microservices.io/patterns/data/saga.html" + }, + { + "text": "Building Serverless Distributed Applications with Saga", + "link": "https://aws.amazon.com/blogs/compute/building-a-serverless-distributed-application-using-a-saga-orchestration-pattern/" + } + ] + }, + "deploy": { + "text": [ + "sam build", + "sam deploy --guided" + ] + }, + "testing": { + "text": [ + "See the README in the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "sam delete --stack-name saga-pattern-demo" + ] + }, + "authors": [ + { + "name": "Surya Sai D", + "image": "", + "bio": "Surya works as a Technical Account Manager at AWS. He is an expert in Serverless frameworks and Event Driven Architectures. Surya is also passionate on technical writing and has contributed to AWS blogs and other Open Source Content.", + "linkedin": "surya-sai-d-64920416a" + } + ] +} diff --git a/aws-lambda-durable-functions-nodejs-saga-pattern/src/orchestrator/index.js b/aws-lambda-durable-functions-nodejs-saga-pattern/src/orchestrator/index.js new file mode 100644 index 000000000..129545178 --- /dev/null +++ b/aws-lambda-durable-functions-nodejs-saga-pattern/src/orchestrator/index.js @@ -0,0 +1,80 @@ +const { withDurableExecution } = require('@aws/durable-execution-sdk-js'); + +exports.handler = withDurableExecution(async (event, context) => { + context.logger.info('=== Saga Orchestrator Started ==='); + context.logger.info('Input:', JSON.stringify(event)); + + const { tripId, userId, simulateFailure } = event; + const completedSteps = []; + + try { + // Step 1: Reserve Flight + const flight = await context.step('reserveFlight', async () => { + context.logger.info('Reserving flight...'); + if (simulateFailure === 'flight') { + throw new Error('Flight reservation failed - no availability'); + } + const reservationId = `FL-${Date.now()}`; + context.logger.info(`Flight reserved: ${reservationId}`); + return { reservationId, from: 'SFO', to: 'NYC', date: '2026-03-15', status: 'CONFIRMED' }; + }); + completedSteps.push({ service: 'flight', data: flight }); + + // Step 2: Reserve Hotel + const hotel = await context.step('reserveHotel', async () => { + context.logger.info('Reserving hotel...'); + if (simulateFailure === 'hotel') { + throw new Error('Hotel reservation failed - no rooms available'); + } + const reservationId = `HT-${Date.now()}`; + context.logger.info(`Hotel reserved: ${reservationId}`); + return { reservationId, name: 'Grand Hotel NYC', checkIn: '2026-03-15', checkOut: '2026-03-18', status: 'CONFIRMED' }; + }, { retry: { maxAttempts: 1 } }); + completedSteps.push({ service: 'hotel', data: hotel }); + + // Step 3: Reserve Car + const car = await context.step('reserveCar', async () => { + context.logger.info('Reserving car...'); + if (simulateFailure === 'car') { + throw new Error('Car reservation failed - no vehicles available'); + } + const reservationId = `CR-${Date.now()}`; + context.logger.info(`Car reserved: ${reservationId}`); + return { reservationId, type: 'SUV', pickupDate: '2026-03-15', returnDate: '2026-03-18', status: 'CONFIRMED' }; + }, { retry: { maxAttempts: 1 } }); + completedSteps.push({ service: 'car', data: car }); + + context.logger.info('=== All Reservations Completed Successfully ==='); + return { + status: 'SUCCESS', + message: 'Trip booked successfully', + tripId, + userId, + reservations: { flight, hotel, car } + }; + + } catch (error) { + context.logger.error('=== Saga Failed - Initiating Compensating Transactions ==='); + context.logger.error('Error:', error.message); + + // Execute compensating transactions in REVERSE order + for (let i = completedSteps.length - 1; i >= 0; i--) { + const step = completedSteps[i]; + await context.step(`cancel_${step.service}`, async () => { + context.logger.info(`Cancelling ${step.service}: ${step.data.reservationId}`); + // Simulate cancellation logic + return { reservationId: step.data.reservationId, status: 'CANCELLED' }; + }); + } + + context.logger.info('=== All Compensating Transactions Completed ==='); + return { + status: 'FAILED', + message: 'Trip booking failed, all reservations rolled back', + tripId, + userId, + error: error.message, + compensatedServices: completedSteps.map(s => s.service) + }; + } +}); diff --git a/aws-lambda-durable-functions-nodejs-saga-pattern/src/orchestrator/package.json b/aws-lambda-durable-functions-nodejs-saga-pattern/src/orchestrator/package.json new file mode 100644 index 000000000..4d8f3db36 --- /dev/null +++ b/aws-lambda-durable-functions-nodejs-saga-pattern/src/orchestrator/package.json @@ -0,0 +1,7 @@ +{ + "name": "saga-orchestrator", + "version": "1.0.0", + "dependencies": { + "@aws/durable-execution-sdk-js": "^1.0.0" + } +} diff --git a/aws-lambda-durable-functions-nodejs-saga-pattern/template.yaml b/aws-lambda-durable-functions-nodejs-saga-pattern/template.yaml new file mode 100644 index 000000000..7e6111ade --- /dev/null +++ b/aws-lambda-durable-functions-nodejs-saga-pattern/template.yaml @@ -0,0 +1,29 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Saga pattern implementation using AWS Lambda durable functions in Node.js + +Resources: + SagaOrchestratorFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: src/orchestrator/ + Handler: index.handler + Runtime: nodejs22.x + Timeout: 900 + MemorySize: 512 + AutoPublishAlias: prod + DurableConfig: + ExecutionTimeout: 3600 + RetentionPeriodInDays: 7 + Environment: + Variables: + LOG_LEVEL: INFO + +Outputs: + SagaOrchestratorFunction: + Description: Saga Orchestrator Lambda Function ARN + Value: !GetAtt SagaOrchestratorFunction.Arn + + SagaOrchestratorFunctionAlias: + Description: Saga Orchestrator Function Alias (use this for invocation) + Value: !Sub '${SagaOrchestratorFunction}:prod'