diff --git a/saga-pattern-lambda-durable-functions/README.md b/saga-pattern-lambda-durable-functions/README.md new file mode 100644 index 000000000..7dda99b73 --- /dev/null +++ b/saga-pattern-lambda-durable-functions/README.md @@ -0,0 +1,435 @@ +# Saga Pattern with Lambda Durable Functions + +This pattern demonstrates how to implement the Saga pattern for distributed transactions using AWS Lambda Durable Functions. The example implements a travel booking system that coordinates flight, hotel, and car reservations with automatic compensating transactions (rollbacks) on failure. + +Durable functions are regular Lambda functions that allow you to write sequential code in your preferred programming language. They track progress, automatically retry on failures, and suspend execution for up to one year at defined points, without paying for idle compute during waits. + +This saga is built using the `aws-durable-execution-sdk` with Python 3.14 runtime and deployed using AWS CDK (TypeScript). + +**Important:** This application uses various AWS services and there are costs associated with these services after the Free Tier usage. Please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Architecture + +![Saga Architecture](./images/saga-lambda-durable-functions-architecture.svg) + +### Components + +- **Saga Orchestrator**: Durable Lambda function that coordinates the distributed transaction +- **Service Functions**: Individual Lambda functions for each service (flight, hotel, car) + - Reserve functions: Create reservations in DynamoDB + - Cancel functions: Rollback reservations (compensating transactions) +- **DynamoDB Tables**: Store reservation state for each service + +### Saga Flow + +**Success Path:** +``` +Reserve Flight → Reserve Hotel → Reserve Car → Complete +``` + +**Failure Path (with Compensation):** +``` +Reserve Flight → Reserve Hotel → Reserve Car (FAILS) + ↓ + Compensation (Reverse Order): + ↓ + Cancel Hotel + ↓ + Cancel Flight +``` + +## Requirements + +- [AWS Account](https://aws.amazon.com/free/) +- [AWS CLI](https://aws.amazon.com/cli/) installed and configured +- [Node.js 18+](https://nodejs.org/) installed +- [AWS CDK](https://aws.amazon.com/cdk/) installed (`npm install -g aws-cdk`) +- [Git](https://git-scm.com/) installed + +## Deployment Instructions + +### 1. Clone the Repository + +```bash +git clone https://github.com/aws-samples/serverless-patterns +cd serverless-patterns/saga-pattern-lambda-durable-functions/saga-pattern-cdk +``` + +### 2. Install Dependencies + +```bash +npm install +``` + +### 3. Bootstrap AWS CDK (First Time Only) + +```bash +cdk bootstrap +``` + +Or with a specific profile: + +```bash +cdk bootstrap --profile your-profile-name +``` + +### 4. Deploy the Stack + +```bash +npm run build +cdk deploy +``` + +The deployment creates: +- 3 DynamoDB tables (flight-bookings, hotel-reservations, car-rentals) +- 7 Lambda functions (1 orchestrator + 6 service functions) +- IAM roles and permissions + +### 5. Note the Outputs + +After deployment, save the function ARNs from the stack outputs: +- `SagaDurableFunctionArn` - Main orchestrator function +- `ReserveFlightFunctionArn` - Flight reservation service +- `CancelFlightFunctionArn` - Flight cancellation service +- `ReserveHotelFunctionArn` - Hotel reservation service +- `CancelHotelFunctionArn` - Hotel cancellation service +- `ReserveCarFunctionArn` - Car rental service +- `CancelCarFunctionArn` - Car cancellation service + +## How It Works + +The Saga pattern maintains data consistency across microservices without using distributed transactions. Instead, it uses: + +1. **Sequential Execution**: Services are invoked one after another +2. **State Tracking**: Each successful operation is tracked +3. **Compensating Transactions**: On failure, successful operations are undone in reverse order +4. **Idempotency**: Operations can be safely retried + +### Key Features + +- **No Distributed Locks**: Each service manages its own data independently +- **Eventual Consistency**: System reaches consistent state through compensations +- **Fault Tolerance**: Handles partial failures gracefully +- **Auditability**: Complete log trail of all operations +- **Automatic Retry**: Durable functions handle retries automatically + +## Testing + +The saga pattern implementation includes built-in failure flags for easy testing of compensating transactions. + +### Failure Flags + +Each reserve function supports a failure flag that simulates service failures: +- `failBookFlight` - Causes flight reservation to fail +- `failBookHotel` - Causes hotel reservation to fail +- `failBookCar` - Causes car reservation to fail + +When set to `true`, the service throws an exception before creating any records, triggering the saga compensation logic. + +### Test Scenario 1: Success Path (All Services Work) + +**Using AWS CLI (Async Invocation):** + +```bash +aws lambda invoke \ + --function-name saga-durable-function \ + --invocation-type Event \ + --cli-binary-format raw-in-base64-out \ + --payload file://test-success.json \ + response.json +``` + +**test-success.json:** +```json +{ + "passengerName": "Michael Johnson", + "flightNumber": "AA456", + "departure": "LAX", + "destination": "MIA", + "flightPrice": 380.00, + "guestName": "Michael Johnson", + "hotelName": "Hilton Downtown Miami", + "roomType": "Ocean View Suite", + "checkIn": "2026-04-10", + "checkOut": "2026-04-15", + "hotelPrice": 320.00, + "driverName": "Michael Johnson", + "carType": "Convertible", + "pickupLocation": "Miami Airport", + "dropoffLocation": "Miami Airport", + "pickupDate": "2026-04-10", + "dropoffDate": "2026-04-15", + "carPrice": 150.00 +} +``` + +**Expected Response:** +```json +{ + "success": true, + "transactionId": "uuid-here", + "message": "All travel arrangements completed successfully", + "bookings": { + "flight": "booking-id", + "hotel": "reservation-id", + "car": "rental-id" + } +} +``` +You can view the function output in the AWS Lambda Durable console as well: +![saga success response](./images/saga-success.png) + +**Validation:** +- All three services succeed +- DynamoDB records show `status: "RESERVED"` +- No compensations triggered + +### Test Scenario 2: Flight Fails Immediately + +**Using AWS CLI:** + +```bash +aws lambda invoke \ + --function-name saga-durable-function \ + --invocation-type Event \ + --cli-binary-format raw-in-base64-out \ + --payload file://test-fail-flight-booking.json \ + response.json +``` + +**test-fail-flight-booking.json:** +```json +{ + "passengerName": "Sarah Williams", + "departure": "ORD", + "destination": "SEA", + "failBookFlight": true +} +``` + +**Using Lambda Console:** + +1. Navigate to AWS Lambda Console +2. Open `saga-durable-function` +3. Go to "Test" tab +4. Click "Create new event" +5. Name it (e.g., "FailFlightTest") +6. Select invocation type: "Event" (for async execution) +7. Paste the JSON payload above +8. Click "Save" then "Test" + +**Expected Behavior:** +- Flight fails immediately +- Hotel and car never attempted +- No compensations needed (nothing to rollback) +- DynamoDB: No records created +![saga-failure-with-car-booking](./images/saga-failure-with-car.png) +### Test Scenario 3: Hotel Fails After Flight Succeeds + +**Using AWS CLI:** + +```bash +aws lambda invoke \ + --function-name saga-durable-function \ + --invocation-type Event \ + --cli-binary-format raw-in-base64-out \ + --payload file://test-fail-hotel-booking.json \ + response.json +``` + +**test-fail-hotel-booking.json:** +```json +{ + "passengerName": "David Martinez", + "departure": "DFW", + "destination": "BOS", + "flightPrice": 420.00, + "guestName": "David Martinez", + "hotelName": "Boston Harbor Hotel", + "failBookHotel": true +} +``` + +**Using Lambda Console:** + +Create a test event named "FailHotelTest" with invocation type "Event" (async) and the JSON payload above. + +**Expected Behavior:** +- Flight reserved +- Hotel fails +- Car never attempted +- Compensation: Flight gets cancelled +- DynamoDB: Flight record with `status: "CANCELLED"` + +**Console View:** + +After the failure, you should see the error state in the Lambda console showing the compensation process: + +![Saga Failure Console](./images/saga-failure-with-car.png) + + +### Test Scenario 4: Car Fails After Flight and Hotel Succeed + +**Using AWS CLI:** + +```bash +aws lambda invoke \ + --function-name saga-durable-function \ + --invocation-type Event \ + --cli-binary-format raw-in-base64-out \ + --payload file://test-fail-car-booking.json \ + response.json +``` + +**test-fail-car-booking.json:** +```json +{ + "passengerName": "Jane Smith", + "departure": "SFO", + "destination": "NYC", + "guestName": "Jane Smith", + "hotelName": "Marriott Times Square", + "driverName": "Jane Smith", + "failBookCar": true +} +``` + +**Using Lambda Console:** + +Create a test event named "FailCarTest" with invocation type "Event" (async) and the JSON payload above. + +**Expected Behavior:** +- Flight reserved +- Hotel reserved +- Car fails +- Compensation: Hotel cancelled, then flight cancelled (reverse order) +- DynamoDB: Flight and hotel records with `status: "CANCELLED"` + +**Console View:** + +After the failure, you should see the error state in the Lambda console showing the compensation process: + +![Saga Failure Console](./images/saga-failure-with-car.png) + +**CloudWatch Logs:** +``` +Step 1: Reserving flight... +Flight reserved successfully: +Step 2: Reserving hotel... +Hotel reserved successfully: +Step 3: Reserving car... +SIMULATED FAILURE: failBookCar flag is set to True +Error in saga workflow: Simulated car rental failure +Starting compensation (rollback) process... +Compensating: Cancelling hotel reservation +Compensating: Cancelling flight booking +Compensation process completed +``` + +### Verify Compensation in DynamoDB + +After triggering a failure, verify the compensation worked: + +```bash +# Check flight bookings +aws dynamodb scan --table-name saga-flight-bookings \ + --projection-expression "bookingId, #s, updatedAt" \ + --expression-attribute-names '{"#s":"status"}' + +# Check hotel reservations +aws dynamodb scan --table-name saga-hotel-reservations \ + --projection-expression "reservationId, #s, updatedAt" \ + --expression-attribute-names '{"#s":"status"}' + +# Check car rentals +aws dynamodb scan --table-name saga-car-rentals \ + --projection-expression "rentalId, #s, updatedAt" \ + --expression-attribute-names '{"#s":"status"}' +``` + +Look for: +- `status` field: `"RESERVED"` or `"CANCELLED"` +- `updatedAt` timestamp changes after cancellation +- No orphaned records + +### Testing in Lambda Console + +1. Navigate to AWS Lambda Console +2. Open `saga-durable-function` +3. Go to "Test" tab +4. Click "Create new event" +5. Name your test event (e.g., "SuccessTest", "FailCarTest") +6. **Important:** Select invocation type "Event" for async execution (recommended for durable functions) +7. Paste the JSON payload from any test scenario above +8. Click "Save" +9. Click "Test" to execute +10. Check CloudWatch Logs for execution results (async invocations return immediately) + +### Async vs Sync Invocation + +**Async Invocation (Event) - Recommended:** +- Lambda returns immediately with 202 Accepted +- Function executes in the background +- Check CloudWatch Logs for results +- Use `--invocation-type Event` in CLI +- Select "Event" in Lambda Console test configuration + +**Sync Invocation (RequestResponse):** +- Lambda waits for function to complete +- Returns the actual response +- Use `--invocation-type RequestResponse` in CLI (default) +- Select "RequestResponse" in Lambda Console test configuration + +For durable functions that can run for extended periods, async invocation is recommended. + +### Additional Test Files + +See `saga-pattern-cdk/lambda/test-events.json` for more test scenarios and `saga-pattern-cdk/TESTING.md` for comprehensive testing documentation. + +## Monitoring + +### CloudWatch Logs + +Each Lambda function logs to CloudWatch: +- `/aws/lambda/saga-durable-function` - Orchestrator logs +- `/aws/lambda/saga-reserve-flight` - Flight service logs +- `/aws/lambda/saga-reserve-hotel` - Hotel service logs +- `/aws/lambda/saga-reserve-car` - Car service logs + +### CloudWatch Insights Query + +Find all compensation events: + +``` +fields @timestamp, @message +| filter @message like /compensation/ +| sort @timestamp desc +| limit 100 +``` + +## Cleanup + +To avoid incurring charges, delete all resources: + +```bash +cdk destroy +``` + +Confirm the deletion when prompted. This will remove: +- All Lambda functions +- All DynamoDB tables and their data +- IAM roles and policies + +## Additional Resources + +- [AWS Lambda Durable Functions Documentation](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html) +- [Saga Pattern Overview](https://microservices.io/patterns/data/saga.html) +- [AWS CDK Documentation](https://docs.aws.amazon.com/cdk/) + + + +--- + +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/saga-pattern-lambda-durable-functions/images/saga-failure-with-car.png b/saga-pattern-lambda-durable-functions/images/saga-failure-with-car.png new file mode 100644 index 000000000..e91123061 Binary files /dev/null and b/saga-pattern-lambda-durable-functions/images/saga-failure-with-car.png differ diff --git a/saga-pattern-lambda-durable-functions/images/saga-lambda-durable-functions-architecture.svg b/saga-pattern-lambda-durable-functions/images/saga-lambda-durable-functions-architecture.svg new file mode 100644 index 000000000..771ac1046 --- /dev/null +++ b/saga-pattern-lambda-durable-functions/images/saga-lambda-durable-functions-architecture.svg @@ -0,0 +1,4 @@ + + + +
AWS Lambda Durable Functions

AWS Cloud
Reserve_Rental

Flights Table
Rental Table
Hotels Table
Cancel Flight
Reserve_Flight
failure
Reserve_Hotel
 success
success
Cancel_Hotel
Success
 success
failure
Failed
Cancel_Rental

failure
\ No newline at end of file diff --git a/saga-pattern-lambda-durable-functions/images/saga-success.png b/saga-pattern-lambda-durable-functions/images/saga-success.png new file mode 100644 index 000000000..8eb455a4e Binary files /dev/null and b/saga-pattern-lambda-durable-functions/images/saga-success.png differ diff --git a/saga-pattern-lambda-durable-functions/saga-pattern-cdk/.gitignore b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/.gitignore new file mode 100644 index 000000000..f60797b6a --- /dev/null +++ b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/.gitignore @@ -0,0 +1,8 @@ +*.js +!jest.config.js +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/saga-pattern-lambda-durable-functions/saga-pattern-cdk/.npmignore b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/.npmignore new file mode 100644 index 000000000..c1d6d45dc --- /dev/null +++ b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/saga-pattern-lambda-durable-functions/saga-pattern-cdk/README.md b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/README.md new file mode 100644 index 000000000..6f3789e00 --- /dev/null +++ b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/README.md @@ -0,0 +1,90 @@ +# Saga Pattern with Lambda Durable Functions + +This CDK project implements the saga pattern for distributed transactions using AWS Lambda Durable Functions. It demonstrates how to coordinate multiple microservices (flight, hotel, and car rental) with automatic rollback capabilities when any step fails. + +## Architecture Overview + +The saga orchestrator coordinates a travel booking workflow that: +1. Reserves a flight +2. Reserves a hotel room +3. Reserves a car rental + +If any step fails, the orchestrator automatically executes compensating transactions to rollback previously completed reservations, ensuring data consistency across all services. + +## Components + +### Lambda Functions + +**Saga Orchestrator (Durable Function)** +- Coordinates the entire workflow across all three services +- Manages state and execution history +- Automatically triggers rollback on failure +- Execution timeout: 1 hour +- Retention period: 30 days + +**Flight Service** +- `saga-reserve-flight`: Creates flight booking reservations +- `saga-cancel-flight`: Compensating transaction to cancel flight bookings + +**Hotel Service** +- `saga-reserve-hotel`: Creates hotel room reservations +- `saga-cancel-hotel`: Compensating transaction to cancel hotel reservations + +**Car Service** +- `saga-reserve-car`: Creates car rental reservations +- `saga-cancel-car`: Compensating transaction to cancel car rentals + +### DynamoDB Tables + +- `saga-flight-bookings`: Stores flight booking records +- `saga-hotel-reservations`: Stores hotel reservation records +- `saga-car-rentals`: Stores car rental records + +All tables use: +- Pay-per-request billing mode +- Point-in-time recovery enabled +- Partition key based on reservation/booking/rental ID + +## Prerequisites + +- Node.js and npm installed +- AWS CDK CLI installed (`npm install -g aws-cdk`) +- AWS credentials configured +- Python 3.14 runtime available in your AWS region + +## Deployment + +### Install dependencies +```bash +npm install +``` + +### Build the project +```bash +npm run build +``` + +### Deploy to AWS +```bash +npx cdk deploy +``` + +The deployment will output the ARNs of all Lambda functions and DynamoDB table names. + +## Useful Commands + +* `npm run build` - Compile TypeScript to JavaScript +* `npm run watch` - Watch for changes and compile automatically +* `npm run test` - Run Jest unit tests +* `npx cdk deploy` - Deploy this stack to your AWS account/region +* `npx cdk diff` - Compare deployed stack with current state +* `npx cdk synth` - Generate CloudFormation template +* `npx cdk destroy` - Remove all resources from AWS + +## Testing the Saga Pattern + +After deployment, invoke the saga orchestrator function with a test payload to see the distributed transaction in action. If any service fails, watch the automatic rollback compensate for completed steps. + +## Configuration + +The `cdk.json` file tells the CDK Toolkit how to execute your app and includes context values for feature flags and environment settings. diff --git a/saga-pattern-lambda-durable-functions/saga-pattern-cdk/bin/saga-pattern-cdk.ts b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/bin/saga-pattern-cdk.ts new file mode 100644 index 000000000..ca99b24c1 --- /dev/null +++ b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/bin/saga-pattern-cdk.ts @@ -0,0 +1,21 @@ +#!/usr/bin/env node +import * as cdk from 'aws-cdk-lib/core'; +import { SagaPatternCdkStack } from '../lib/saga-pattern-cdk-stack'; + +const app = new cdk.App(); +new SagaPatternCdkStack(app, 'SagaPatternCdkStack', { + description: "This templates deploys AWS Lambda Durable Function workflow that implements the SAGA pattern for travel booking use case." + /* If you don't specify 'env', this stack will be environment-agnostic. + * Account/Region-dependent features and context lookups will not work, + * but a single synthesized template can be deployed anywhere. */ + + /* Uncomment the next line to specialize this stack for the AWS Account + * and Region that are implied by the current CLI configuration. */ + // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, + + /* Uncomment the next line if you know exactly what Account and Region you + * want to deploy the stack to. */ + // env: { account: '123456789012', region: 'us-east-1' }, + + /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ +}); diff --git a/saga-pattern-lambda-durable-functions/saga-pattern-cdk/cdk.json b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/cdk.json new file mode 100644 index 000000000..4b5e9c99a --- /dev/null +++ b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/cdk.json @@ -0,0 +1,103 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/saga-pattern-cdk.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-signer:signingProfileNamePassedToCfn": true, + "@aws-cdk/aws-ecs-patterns:secGroupsDisablesImplicitOpenListener": true, + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-kms:applyImportedAliasPermissionsToPrincipal": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/core:explicitStackTags": true, + "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false, + "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, + "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true, + "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, + "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, + "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, + "@aws-cdk/core:enableAdditionalMetadataCollection": true, + "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": false, + "@aws-cdk/aws-s3:setUniqueReplicationRoleName": true, + "@aws-cdk/aws-events:requireEventBusPolicySid": true, + "@aws-cdk/core:aspectPrioritiesMutating": true, + "@aws-cdk/aws-dynamodb:retainTableReplica": true, + "@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": true, + "@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptions": true, + "@aws-cdk/aws-ec2:requirePrivateSubnetsForEgressOnlyInternetGateway": true, + "@aws-cdk/aws-s3:publicAccessBlockedByDefault": true, + "@aws-cdk/aws-lambda:useCdkManagedLogGroup": true, + "@aws-cdk/aws-elasticloadbalancingv2:networkLoadBalancerWithSecurityGroupByDefault": true, + "@aws-cdk/aws-ecs-patterns:uniqueTargetGroupId": true + } +} diff --git a/saga-pattern-lambda-durable-functions/saga-pattern-cdk/jest.config.js b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/jest.config.js new file mode 100644 index 000000000..fe2e9f679 --- /dev/null +++ b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/jest.config.js @@ -0,0 +1,9 @@ +module.exports = { + testEnvironment: 'node', + roots: ['/test'], + testMatch: ['**/*.test.ts'], + transform: { + '^.+\\.tsx?$': 'ts-jest' + }, + setupFilesAfterEnv: ['aws-cdk-lib/testhelpers/jest-autoclean'], +}; diff --git a/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/car/cancel_car.py b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/car/cancel_car.py new file mode 100644 index 000000000..9fe3dd248 --- /dev/null +++ b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/car/cancel_car.py @@ -0,0 +1,101 @@ +import json +import os +from datetime import datetime +import boto3 +from botocore.exceptions import ClientError + +dynamodb = boto3.resource('dynamodb') +table_name = os.environ.get('TABLE_NAME', 'saga-car-rentals') +table = dynamodb.Table(table_name) + +def lambda_handler(event, context): + """ + Cancel a car rental by updating the status + Handles both direct invocation and API Gateway events + """ + try: + # Handle different event formats + if isinstance(event, str): + event = json.loads(event) + + # If event has a 'body' field (API Gateway format), parse it + if 'body' in event: + body = json.loads(event['body']) if isinstance(event['body'], str) else event['body'] + else: + body = event + + # Extract rental ID from event + rental_id = body.get('rentalId') + + if not rental_id: + return { + 'statusCode': 400, + 'body': json.dumps({ + 'message': 'rentalId is required' + }) + } + + # Check if rental exists + response = table.get_item(Key={'rentalId': rental_id}) + + if 'Item' not in response: + return { + 'statusCode': 404, + 'body': json.dumps({ + 'message': f'Rental {rental_id} not found' + }) + } + + # Update rental status to CANCELLED + table.update_item( + Key={'rentalId': rental_id}, + UpdateExpression='SET #status = :status, updatedAt = :updatedAt', + ExpressionAttributeNames={'#status': 'status'}, + ExpressionAttributeValues={ + ':status': 'CANCELLED', + ':updatedAt': datetime.utcnow().isoformat() + } + ) + + return { + 'statusCode': 200, + 'body': json.dumps({ + 'message': 'Car rental cancelled successfully', + 'rentalId': rental_id, + 'status': 'CANCELLED' + }) + } + + except ClientError as e: + error_msg = f"DynamoDB error: {e.response['Error']['Message']}" + print(error_msg) + return { + 'statusCode': 500, + 'body': json.dumps({ + 'message': 'Failed to cancel car rental', + 'error': e.response['Error']['Message'], + 'errorType': 'DynamoDBError' + }) + } + except json.JSONDecodeError as e: + error_msg = f"JSON parsing error: {str(e)}" + print(error_msg) + return { + 'statusCode': 400, + 'body': json.dumps({ + 'message': 'Invalid JSON in request', + 'error': str(e), + 'errorType': 'JSONDecodeError' + }) + } + except Exception as e: + error_msg = f"Unexpected error: {str(e)}" + print(error_msg) + return { + 'statusCode': 500, + 'body': json.dumps({ + 'message': 'Failed to cancel car rental', + 'error': str(e), + 'errorType': type(e).__name__ + }) + } diff --git a/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/car/reserve_car.py b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/car/reserve_car.py new file mode 100644 index 000000000..36b258931 --- /dev/null +++ b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/car/reserve_car.py @@ -0,0 +1,115 @@ +import json +import os +import uuid +from datetime import datetime +from decimal import Decimal +import boto3 +from botocore.exceptions import ClientError + +dynamodb = boto3.resource('dynamodb') +table_name = os.environ.get('TABLE_NAME', 'saga-car-rentals') +table = dynamodb.Table(table_name) + +def lambda_handler(event, context): + """ + Reserve a car rental by creating a rental record in DynamoDB + Handles both direct invocation and API Gateway events + Supports failBookCar flag for testing saga compensation + """ + try: + # Handle different event formats + if isinstance(event, str): + event = json.loads(event) + + # If event has a 'body' field (API Gateway format), parse it + if 'body' in event: + body = json.loads(event['body']) if isinstance(event['body'], str) else event['body'] + else: + body = event + + # Check for failure flag to test saga compensation + if body.get('failBookCar', False): + print("SIMULATED FAILURE: failBookCar flag is set to True") + raise Exception("Simulated car rental failure for testing saga compensation") + + # Extract rental details from event + rental_id = body.get('rentalId', str(uuid.uuid4())) + driver_name = body.get('driverName', 'John Doe') + car_type = body.get('carType', 'Sedan') + pickup_location = body.get('pickupLocation', 'Airport') + dropoff_location = body.get('dropoffLocation', 'Airport') + pickup_date = body.get('pickupDate', datetime.utcnow().date().isoformat()) + dropoff_date = body.get('dropoffDate', datetime.utcnow().date().isoformat()) + price = Decimal(str(body.get('price', 89.99))) # Convert to Decimal for DynamoDB + + # Validate required fields + if not driver_name or not car_type: + return { + 'statusCode': 400, + 'body': json.dumps({ + 'message': 'Missing required fields: driverName, carType' + }) + } + + # Create rental record + item = { + 'rentalId': rental_id, + 'driverName': driver_name, + 'carType': car_type, + 'pickupLocation': pickup_location, + 'dropoffLocation': dropoff_location, + 'pickupDate': pickup_date, + 'dropoffDate': dropoff_date, + 'price': price, + 'status': 'RESERVED', + 'createdAt': datetime.utcnow().isoformat(), + 'updatedAt': datetime.utcnow().isoformat() + } + + # Put item in DynamoDB + table.put_item(Item=item) + + return { + 'statusCode': 200, + 'body': json.dumps({ + 'message': 'Car reserved successfully', + 'rentalId': rental_id, + 'carType': car_type, + 'status': 'RESERVED', + 'price': float(price) # Convert back to float for JSON response + }) + } + + except ClientError as e: + error_msg = f"DynamoDB error: {e.response['Error']['Message']}" + print(error_msg) + return { + 'statusCode': 500, + 'body': json.dumps({ + 'message': 'Failed to reserve car', + 'error': e.response['Error']['Message'], + 'errorType': 'DynamoDBError' + }) + } + except json.JSONDecodeError as e: + error_msg = f"JSON parsing error: {str(e)}" + print(error_msg) + return { + 'statusCode': 400, + 'body': json.dumps({ + 'message': 'Invalid JSON in request', + 'error': str(e), + 'errorType': 'JSONDecodeError' + }) + } + except Exception as e: + error_msg = f"Unexpected error: {str(e)}" + print(error_msg) + return { + 'statusCode': 500, + 'body': json.dumps({ + 'message': 'Failed to reserve car', + 'error': str(e), + 'errorType': type(e).__name__ + }) + } diff --git a/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/flight/cancel_flight.py b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/flight/cancel_flight.py new file mode 100644 index 000000000..7ecb322e8 --- /dev/null +++ b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/flight/cancel_flight.py @@ -0,0 +1,101 @@ +import json +import os +from datetime import datetime +import boto3 +from botocore.exceptions import ClientError + +dynamodb = boto3.resource('dynamodb') +table_name = os.environ.get('TABLE_NAME', 'saga-flight-bookings') +table = dynamodb.Table(table_name) + +def lambda_handler(event, context): + """ + Cancel a flight booking by updating the status + Handles both direct invocation and API Gateway events + """ + try: + # Handle different event formats + if isinstance(event, str): + event = json.loads(event) + + # If event has a 'body' field (API Gateway format), parse it + if 'body' in event: + body = json.loads(event['body']) if isinstance(event['body'], str) else event['body'] + else: + body = event + + # Extract booking ID from event + booking_id = body.get('bookingId') + + if not booking_id: + return { + 'statusCode': 400, + 'body': json.dumps({ + 'message': 'bookingId is required' + }) + } + + # Check if booking exists + response = table.get_item(Key={'bookingId': booking_id}) + + if 'Item' not in response: + return { + 'statusCode': 404, + 'body': json.dumps({ + 'message': f'Booking {booking_id} not found' + }) + } + + # Update booking status to CANCELLED + table.update_item( + Key={'bookingId': booking_id}, + UpdateExpression='SET #status = :status, updatedAt = :updatedAt', + ExpressionAttributeNames={'#status': 'status'}, + ExpressionAttributeValues={ + ':status': 'CANCELLED', + ':updatedAt': datetime.utcnow().isoformat() + } + ) + + return { + 'statusCode': 200, + 'body': json.dumps({ + 'message': 'Flight booking cancelled successfully', + 'bookingId': booking_id, + 'status': 'CANCELLED' + }) + } + + except ClientError as e: + error_msg = f"DynamoDB error: {e.response['Error']['Message']}" + print(error_msg) + return { + 'statusCode': 500, + 'body': json.dumps({ + 'message': 'Failed to cancel flight booking', + 'error': e.response['Error']['Message'], + 'errorType': 'DynamoDBError' + }) + } + except json.JSONDecodeError as e: + error_msg = f"JSON parsing error: {str(e)}" + print(error_msg) + return { + 'statusCode': 400, + 'body': json.dumps({ + 'message': 'Invalid JSON in request', + 'error': str(e), + 'errorType': 'JSONDecodeError' + }) + } + except Exception as e: + error_msg = f"Unexpected error: {str(e)}" + print(error_msg) + return { + 'statusCode': 500, + 'body': json.dumps({ + 'message': 'Failed to cancel flight booking', + 'error': str(e), + 'errorType': type(e).__name__ + }) + } diff --git a/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/flight/reserve_flight.py b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/flight/reserve_flight.py new file mode 100644 index 000000000..c1e6ecfd7 --- /dev/null +++ b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/flight/reserve_flight.py @@ -0,0 +1,111 @@ +import json +import os +import uuid +from datetime import datetime +from decimal import Decimal +import boto3 +from botocore.exceptions import ClientError + +dynamodb = boto3.resource('dynamodb') +table_name = os.environ.get('TABLE_NAME', 'saga-flight-bookings') +table = dynamodb.Table(table_name) + +def lambda_handler(event, context): + """ + Reserve a flight by creating a booking record in DynamoDB + Handles both direct invocation and API Gateway events + Supports failBookFlight flag for testing saga compensation + """ + try: + # Handle different event formats (direct invocation vs API Gateway) + if isinstance(event, str): + event = json.loads(event) + + # If event has a 'body' field (API Gateway format), parse it + if 'body' in event: + body = json.loads(event['body']) if isinstance(event['body'], str) else event['body'] + else: + body = event + + # Check for failure flag to test saga compensation + if body.get('failBookFlight', False): + print("SIMULATED FAILURE: failBookFlight flag is set to True") + raise Exception("Simulated flight booking failure for testing saga compensation") + + # Extract booking details from event + booking_id = body.get('bookingId', str(uuid.uuid4())) + passenger_name = body.get('passengerName', 'John Doe') + flight_number = body.get('flightNumber', f'FL{uuid.uuid4().hex[:6].upper()}') + departure = body.get('departure', 'JFK') + destination = body.get('destination', 'LAX') + price = Decimal(str(body.get('price', 299.99))) # Convert to Decimal for DynamoDB + + # Validate required fields + if not passenger_name or not departure or not destination: + return { + 'statusCode': 400, + 'body': json.dumps({ + 'message': 'Missing required fields: passengerName, departure, destination' + }) + } + + # Create booking record + item = { + 'bookingId': booking_id, + 'passengerName': passenger_name, + 'flightNumber': flight_number, + 'departure': departure, + 'destination': destination, + 'price': price, + 'status': 'RESERVED', + 'createdAt': datetime.utcnow().isoformat(), + 'updatedAt': datetime.utcnow().isoformat() + } + + # Put item in DynamoDB + table.put_item(Item=item) + + return { + 'statusCode': 200, + 'body': json.dumps({ + 'message': 'Flight reserved successfully', + 'bookingId': booking_id, + 'flightNumber': flight_number, + 'status': 'RESERVED', + 'price': float(price) # Convert back to float for JSON response + }) + } + + except ClientError as e: + error_msg = f"DynamoDB error: {e.response['Error']['Message']}" + print(error_msg) + return { + 'statusCode': 500, + 'body': json.dumps({ + 'message': 'Failed to reserve flight', + 'error': e.response['Error']['Message'], + 'errorType': 'DynamoDBError' + }) + } + except json.JSONDecodeError as e: + error_msg = f"JSON parsing error: {str(e)}" + print(error_msg) + return { + 'statusCode': 400, + 'body': json.dumps({ + 'message': 'Invalid JSON in request', + 'error': str(e), + 'errorType': 'JSONDecodeError' + }) + } + except Exception as e: + error_msg = f"Unexpected error: {str(e)}" + print(error_msg) + return { + 'statusCode': 500, + 'body': json.dumps({ + 'message': 'Failed to reserve flight', + 'error': str(e), + 'errorType': type(e).__name__ + }) + } diff --git a/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/hotel/cancel_hotel.py b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/hotel/cancel_hotel.py new file mode 100644 index 000000000..5b3c31b86 --- /dev/null +++ b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/hotel/cancel_hotel.py @@ -0,0 +1,101 @@ +import json +import os +from datetime import datetime +import boto3 +from botocore.exceptions import ClientError + +dynamodb = boto3.resource('dynamodb') +table_name = os.environ.get('TABLE_NAME', 'saga-hotel-reservations') +table = dynamodb.Table(table_name) + +def lambda_handler(event, context): + """ + Cancel a hotel reservation by updating the status + Handles both direct invocation and API Gateway events + """ + try: + # Handle different event formats + if isinstance(event, str): + event = json.loads(event) + + # If event has a 'body' field (API Gateway format), parse it + if 'body' in event: + body = json.loads(event['body']) if isinstance(event['body'], str) else event['body'] + else: + body = event + + # Extract reservation ID from event + reservation_id = body.get('reservationId') + + if not reservation_id: + return { + 'statusCode': 400, + 'body': json.dumps({ + 'message': 'reservationId is required' + }) + } + + # Check if reservation exists + response = table.get_item(Key={'reservationId': reservation_id}) + + if 'Item' not in response: + return { + 'statusCode': 404, + 'body': json.dumps({ + 'message': f'Reservation {reservation_id} not found' + }) + } + + # Update reservation status to CANCELLED + table.update_item( + Key={'reservationId': reservation_id}, + UpdateExpression='SET #status = :status, updatedAt = :updatedAt', + ExpressionAttributeNames={'#status': 'status'}, + ExpressionAttributeValues={ + ':status': 'CANCELLED', + ':updatedAt': datetime.utcnow().isoformat() + } + ) + + return { + 'statusCode': 200, + 'body': json.dumps({ + 'message': 'Hotel reservation cancelled successfully', + 'reservationId': reservation_id, + 'status': 'CANCELLED' + }) + } + + except ClientError as e: + error_msg = f"DynamoDB error: {e.response['Error']['Message']}" + print(error_msg) + return { + 'statusCode': 500, + 'body': json.dumps({ + 'message': 'Failed to cancel hotel reservation', + 'error': e.response['Error']['Message'], + 'errorType': 'DynamoDBError' + }) + } + except json.JSONDecodeError as e: + error_msg = f"JSON parsing error: {str(e)}" + print(error_msg) + return { + 'statusCode': 400, + 'body': json.dumps({ + 'message': 'Invalid JSON in request', + 'error': str(e), + 'errorType': 'JSONDecodeError' + }) + } + except Exception as e: + error_msg = f"Unexpected error: {str(e)}" + print(error_msg) + return { + 'statusCode': 500, + 'body': json.dumps({ + 'message': 'Failed to cancel hotel reservation', + 'error': str(e), + 'errorType': type(e).__name__ + }) + } diff --git a/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/hotel/reserve_hotel.py b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/hotel/reserve_hotel.py new file mode 100644 index 000000000..1e73aeb38 --- /dev/null +++ b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/hotel/reserve_hotel.py @@ -0,0 +1,114 @@ +import json +import os +import uuid +from datetime import datetime +from decimal import Decimal +import boto3 +from botocore.exceptions import ClientError + +dynamodb = boto3.resource('dynamodb') +table_name = os.environ.get('TABLE_NAME', 'saga-hotel-reservations') +table = dynamodb.Table(table_name) + +def lambda_handler(event, context): + """ + Reserve a hotel by creating a reservation record in DynamoDB + Handles both direct invocation and API Gateway events + Supports failBookHotel flag for testing saga compensation + """ + try: + # Handle different event formats + if isinstance(event, str): + event = json.loads(event) + + # If event has a 'body' field (API Gateway format), parse it + if 'body' in event: + body = json.loads(event['body']) if isinstance(event['body'], str) else event['body'] + else: + body = event + + # Check for failure flag to test saga compensation + if body.get('failBookHotel', False): + print("SIMULATED FAILURE: failBookHotel flag is set to True") + raise Exception("Simulated hotel reservation failure for testing saga compensation") + + # Extract reservation details from event + reservation_id = body.get('reservationId', str(uuid.uuid4())) + guest_name = body.get('guestName', 'John Doe') + hotel_name = body.get('hotelName', 'Grand Hotel') + room_type = body.get('roomType', 'Deluxe Suite') + check_in = body.get('checkIn', datetime.utcnow().date().isoformat()) + check_out = body.get('checkOut', datetime.utcnow().date().isoformat()) + price = Decimal(str(body.get('price', 199.99))) # Convert to Decimal for DynamoDB + + # Validate required fields + if not guest_name or not hotel_name: + return { + 'statusCode': 400, + 'body': json.dumps({ + 'message': 'Missing required fields: guestName, hotelName' + }) + } + + # Create reservation record + item = { + 'reservationId': reservation_id, + 'guestName': guest_name, + 'hotelName': hotel_name, + 'roomType': room_type, + 'checkIn': check_in, + 'checkOut': check_out, + 'price': price, + 'status': 'RESERVED', + 'createdAt': datetime.utcnow().isoformat(), + 'updatedAt': datetime.utcnow().isoformat() + } + + # Put item in DynamoDB + table.put_item(Item=item) + + return { + 'statusCode': 200, + 'body': json.dumps({ + 'message': 'Hotel reserved successfully', + 'reservationId': reservation_id, + 'hotelName': hotel_name, + 'roomType': room_type, + 'status': 'RESERVED', + 'price': float(price) # Convert back to float for JSON response + }) + } + + except ClientError as e: + error_msg = f"DynamoDB error: {e.response['Error']['Message']}" + print(error_msg) + return { + 'statusCode': 500, + 'body': json.dumps({ + 'message': 'Failed to reserve hotel', + 'error': e.response['Error']['Message'], + 'errorType': 'DynamoDBError' + }) + } + except json.JSONDecodeError as e: + error_msg = f"JSON parsing error: {str(e)}" + print(error_msg) + return { + 'statusCode': 400, + 'body': json.dumps({ + 'message': 'Invalid JSON in request', + 'error': str(e), + 'errorType': 'JSONDecodeError' + }) + } + except Exception as e: + error_msg = f"Unexpected error: {str(e)}" + print(error_msg) + return { + 'statusCode': 500, + 'body': json.dumps({ + 'message': 'Failed to reserve hotel', + 'error': str(e), + 'errorType': type(e).__name__ + }) + } diff --git a/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/saga-workflow/index.py b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/saga-workflow/index.py new file mode 100644 index 000000000..8b23172ee --- /dev/null +++ b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/saga-workflow/index.py @@ -0,0 +1,238 @@ +from aws_durable_execution_sdk_python.config import Duration +from aws_durable_execution_sdk_python.context import DurableContext, StepContext, durable_step +from aws_durable_execution_sdk_python.execution import durable_execution +import random +import datetime +import boto3 +import logging +import json +import os +import uuid + +dynamodb = boto3.client('dynamodb') + +# Get function names from environment variables +RESERVE_FLIGHT_FUNCTION = os.environ.get('RESERVE_FLIGHT_FUNCTION', 'saga-reserve-flight') +CANCEL_FLIGHT_FUNCTION = os.environ.get('CANCEL_FLIGHT_FUNCTION', 'saga-cancel-flight') +RESERVE_HOTEL_FUNCTION = os.environ.get('RESERVE_HOTEL_FUNCTION', 'saga-reserve-hotel') +CANCEL_HOTEL_FUNCTION = os.environ.get('CANCEL_HOTEL_FUNCTION', 'saga-cancel-hotel') +RESERVE_CAR_FUNCTION = os.environ.get('RESERVE_CAR_FUNCTION', 'saga-reserve-car') +CANCEL_CAR_FUNCTION = os.environ.get('CANCEL_CAR_FUNCTION', 'saga-cancel-car') + + +@durable_execution +def lambda_handler(event: dict, context: DurableContext) -> dict: + """ + Saga orchestrator that coordinates distributed transactions across flight, hotel, and car services. + Implements compensating transactions (cancellations) if any step fails. + Uses direct context.invoke() calls instead of durable steps. + """ + print(f"Saga workflow started at: {datetime.datetime.now()}") + context.logger.info(f"Received event: {json.dumps(event)}") + + # Generate unique IDs for this saga transaction + transaction_id = str(uuid.uuid4()) + booking_id = None + reservation_id = None + rental_id = None + + try: + # Step 1: Reserve the flight + context.logger.info("Step 1: Reserving flight...") + flight_data = { + "bookingId": str(uuid.uuid4()), + "passengerName": event.get("passengerName", "John Doe"), + "flightNumber": event.get("flightNumber", f"FL{uuid.uuid4().hex[:6].upper()}"), + "departure": event.get("departure", "JFK"), + "destination": event.get("destination", "LAX"), + "price": event.get("flightPrice", 299.99), + "failBookFlight": event.get("failBookFlight", False) # Pass failure flag + } + + flight_result = context.invoke( + function_name=RESERVE_FLIGHT_FUNCTION, + payload=flight_data, + name="reserve_flight_invocation" + ) + + if flight_result is None: + context.logger.error("Flight reservation returned None - invocation may have failed") + raise Exception("Flight reservation returned None - invocation may have failed") + + # If it's a string, parse it first + if isinstance(flight_result, str): + flight_result = json.loads(flight_result) + + # Parse the Lambda response format + if isinstance(flight_result, dict) and 'body' in flight_result: + flight_body = json.loads(flight_result['body']) if isinstance(flight_result['body'], str) else flight_result['body'] + if flight_result.get('statusCode') != 200: + raise Exception(f"Flight reservation failed: {flight_body.get('message')}") + booking_id = flight_body.get('bookingId') + elif isinstance(flight_result, dict): + booking_id = flight_result.get('bookingId') + else: + raise Exception(f"Unexpected flight result format: {type(flight_result)}") + + context.logger.info(f"Flight reserved successfully: {booking_id}") + + # Step 2: Reserve hotel + context.logger.info("Step 2: Reserving hotel...") + hotel_data = { + "reservationId": str(uuid.uuid4()), + "guestName": event.get("guestName", "John Doe"), + "hotelName": event.get("hotelName", "Grand Hotel"), + "roomType": event.get("roomType", "Deluxe Suite"), + "checkIn": event.get("checkIn", datetime.datetime.utcnow().date().isoformat()), + "checkOut": event.get("checkOut", datetime.datetime.utcnow().date().isoformat()), + "price": event.get("hotelPrice", 199.99), + "failBookHotel": event.get("failBookHotel", False) # Pass failure flag + } + + hotel_result = context.invoke( + function_name=RESERVE_HOTEL_FUNCTION, + payload=hotel_data, + name="reserve_hotel_invocation" + ) + + context.logger.info(f"Hotel result after invoke: {hotel_result}") + + if hotel_result is None: + raise Exception("Hotel reservation returned None - invocation may have failed") + + # If it's a string, parse it first + if isinstance(hotel_result, str): + hotel_result = json.loads(hotel_result) + + # parse the Lambda response format + if isinstance(hotel_result, dict) and 'body' in hotel_result: + hotel_body = json.loads(hotel_result['body']) if isinstance(hotel_result['body'], str) else hotel_result['body'] + if hotel_result.get('statusCode') != 200: + raise Exception(f"Hotel reservation failed: {hotel_body.get('message')}") + reservation_id = hotel_body.get('reservationId') + elif isinstance(hotel_result, dict): + reservation_id = hotel_result.get('reservationId') + else: + raise Exception(f"Unexpected hotel result format: {type(hotel_result)}") + + context.logger.info(f"Hotel reserved successfully: {reservation_id}") + + # Step 3: Reserve car + context.logger.info("Step 3: Reserving car...") + car_data = { + "rentalId": str(uuid.uuid4()), + "driverName": event.get("driverName", "John Doe"), + "carType": event.get("carType", "Sedan"), + "pickupLocation": event.get("pickupLocation", "Airport"), + "dropoffLocation": event.get("dropoffLocation", "Airport"), + "pickupDate": event.get("pickupDate", datetime.datetime.utcnow().date().isoformat()), + "dropoffDate": event.get("dropoffDate", datetime.datetime.utcnow().date().isoformat()), + "price": event.get("carPrice", 89.99), + "failBookCar": event.get("failBookCar", False) # Pass failure flag + } + + car_result = context.invoke( + function_name=RESERVE_CAR_FUNCTION, + payload=car_data, + name="reserve_car_invocation" + ) + + context.logger.info(f"Car result after invoke: {car_result}") + + if car_result is None: + raise Exception("Car reservation returned None - invocation may have failed") + + # If it's a string, parse it first + if isinstance(car_result, str): + car_result = json.loads(car_result) + + # Parse the Lambda response format + if isinstance(car_result, dict) and 'body' in car_result: + car_body = json.loads(car_result['body']) if isinstance(car_result['body'], str) else car_result['body'] + if car_result.get('statusCode') != 200: + raise Exception(f"Car reservation failed: {car_body.get('message')}") + rental_id = car_body.get('rentalId') + elif isinstance(car_result, dict): + rental_id = car_result.get('rentalId') + else: + raise Exception(f"Unexpected car result format: {type(car_result)}") + + context.logger.info(f"Car reserved successfully: {rental_id}") + + # All reservations successful + context.logger.info("All reservations completed successfully!") + + return { + "success": True, + "transactionId": transaction_id, + "message": "All travel arrangements completed successfully", + "bookings": { + "flight": booking_id, + "hotel": reservation_id, + "car": rental_id + } + } + + except Exception as e: + # Saga compensation: rollback all successful reservations + context.logger.error(f"Error in saga workflow: {str(e)}") + context.logger.info("Starting compensation (rollback) process...") + + compensation_results = [] + + # Cancel car if it was reserved + if rental_id: + try: + context.logger.info(f"Compensating: Cancelling car rental {rental_id}") + # Direct invoke call for cancellation + cancel_car_result = context.invoke( + function_name=CANCEL_CAR_FUNCTION, + payload={"rentalId": rental_id}, + name="cancel_car_invocation" + ) + compensation_results.append({"car": "cancelled", "rentalId": rental_id}) + except Exception as cancel_error: + context.logger.error(f"Failed to cancel car: {str(cancel_error)}") + compensation_results.append({"car": "cancellation_failed", "error": str(cancel_error)}) + + # Cancel hotel if it was reserved + if reservation_id: + try: + context.logger.info(f"Compensating: Cancelling hotel reservation {reservation_id}") + # Direct invoke call for cancellation + cancel_hotel_result = context.invoke( + function_name=CANCEL_HOTEL_FUNCTION, + payload={"reservationId": reservation_id}, + name="cancel_hotel_invocation" + ) + compensation_results.append({"hotel": "cancelled", "reservationId": reservation_id}) + except Exception as cancel_error: + context.logger.error(f"Failed to cancel hotel: {str(cancel_error)}") + compensation_results.append({"hotel": "cancellation_failed", "error": str(cancel_error)}) + + # Cancel flight if it was reserved + if booking_id: + try: + context.logger.info(f"Compensating: Cancelling flight booking {booking_id}") + # Direct invoke call for cancellation + cancel_flight_result = context.invoke( + function_name=CANCEL_FLIGHT_FUNCTION, + payload={"bookingId": booking_id}, + name="cancel_flight_invocation" + ) + compensation_results.append({"flight": "cancelled", "bookingId": booking_id}) + except Exception as cancel_error: + context.logger.error(f"Failed to cancel flight: {str(cancel_error)}") + compensation_results.append({"flight": "cancellation_failed", "error": str(cancel_error)}) + + context.logger.info("Compensation process completed") + + # Raise an exception with compensation details + error_details = { + "transactionId": transaction_id, + "originalError": str(e), + "compensations": compensation_results, + "message": f"Transaction failed and rolled back: {str(e)}" + } + + raise Exception(f"Saga transaction failed and compensated. Details: {json.dumps(error_details)}") diff --git a/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/saga-workflow/saga-layer.zip b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/saga-workflow/saga-layer.zip new file mode 100644 index 000000000..b5a336461 Binary files /dev/null and b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lambda/saga-workflow/saga-layer.zip differ diff --git a/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lib/saga-pattern-cdk-stack.ts b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lib/saga-pattern-cdk-stack.ts new file mode 100644 index 000000000..80e7635f8 --- /dev/null +++ b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/lib/saga-pattern-cdk-stack.ts @@ -0,0 +1,274 @@ +import * as cdk from 'aws-cdk-lib/core'; +import { Construct } from 'constructs'; +import { aws_dynamodb, aws_lambda, aws_logs, Duration, RemovalPolicy } from 'aws-cdk-lib'; + +export class SagaPatternCdkStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // Hotel reservations table + const hotelTable = new aws_dynamodb.Table(this, 'HotelReservationsTable', { + tableName: 'saga-hotel-reservations', + partitionKey: { name: 'reservationId', type: aws_dynamodb.AttributeType.STRING }, + billingMode: aws_dynamodb.BillingMode.PAY_PER_REQUEST, + removalPolicy: RemovalPolicy.DESTROY, + }); + + // Flight bookings table + const flightTable = new aws_dynamodb.Table(this, 'FlightBookingsTable', { + tableName: 'saga-flight-bookings', + partitionKey: { name: 'bookingId', type: aws_dynamodb.AttributeType.STRING }, + billingMode: aws_dynamodb.BillingMode.PAY_PER_REQUEST, + removalPolicy: RemovalPolicy.DESTROY, + }); + + // Car rentals table + const carTable = new aws_dynamodb.Table(this, 'CarRentalsTable', { + tableName: 'saga-car-rentals', + partitionKey: { name: 'rentalId', type: aws_dynamodb.AttributeType.STRING }, + billingMode: aws_dynamodb.BillingMode.PAY_PER_REQUEST, + removalPolicy: RemovalPolicy.DESTROY, + }); + + // Flight Lambda functions + const reserveFlightLogGroup = new aws_logs.LogGroup(this, 'ReserveFlightLogGroup', { + logGroupName: '/aws/lambda/saga-reserve-flight', + retention: aws_logs.RetentionDays.ONE_WEEK, + removalPolicy: RemovalPolicy.DESTROY, + }); + + const reserveFlight = new aws_lambda.Function(this, 'ReserveFlightFunction', { + functionName: 'saga-reserve-flight', + description: 'Creates a new flight booking reservation and stores it in DynamoDB as part of the saga transaction', + runtime: aws_lambda.Runtime.PYTHON_3_14, + handler: 'reserve_flight.lambda_handler', + code: aws_lambda.Code.fromAsset('lambda/flight'), + tracing: aws_lambda.Tracing.ACTIVE, + loggingFormat: aws_lambda.LoggingFormat.JSON, + timeout: Duration.minutes(1), + logGroup: reserveFlightLogGroup, + environment: { + TABLE_NAME: flightTable.tableName + }, + }); + + const cancelFlightLogGroup = new aws_logs.LogGroup(this, 'CancelFlightLogGroup', { + logGroupName: '/aws/lambda/saga-cancel-flight', + retention: aws_logs.RetentionDays.ONE_WEEK, + removalPolicy: RemovalPolicy.DESTROY, + }); + + const cancelFlight = new aws_lambda.Function(this, 'CancelFlightFunction', { + functionName: 'saga-cancel-flight', + description: 'Compensating transaction that cancels a flight booking when the saga needs to rollback', + runtime: aws_lambda.Runtime.PYTHON_3_14, + handler: 'cancel_flight.lambda_handler', + code: aws_lambda.Code.fromAsset('lambda/flight'), + tracing: aws_lambda.Tracing.ACTIVE, + loggingFormat: aws_lambda.LoggingFormat.JSON, + timeout: Duration.minutes(1), + logGroup: cancelFlightLogGroup, + environment: { + TABLE_NAME: flightTable.tableName + }, + }); + + // Hotel Lambda functions + const reserveHotelLogGroup = new aws_logs.LogGroup(this, 'ReserveHotelLogGroup', { + logGroupName: '/aws/lambda/saga-reserve-hotel', + retention: aws_logs.RetentionDays.ONE_WEEK, + removalPolicy: RemovalPolicy.DESTROY, + }); + + const reserveHotel = new aws_lambda.Function(this, 'ReserveHotelFunction', { + functionName: 'saga-reserve-hotel', + description: 'Creates a new hotel room reservation and stores it in DynamoDB as part of the saga transaction', + runtime: aws_lambda.Runtime.PYTHON_3_14, + handler: 'reserve_hotel.lambda_handler', + code: aws_lambda.Code.fromAsset('lambda/hotel'), + tracing: aws_lambda.Tracing.ACTIVE, + loggingFormat: aws_lambda.LoggingFormat.JSON, + timeout: Duration.minutes(1), + logGroup: reserveHotelLogGroup, + environment: { + TABLE_NAME: hotelTable.tableName + }, + }); + + const cancelHotelLogGroup = new aws_logs.LogGroup(this, 'CancelHotelLogGroup', { + logGroupName: '/aws/lambda/saga-cancel-hotel', + retention: aws_logs.RetentionDays.ONE_WEEK, + removalPolicy: RemovalPolicy.DESTROY, + }); + + const cancelHotel = new aws_lambda.Function(this, 'CancelHotelFunction', { + functionName: 'saga-cancel-hotel', + description: 'Compensating transaction that cancels a hotel reservation when the saga needs to rollback', + runtime: aws_lambda.Runtime.PYTHON_3_14, + handler: 'cancel_hotel.lambda_handler', + code: aws_lambda.Code.fromAsset('lambda/hotel'), + tracing: aws_lambda.Tracing.ACTIVE, + loggingFormat: aws_lambda.LoggingFormat.JSON, + timeout: Duration.minutes(1), + logGroup: cancelHotelLogGroup, + environment: { + TABLE_NAME: hotelTable.tableName + }, + }); + + // Car Lambda functions + const reserveCarLogGroup = new aws_logs.LogGroup(this, 'ReserveCarLogGroup', { + logGroupName: '/aws/lambda/saga-reserve-car', + retention: aws_logs.RetentionDays.ONE_WEEK, + removalPolicy: RemovalPolicy.DESTROY, + }); + + const reserveCar = new aws_lambda.Function(this, 'ReserveCarFunction', { + functionName: 'saga-reserve-car', + description: 'Creates a new car rental reservation and stores it in DynamoDB as part of the saga transaction', + runtime: aws_lambda.Runtime.PYTHON_3_14, + handler: 'reserve_car.lambda_handler', + code: aws_lambda.Code.fromAsset('lambda/car'), + tracing: aws_lambda.Tracing.ACTIVE, + loggingFormat: aws_lambda.LoggingFormat.JSON, + timeout: Duration.minutes(1), + logGroup: reserveCarLogGroup, + environment: { + TABLE_NAME: carTable.tableName + }, + }); + + const cancelCarLogGroup = new aws_logs.LogGroup(this, 'CancelCarLogGroup', { + logGroupName: '/aws/lambda/saga-cancel-car', + retention: aws_logs.RetentionDays.ONE_WEEK, + removalPolicy: RemovalPolicy.DESTROY, + }); + + const cancelCar = new aws_lambda.Function(this, 'CancelCarFunction', { + functionName: 'saga-cancel-car', + description: 'Compensating transaction that cancels a car rental reservation when the saga needs to rollback', + runtime: aws_lambda.Runtime.PYTHON_3_14, + handler: 'cancel_car.lambda_handler', + code: aws_lambda.Code.fromAsset('lambda/car'), + tracing: aws_lambda.Tracing.ACTIVE, + loggingFormat: aws_lambda.LoggingFormat.JSON, + timeout: Duration.minutes(1), + logGroup: cancelCarLogGroup, + environment: { + TABLE_NAME: carTable.tableName + }, + }); + + // Saga orchestrator durable function + const sagaDurableFunctionLogGroup = new aws_logs.LogGroup(this, 'SagaDurableFunctionLogGroup', { + logGroupName: '/aws/lambda/saga-durable-function', + retention: aws_logs.RetentionDays.ONE_WEEK, + removalPolicy: RemovalPolicy.DESTROY, + }); + + const sagaDependenciesLayer = new aws_lambda.LayerVersion(this, 'SagaDependenciesLayer', { + code: aws_lambda.Code.fromAsset('lambda/saga-workflow/saga-layer.zip'), + compatibleRuntimes: [aws_lambda.Runtime.PYTHON_3_14], + description: 'Saga pattern dependencies (durable SDK, boto3, etc.)', + }); + + const sagaDurableFunction = new aws_lambda.Function(this, 'SagaDurableFunction', { + runtime: aws_lambda.Runtime.PYTHON_3_14, + tracing: aws_lambda.Tracing.ACTIVE, + functionName: 'saga-durable-function', + description: 'Orchestrates the saga pattern workflow by coordinating flight, hotel, and car reservations with automatic rollback on failure', + loggingFormat: aws_lambda.LoggingFormat.JSON, + handler: 'index.lambda_handler', + logGroup: sagaDurableFunctionLogGroup, + layers: [sagaDependenciesLayer], + durableConfig: { + executionTimeout: Duration.hours(1), + retentionPeriod: Duration.days(30) + }, + code: aws_lambda.Code.fromAsset('lambda/saga-workflow'), + environment: { + HOTEL_TABLE_NAME: hotelTable.tableName, + FLIGHT_TABLE_NAME: flightTable.tableName, + CAR_TABLE_NAME: carTable.tableName, + RESERVE_FLIGHT_FUNCTION: reserveFlight.functionName, + CANCEL_FLIGHT_FUNCTION: cancelFlight.functionName, + RESERVE_HOTEL_FUNCTION: reserveHotel.functionName, + CANCEL_HOTEL_FUNCTION: cancelHotel.functionName, + RESERVE_CAR_FUNCTION: reserveCar.functionName, + CANCEL_CAR_FUNCTION: cancelCar.functionName, + }, + }); + + // Grant Lambda permissions to access DynamoDB tables + hotelTable.grantReadWriteData(sagaDurableFunction); + flightTable.grantReadWriteData(sagaDurableFunction); + carTable.grantReadWriteData(sagaDurableFunction); + + // Grant individual Lambda functions access to their respective tables + flightTable.grantReadWriteData(reserveFlight); + flightTable.grantReadWriteData(cancelFlight); + hotelTable.grantReadWriteData(reserveHotel); + hotelTable.grantReadWriteData(cancelHotel); + carTable.grantReadWriteData(reserveCar); + carTable.grantReadWriteData(cancelCar); + + // Grant saga orchestrator permission to invoke service functions + reserveFlight.grantInvoke(sagaDurableFunction); + cancelFlight.grantInvoke(sagaDurableFunction); + reserveHotel.grantInvoke(sagaDurableFunction); + cancelHotel.grantInvoke(sagaDurableFunction); + reserveCar.grantInvoke(sagaDurableFunction); + cancelCar.grantInvoke(sagaDurableFunction); + + // Outputs + new cdk.CfnOutput(this, 'SagaDurableFunctionArn', { + value: sagaDurableFunction.functionArn, + description: 'The ARN of the Saga durable function', + }); + + new cdk.CfnOutput(this, 'ReserveFlightFunctionArn', { + value: reserveFlight.functionArn, + description: 'Reserve flight function ARN', + }); + + new cdk.CfnOutput(this, 'CancelFlightFunctionArn', { + value: cancelFlight.functionArn, + description: 'Cancel flight function ARN', + }); + + new cdk.CfnOutput(this, 'ReserveHotelFunctionArn', { + value: reserveHotel.functionArn, + description: 'Reserve hotel function ARN', + }); + + new cdk.CfnOutput(this, 'CancelHotelFunctionArn', { + value: cancelHotel.functionArn, + description: 'Cancel hotel function ARN', + }); + + new cdk.CfnOutput(this, 'ReserveCarFunctionArn', { + value: reserveCar.functionArn, + description: 'Reserve car function ARN', + }); + + new cdk.CfnOutput(this, 'CancelCarFunctionArn', { + value: cancelCar.functionArn, + description: 'Cancel car function ARN', + }); + + new cdk.CfnOutput(this, 'HotelTableName', { + value: hotelTable.tableName, + description: 'Hotel reservations table name', + }); + + new cdk.CfnOutput(this, 'FlightTableName', { + value: flightTable.tableName, + description: 'Flight bookings table name', + }); + + new cdk.CfnOutput(this, 'CarTableName', { + value: carTable.tableName, + description: 'Car rentals table name', + }); + } +} diff --git a/saga-pattern-lambda-durable-functions/saga-pattern-cdk/package.json b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/package.json new file mode 100644 index 000000000..912e9a17c --- /dev/null +++ b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/package.json @@ -0,0 +1,26 @@ +{ + "name": "saga-pattern-cdk", + "version": "0.1.0", + "bin": { + "saga-pattern-cdk": "bin/saga-pattern-cdk.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk" + }, + "devDependencies": { + "@types/jest": "^30", + "@types/node": "^24.10.1", + "jest": "^30", + "ts-jest": "^29", + "aws-cdk": "2.1100.3", + "ts-node": "^10.9.2", + "typescript": "~5.9.3" + }, + "dependencies": { + "aws-cdk-lib": "^2.232.2", + "constructs": "^10.0.0" + } +} diff --git a/saga-pattern-lambda-durable-functions/saga-pattern-cdk/test-fail-car-booking.json b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/test-fail-car-booking.json new file mode 100644 index 000000000..a02934b94 --- /dev/null +++ b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/test-fail-car-booking.json @@ -0,0 +1,9 @@ +{ + "passengerName": "Jane Smith", + "departure": "SFO", + "destination": "NYC", + "guestName": "Jane Smith", + "hotelName": "Marriott Times Square", + "driverName": "Jane Smith", + "failBookCar": true +} diff --git a/saga-pattern-lambda-durable-functions/saga-pattern-cdk/test-fail-flight-booking.json b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/test-fail-flight-booking.json new file mode 100644 index 000000000..940e055f8 --- /dev/null +++ b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/test-fail-flight-booking.json @@ -0,0 +1,6 @@ +{ + "passengerName": "Sarah Williams", + "departure": "ORD", + "destination": "SEA", + "failBookFlight": true +} diff --git a/saga-pattern-lambda-durable-functions/saga-pattern-cdk/test-fail-hotel-booking.json b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/test-fail-hotel-booking.json new file mode 100644 index 000000000..f2435b933 --- /dev/null +++ b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/test-fail-hotel-booking.json @@ -0,0 +1,9 @@ +{ + "passengerName": "David Martinez", + "departure": "DFW", + "destination": "BOS", + "flightPrice": 420.00, + "guestName": "David Martinez", + "hotelName": "Boston Harbor Hotel", + "failBookHotel": true +} diff --git a/saga-pattern-lambda-durable-functions/saga-pattern-cdk/test-success.json b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/test-success.json new file mode 100644 index 000000000..a864468de --- /dev/null +++ b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/test-success.json @@ -0,0 +1,20 @@ +{ + "passengerName": "Michael Johnson", + "flightNumber": "AA456", + "departure": "LAX", + "destination": "MIA", + "flightPrice": 380.00, + "guestName": "Michael Johnson", + "hotelName": "Hilton Downtown Miami", + "roomType": "Ocean View Suite", + "checkIn": "2026-04-10", + "checkOut": "2026-04-15", + "hotelPrice": 320.00, + "driverName": "Michael Johnson", + "carType": "Convertible", + "pickupLocation": "Miami Airport", + "dropoffLocation": "Miami Airport", + "pickupDate": "2026-04-10", + "dropoffDate": "2026-04-15", + "carPrice": 150.00 +} diff --git a/saga-pattern-lambda-durable-functions/saga-pattern-cdk/tsconfig.json b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/tsconfig.json new file mode 100644 index 000000000..bfc61bf83 --- /dev/null +++ b/saga-pattern-lambda-durable-functions/saga-pattern-cdk/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": [ + "es2022" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "skipLibCheck": true, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +}