From 57131c97ff32fa7ccddde149f469e5594bb8550b Mon Sep 17 00:00:00 2001 From: smyvens Date: Wed, 28 Jan 2026 14:29:42 -0500 Subject: [PATCH 1/5] Create cdk app --- .../.gitignore | 8 ++ .../.npmignore | 6 ++ .../README.md | 14 +++ .../lambda-durable-function-chaining-cdk.ts | 11 ++ lambda-durable-function-chaining-cdk/cdk.json | 102 ++++++++++++++++++ ...bda-durable-function-chaining-cdk-stack.ts | 8 ++ .../package.json | 24 +++++ .../tsconfig.json | 32 ++++++ 8 files changed, 205 insertions(+) create mode 100644 lambda-durable-function-chaining-cdk/.gitignore create mode 100644 lambda-durable-function-chaining-cdk/.npmignore create mode 100644 lambda-durable-function-chaining-cdk/README.md create mode 100644 lambda-durable-function-chaining-cdk/bin/lambda-durable-function-chaining-cdk.ts create mode 100644 lambda-durable-function-chaining-cdk/cdk.json create mode 100644 lambda-durable-function-chaining-cdk/lib/lambda-durable-function-chaining-cdk-stack.ts create mode 100644 lambda-durable-function-chaining-cdk/package.json create mode 100644 lambda-durable-function-chaining-cdk/tsconfig.json diff --git a/lambda-durable-function-chaining-cdk/.gitignore b/lambda-durable-function-chaining-cdk/.gitignore new file mode 100644 index 0000000000..f60797b6a9 --- /dev/null +++ b/lambda-durable-function-chaining-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/lambda-durable-function-chaining-cdk/.npmignore b/lambda-durable-function-chaining-cdk/.npmignore new file mode 100644 index 0000000000..c1d6d45dcf --- /dev/null +++ b/lambda-durable-function-chaining-cdk/.npmignore @@ -0,0 +1,6 @@ +*.ts +!*.d.ts + +# CDK asset staging directory +.cdk.staging +cdk.out diff --git a/lambda-durable-function-chaining-cdk/README.md b/lambda-durable-function-chaining-cdk/README.md new file mode 100644 index 0000000000..9315fe5b9f --- /dev/null +++ b/lambda-durable-function-chaining-cdk/README.md @@ -0,0 +1,14 @@ +# Welcome to your CDK TypeScript project + +This is a blank project for CDK development with TypeScript. + +The `cdk.json` file tells the CDK Toolkit how to execute your app. + +## Useful commands + +* `npm run build` compile typescript to js +* `npm run watch` watch for changes and compile +* `npm run test` perform the jest unit tests +* `npx cdk deploy` deploy this stack to your default AWS account/region +* `npx cdk diff` compare deployed stack with current state +* `npx cdk synth` emits the synthesized CloudFormation template diff --git a/lambda-durable-function-chaining-cdk/bin/lambda-durable-function-chaining-cdk.ts b/lambda-durable-function-chaining-cdk/bin/lambda-durable-function-chaining-cdk.ts new file mode 100644 index 0000000000..20e1037390 --- /dev/null +++ b/lambda-durable-function-chaining-cdk/bin/lambda-durable-function-chaining-cdk.ts @@ -0,0 +1,11 @@ +#!/usr/bin/env node +import * as cdk from "aws-cdk-lib/core"; +import { LambdaDurableFunctionChainingCdkStack } from "../lib/lambda-durable-function-chaining-cdk-stack"; + +const app = new cdk.App(); +new LambdaDurableFunctionChainingCdkStack(app, "LambdaDurableFunctionChainingCdkStack", { + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, + }, +}); diff --git a/lambda-durable-function-chaining-cdk/cdk.json b/lambda-durable-function-chaining-cdk/cdk.json new file mode 100644 index 0000000000..0307df5a27 --- /dev/null +++ b/lambda-durable-function-chaining-cdk/cdk.json @@ -0,0 +1,102 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/lambda-durable-function-chaining-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: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, + "@aws-cdk/aws-route53-patterns:useDistribution": true + } +} diff --git a/lambda-durable-function-chaining-cdk/lib/lambda-durable-function-chaining-cdk-stack.ts b/lambda-durable-function-chaining-cdk/lib/lambda-durable-function-chaining-cdk-stack.ts new file mode 100644 index 0000000000..aecfad7e89 --- /dev/null +++ b/lambda-durable-function-chaining-cdk/lib/lambda-durable-function-chaining-cdk-stack.ts @@ -0,0 +1,8 @@ +import * as cdk from 'aws-cdk-lib/core'; +import { Construct } from 'constructs'; + +export class LambdaDurableFunctionChainingCdkStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + } +} diff --git a/lambda-durable-function-chaining-cdk/package.json b/lambda-durable-function-chaining-cdk/package.json new file mode 100644 index 0000000000..21f08bbd27 --- /dev/null +++ b/lambda-durable-function-chaining-cdk/package.json @@ -0,0 +1,24 @@ +{ + "name": "lambda-durable-function-chaining-cdk", + "version": "0.1.0", + "bin": { + "lambda-durable-function-chaining-cdk": "bin/lambda-durable-function-chaining-cdk.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "cdk": "cdk" + }, + "devDependencies": { + "@types/jest": "^30", + "@types/node": "^24.10.1", + "aws-cdk": "2.1104.0", + "ts-node": "^10.9.2", + "typescript": "~5.9.3" + }, + "dependencies": { + "@aws-cdk/aws-lambda-python-alpha": "^2.236.0-alpha.0", + "aws-cdk-lib": "^2.236.0", + "constructs": "^10.0.0" + } +} diff --git a/lambda-durable-function-chaining-cdk/tsconfig.json b/lambda-durable-function-chaining-cdk/tsconfig.json new file mode 100644 index 0000000000..bfc61bf833 --- /dev/null +++ b/lambda-durable-function-chaining-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" + ] +} From b8526091a54d66147d1baf397dc36269da51efcc Mon Sep 17 00:00:00 2001 From: smyvens Date: Thu, 29 Jan 2026 10:36:42 -0500 Subject: [PATCH 2/5] created basic example of using durable functions --- .../lib/functions/AllocateInventory/index.ts | 1 + .../lib/functions/FulfillOrder/index.ts | 1 + .../lib/functions/authorizePayment/index.ts | 1 + .../lib/functions/validateOrder/index.ts | 65 +++++++++++++++++++ ...bda-durable-function-chaining-cdk-stack.ts | 51 ++++++++++++++- .../package.json | 3 +- 6 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 lambda-durable-function-chaining-cdk/lib/functions/AllocateInventory/index.ts create mode 100644 lambda-durable-function-chaining-cdk/lib/functions/FulfillOrder/index.ts create mode 100644 lambda-durable-function-chaining-cdk/lib/functions/authorizePayment/index.ts create mode 100644 lambda-durable-function-chaining-cdk/lib/functions/validateOrder/index.ts diff --git a/lambda-durable-function-chaining-cdk/lib/functions/AllocateInventory/index.ts b/lambda-durable-function-chaining-cdk/lib/functions/AllocateInventory/index.ts new file mode 100644 index 0000000000..ce0c87f62d --- /dev/null +++ b/lambda-durable-function-chaining-cdk/lib/functions/AllocateInventory/index.ts @@ -0,0 +1 @@ +export async function handler() {} diff --git a/lambda-durable-function-chaining-cdk/lib/functions/FulfillOrder/index.ts b/lambda-durable-function-chaining-cdk/lib/functions/FulfillOrder/index.ts new file mode 100644 index 0000000000..ce0c87f62d --- /dev/null +++ b/lambda-durable-function-chaining-cdk/lib/functions/FulfillOrder/index.ts @@ -0,0 +1 @@ +export async function handler() {} diff --git a/lambda-durable-function-chaining-cdk/lib/functions/authorizePayment/index.ts b/lambda-durable-function-chaining-cdk/lib/functions/authorizePayment/index.ts new file mode 100644 index 0000000000..ce0c87f62d --- /dev/null +++ b/lambda-durable-function-chaining-cdk/lib/functions/authorizePayment/index.ts @@ -0,0 +1 @@ +export async function handler() {} diff --git a/lambda-durable-function-chaining-cdk/lib/functions/validateOrder/index.ts b/lambda-durable-function-chaining-cdk/lib/functions/validateOrder/index.ts new file mode 100644 index 0000000000..c3a9fd22af --- /dev/null +++ b/lambda-durable-function-chaining-cdk/lib/functions/validateOrder/index.ts @@ -0,0 +1,65 @@ +import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js"; +import { LambdaClient, InvokeCommand } from "@aws-sdk/client-lambda"; + +interface Order { + orderId: string; + customerId: string; + items: { + productId: string; + quantity: number; + unitPrice: number; + }[]; + shippingAddress: string; + billingAddress: string; + paymentMethod: { + type: "credit" | "debit"; + cardNumber: number; + cardBrand: "Visa" | "Mastercard" | "Amex"; + }; + orderTotal: number; + orderTimestamp: string; +} + +const lambdaClient = new LambdaClient(); + +export const handler = withDurableExecution(async (event: Order, context: DurableContext) => { + const { orderId, items, shippingAddress, billingAddress, paymentMethod, orderTotal } = event; + + const validation = await context.step("validate-order", async () => { + return { isValid: true }; + }); + + if (!validation.isValid) { + return { orderId, status: "rejected", reason: "invalid_items" }; + } + + const authorization = await context.step("authorize-payment", async () => { + return await lambdaClient.send( + new InvokeCommand({ + FunctionName: process.env.AUTHORIZE_PAYMENT_FUNCTION, + }), + ); + }); + + const allocation = await context.step("allocate-inventory", async () => { + return await lambdaClient.send( + new InvokeCommand({ + FunctionName: process.env.ALLOCATE_INVENTORY_FUNCTION, + }), + ); + }); + + const fulfillment = await context.step("fulfill-order", async () => { + return await lambdaClient.send( + new InvokeCommand({ + FunctionName: process.env.FULFILL_ORDER_FUNCTION, + }), + ); + }); + + return { + orderId, + status: "completed", + timestamp: new Date().toISOString(), + }; +}); diff --git a/lambda-durable-function-chaining-cdk/lib/lambda-durable-function-chaining-cdk-stack.ts b/lambda-durable-function-chaining-cdk/lib/lambda-durable-function-chaining-cdk-stack.ts index aecfad7e89..d90c05798a 100644 --- a/lambda-durable-function-chaining-cdk/lib/lambda-durable-function-chaining-cdk-stack.ts +++ b/lambda-durable-function-chaining-cdk/lib/lambda-durable-function-chaining-cdk-stack.ts @@ -1,8 +1,55 @@ -import * as cdk from 'aws-cdk-lib/core'; -import { Construct } from 'constructs'; +import * as cdk from "aws-cdk-lib/core"; +import { Construct } from "constructs"; +import * as nodejs from "aws-cdk-lib/aws-lambda-nodejs"; +import * as lambda from "aws-cdk-lib/aws-lambda"; +import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; +import path from "path"; export class LambdaDurableFunctionChainingCdkStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); + + const productCatalogTable = new dynamodb.Table(this, "ProductCatalogTable", { + partitionKey: { + name: "productId", + type: dynamodb.AttributeType.STRING, + }, + }); + + const validateOrderFunction = new nodejs.NodejsFunction(this, "ValidateOrderFunction", { + runtime: lambda.Runtime.NODEJS_24_X, + entry: path.join(__dirname, "functions", "validateOrder", "index.ts"), + durableConfig: { + executionTimeout: cdk.Duration.hours(1), + retentionPeriod: cdk.Duration.days(7), + }, + }); + + const authorizePaymentFunction = new nodejs.NodejsFunction(this, "AuthorizePaymentFunction", { + runtime: lambda.Runtime.NODEJS_24_X, + entry: path.join(__dirname, "functions", "authorizePayment", "index.ts"), + }); + + validateOrderFunction.addEnvironment("ALLOCATE_INVENTORY_FUNCTION", authorizePaymentFunction.functionName); + authorizePaymentFunction.grantInvoke(validateOrderFunction); + + const allocateInventoryFunction = new nodejs.NodejsFunction(this, "AllocateInventoryFunction", { + runtime: lambda.Runtime.NODEJS_24_X, + entry: path.join(__dirname, "functions", "allocateInventory", "index.ts"), + }); + + validateOrderFunction.addEnvironment("AUTHORIZE_PAYMENT_FUNCTION", allocateInventoryFunction.functionName); + allocateInventoryFunction.grantInvoke(validateOrderFunction); + + const fulfillOrderFunction = new nodejs.NodejsFunction(this, "FulfillOrderFunction", { + runtime: lambda.Runtime.NODEJS_24_X, + entry: path.join(__dirname, "functions", "fulfillOrder", "index.ts"), + }); + + validateOrderFunction.addEnvironment("FULFILL_ORDER_FUNCTION", fulfillOrderFunction.functionName); + fulfillOrderFunction.grantInvoke(validateOrderFunction); + + productCatalogTable.grantReadWriteData(validateOrderFunction); + productCatalogTable.grantWriteData(allocateInventoryFunction); } } diff --git a/lambda-durable-function-chaining-cdk/package.json b/lambda-durable-function-chaining-cdk/package.json index 21f08bbd27..c8ef541a55 100644 --- a/lambda-durable-function-chaining-cdk/package.json +++ b/lambda-durable-function-chaining-cdk/package.json @@ -17,7 +17,8 @@ "typescript": "~5.9.3" }, "dependencies": { - "@aws-cdk/aws-lambda-python-alpha": "^2.236.0-alpha.0", + "@aws-sdk/client-lambda": "^3.978.0", + "@aws/durable-execution-sdk-js": "^1.0.2", "aws-cdk-lib": "^2.236.0", "constructs": "^10.0.0" } From 42921f192b9fa6d3ec47b1a99175bc91595e946c Mon Sep 17 00:00:00 2001 From: smyvens Date: Fri, 30 Jan 2026 13:51:17 -0500 Subject: [PATCH 3/5] added mock implementation logic for each step --- .../lib/functions/AllocateInventory/index.ts | 75 +++++++++++- .../lib/functions/FulfillOrder/index.ts | 58 ++++++++- .../lib/functions/authorizePayment/index.ts | 46 ++++++- .../lib/functions/validateOrder/index.ts | 114 +++++++++++++++++- ...bda-durable-function-chaining-cdk-stack.ts | 13 +- .../package.json | 1 + 6 files changed, 294 insertions(+), 13 deletions(-) diff --git a/lambda-durable-function-chaining-cdk/lib/functions/AllocateInventory/index.ts b/lambda-durable-function-chaining-cdk/lib/functions/AllocateInventory/index.ts index ce0c87f62d..f99576f59e 100644 --- a/lambda-durable-function-chaining-cdk/lib/functions/AllocateInventory/index.ts +++ b/lambda-durable-function-chaining-cdk/lib/functions/AllocateInventory/index.ts @@ -1 +1,74 @@ -export async function handler() {} +import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb"; + +interface InventoryEvent { + orderId: string; + items: { + productId: string; + quantity: number; + unitPrice: number; + }[]; +} + +const dynamoClient = new DynamoDBClient(); + +export async function handler(event: InventoryEvent) { + console.log("Allocating inventory for order:", event.orderId); + + const { orderId, items } = event; + const allocations = []; + + for (const item of items) { + const { productId, quantity } = item; + + // Get product from catalog + const result = await dynamoClient.send( + new GetItemCommand({ + TableName: process.env.PRODUCT_CATALOG_TABLE, + Key: { productId: { S: productId } }, + }), + ); + + if (!result.Item) { + return { + orderId, + status: "failed", + reason: "product_not_found", + productId, + timestamp: new Date().toISOString(), + }; + } + + const stockLevel = parseInt(result.Item.stockLevel.N!); + const warehouseLocation = result.Item.warehouseLocation.S!; + + if (stockLevel < quantity) { + return { + orderId, + status: "failed", + reason: "insufficient_inventory", + productId, + requested: quantity, + available: stockLevel, + timestamp: new Date().toISOString(), + }; + } + + const allocationId = `ALLOC-${productId}-${crypto.randomUUID()}`; + + allocations.push({ + allocationId, + productId, + quantity, + warehouseLocation, + reservedUntil: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + }); + } + + return { + orderId, + status: "allocated", + allocations, + totalItems: items.reduce((sum, item) => sum + item.quantity, 0), + timestamp: new Date().toISOString(), + }; +} diff --git a/lambda-durable-function-chaining-cdk/lib/functions/FulfillOrder/index.ts b/lambda-durable-function-chaining-cdk/lib/functions/FulfillOrder/index.ts index ce0c87f62d..d557109fbd 100644 --- a/lambda-durable-function-chaining-cdk/lib/functions/FulfillOrder/index.ts +++ b/lambda-durable-function-chaining-cdk/lib/functions/FulfillOrder/index.ts @@ -1 +1,57 @@ -export async function handler() {} +interface FulfillmentEvent { + orderId: string; + customerId: string; + items: { + productId: string; + quantity: number; + unitPrice: number; + }[]; + shippingAddress: string; + allocations: { + allocationId: string; + productId: string; + quantity: number; + warehouseLocation: string; + }[]; +} + +export async function handler(event: FulfillmentEvent) { + console.log("Fulfilling order:", event.orderId); + + const { orderId, customerId, items, shippingAddress, allocations } = event; + + // Mock shipping carrier selection + const carriers = ["Amazon Prime", "My Carrier", "Mock Carrier"]; + const selectedCarrier = carriers[Math.floor(crypto.getRandomValues(new Uint32Array(1))[0] / 0x100000000 * carriers.length)]; + + const trackingNumber = `${selectedCarrier}-${Date.now()}-${crypto.randomUUID()}`; + + const estimatedDeliveryDays = Math.floor(crypto.getRandomValues(new Uint32Array(1))[0] / 0x100000000 * 5) + 3; + const estimatedDeliveryDate = new Date(Date.now() + estimatedDeliveryDays * 24 * 60 * 60 * 1000); + + const shipments = allocations.map((allocation) => ({ + shipmentId: `SHIP-${allocation.warehouseLocation}-${crypto.randomUUID()}`, + warehouseLocation: allocation.warehouseLocation, + productId: allocation.productId, + quantity: allocation.quantity, + status: "preparing", + })); + + // Mock order fulfillment details + return { + orderId, + customerId, + status: "fulfilled", + trackingNumber, + carrier: selectedCarrier, + shippingAddress, + shipments, + estimatedDeliveryDate: estimatedDeliveryDate.toISOString(), + totalItems: items.reduce((sum, item) => sum + item.quantity, 0), + fulfillmentTimestamp: new Date().toISOString(), + notifications: { + emailSent: true, + smsSent: true, + }, + }; +} diff --git a/lambda-durable-function-chaining-cdk/lib/functions/authorizePayment/index.ts b/lambda-durable-function-chaining-cdk/lib/functions/authorizePayment/index.ts index ce0c87f62d..d55e0a0662 100644 --- a/lambda-durable-function-chaining-cdk/lib/functions/authorizePayment/index.ts +++ b/lambda-durable-function-chaining-cdk/lib/functions/authorizePayment/index.ts @@ -1 +1,45 @@ -export async function handler() {} +interface PaymentEvent { + orderId: string; + customerId: string; + paymentMethod: { + type: "credit" | "debit"; + cardNumber: string; + cardBrand: "Visa" | "Mastercard" | "Amex"; + }; + amount: number; +} + +export async function handler(event: PaymentEvent) { + console.log("Authorizing payment for order:", event.orderId); + + // Mock payment authorization logic + const { paymentMethod, amount, orderId, customerId } = event; + + // Simulate payment gateway call + const authorizationId = `AUTH-${Date.now()}-${crypto.randomUUID()}`; + + // Mock validation - reject if amount is too high (for demo purposes) + if (amount > 10000) { + return { + orderId, + status: "declined", + reason: "amount_exceeds_limit", + timestamp: new Date().toISOString(), + }; + } + + // Mock card validation + const lastFourDigits = String(paymentMethod.cardNumber).slice(-4); + + return { + orderId, + customerId, + status: "authorized", + authorizationId, + amount, + cardBrand: paymentMethod.cardBrand, + lastFourDigits, + timestamp: new Date().toISOString(), + expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days + }; +} diff --git a/lambda-durable-function-chaining-cdk/lib/functions/validateOrder/index.ts b/lambda-durable-function-chaining-cdk/lib/functions/validateOrder/index.ts index c3a9fd22af..42e1398132 100644 --- a/lambda-durable-function-chaining-cdk/lib/functions/validateOrder/index.ts +++ b/lambda-durable-function-chaining-cdk/lib/functions/validateOrder/index.ts @@ -26,40 +26,142 @@ export const handler = withDurableExecution(async (event: Order, context: Durabl const { orderId, items, shippingAddress, billingAddress, paymentMethod, orderTotal } = event; const validation = await context.step("validate-order", async () => { - return { isValid: true }; + // Mock order validation logic + const errors = []; + + // Validate items + if (!items || items.length === 0) { + errors.push("No items in order"); + } + + // Validate addresses + if (!shippingAddress || shippingAddress.trim() === "") { + errors.push("Invalid shipping address"); + } + + if (!billingAddress || billingAddress.trim() === "") { + errors.push("Invalid billing address"); + } + + // Validate payment method + if (!paymentMethod || !paymentMethod.cardNumber) { + errors.push("Invalid payment method"); + } + + // Validate order total + const calculatedTotal = items.reduce((sum, item) => sum + item.quantity * item.unitPrice, 0); + if (Math.abs(calculatedTotal - orderTotal) > 0.01) { + errors.push("Order total mismatch"); + } + + return { + isValid: errors.length === 0, + errors, + validatedAt: new Date().toISOString(), + }; }); if (!validation.isValid) { - return { orderId, status: "rejected", reason: "invalid_items" }; + return { + orderId, + status: "rejected", + reason: "validation_failed", + errors: validation.errors, + timestamp: new Date().toISOString(), + }; } const authorization = await context.step("authorize-payment", async () => { - return await lambdaClient.send( + const response = await lambdaClient.send( new InvokeCommand({ FunctionName: process.env.AUTHORIZE_PAYMENT_FUNCTION, + Payload: JSON.stringify({ + orderId, + customerId: event.customerId, + paymentMethod, + amount: orderTotal, + }), }), ); + + const payload = JSON.parse(new TextDecoder().decode(response.Payload)); + return payload; }); + if (authorization.status === "declined") { + return { + orderId, + status: "payment_declined", + reason: authorization.reason, + timestamp: new Date().toISOString(), + }; + } + const allocation = await context.step("allocate-inventory", async () => { - return await lambdaClient.send( + const response = await lambdaClient.send( new InvokeCommand({ FunctionName: process.env.ALLOCATE_INVENTORY_FUNCTION, + Payload: JSON.stringify({ + orderId, + items, + }), }), ); + + const payload = JSON.parse(new TextDecoder().decode(response.Payload)); + return payload; }); + if (allocation.status === "failed") { + return { + orderId, + status: "inventory_unavailable", + reason: allocation.reason, + productId: allocation.productId, + timestamp: new Date().toISOString(), + }; + } + const fulfillment = await context.step("fulfill-order", async () => { - return await lambdaClient.send( + const response = await lambdaClient.send( new InvokeCommand({ FunctionName: process.env.FULFILL_ORDER_FUNCTION, + Payload: JSON.stringify({ + orderId, + customerId: event.customerId, + items, + shippingAddress, + allocations: allocation.allocations, + }), }), ); + + const payload = JSON.parse(new TextDecoder().decode(response.Payload)); + return payload; }); return { orderId, + customerId: event.customerId, status: "completed", - timestamp: new Date().toISOString(), + orderTotal, + validation, + payment: { + authorizationId: authorization.authorizationId, + amount: authorization.amount, + cardBrand: authorization.cardBrand, + lastFourDigits: authorization.lastFourDigits, + }, + inventory: { + totalItems: allocation.totalItems, + allocations: allocation.allocations, + }, + fulfillment: { + trackingNumber: fulfillment.trackingNumber, + carrier: fulfillment.carrier, + estimatedDeliveryDate: fulfillment.estimatedDeliveryDate, + shipments: fulfillment.shipments, + }, + completedAt: new Date().toISOString(), }; }); diff --git a/lambda-durable-function-chaining-cdk/lib/lambda-durable-function-chaining-cdk-stack.ts b/lambda-durable-function-chaining-cdk/lib/lambda-durable-function-chaining-cdk-stack.ts index d90c05798a..b9f2928acc 100644 --- a/lambda-durable-function-chaining-cdk/lib/lambda-durable-function-chaining-cdk-stack.ts +++ b/lambda-durable-function-chaining-cdk/lib/lambda-durable-function-chaining-cdk-stack.ts @@ -28,28 +28,33 @@ export class LambdaDurableFunctionChainingCdkStack extends cdk.Stack { const authorizePaymentFunction = new nodejs.NodejsFunction(this, "AuthorizePaymentFunction", { runtime: lambda.Runtime.NODEJS_24_X, entry: path.join(__dirname, "functions", "authorizePayment", "index.ts"), + timeout: cdk.Duration.seconds(30), }); - validateOrderFunction.addEnvironment("ALLOCATE_INVENTORY_FUNCTION", authorizePaymentFunction.functionName); + validateOrderFunction.addEnvironment("AUTHORIZE_PAYMENT_FUNCTION", authorizePaymentFunction.functionName); authorizePaymentFunction.grantInvoke(validateOrderFunction); const allocateInventoryFunction = new nodejs.NodejsFunction(this, "AllocateInventoryFunction", { runtime: lambda.Runtime.NODEJS_24_X, entry: path.join(__dirname, "functions", "allocateInventory", "index.ts"), + environment: { + PRODUCT_CATALOG_TABLE: productCatalogTable.tableName, + }, + timeout: cdk.Duration.seconds(30), }); - validateOrderFunction.addEnvironment("AUTHORIZE_PAYMENT_FUNCTION", allocateInventoryFunction.functionName); + validateOrderFunction.addEnvironment("ALLOCATE_INVENTORY_FUNCTION", allocateInventoryFunction.functionName); allocateInventoryFunction.grantInvoke(validateOrderFunction); const fulfillOrderFunction = new nodejs.NodejsFunction(this, "FulfillOrderFunction", { runtime: lambda.Runtime.NODEJS_24_X, entry: path.join(__dirname, "functions", "fulfillOrder", "index.ts"), + timeout: cdk.Duration.seconds(30), }); validateOrderFunction.addEnvironment("FULFILL_ORDER_FUNCTION", fulfillOrderFunction.functionName); fulfillOrderFunction.grantInvoke(validateOrderFunction); - productCatalogTable.grantReadWriteData(validateOrderFunction); - productCatalogTable.grantWriteData(allocateInventoryFunction); + productCatalogTable.grantReadData(allocateInventoryFunction); } } diff --git a/lambda-durable-function-chaining-cdk/package.json b/lambda-durable-function-chaining-cdk/package.json index c8ef541a55..ad72c7cbe1 100644 --- a/lambda-durable-function-chaining-cdk/package.json +++ b/lambda-durable-function-chaining-cdk/package.json @@ -17,6 +17,7 @@ "typescript": "~5.9.3" }, "dependencies": { + "@aws-sdk/client-dynamodb": "^3.978.0", "@aws-sdk/client-lambda": "^3.978.0", "@aws/durable-execution-sdk-js": "^1.0.2", "aws-cdk-lib": "^2.236.0", From cc4cd58f63ad65c8edc4983adc759c224c9e287a Mon Sep 17 00:00:00 2001 From: smyvens Date: Thu, 5 Feb 2026 13:35:53 -0500 Subject: [PATCH 4/5] updated readme --- .../README.md | 463 +++++++++++++++++- 1 file changed, 453 insertions(+), 10 deletions(-) diff --git a/lambda-durable-function-chaining-cdk/README.md b/lambda-durable-function-chaining-cdk/README.md index 9315fe5b9f..028c61500b 100644 --- a/lambda-durable-function-chaining-cdk/README.md +++ b/lambda-durable-function-chaining-cdk/README.md @@ -1,14 +1,457 @@ -# Welcome to your CDK TypeScript project +# Order Processing with AWS Lambda Durable Functions (CDK) -This is a blank project for CDK development with TypeScript. +This pattern demonstrates an e-commerce order processing workflow using AWS Lambda Durable Functions with function chaining. The workflow orchestrates order validation, payment authorization, inventory allocation, and order fulfillment with automatic checkpointing and state persistence. -The `cdk.json` file tells the CDK Toolkit how to execute your app. +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/lambda-durable-function-chaining-cdk -## Useful commands +## Architecture -* `npm run build` compile typescript to js -* `npm run watch` watch for changes and compile -* `npm run test` perform the jest unit tests -* `npx cdk deploy` deploy this stack to your default AWS account/region -* `npx cdk diff` compare deployed stack with current state -* `npx cdk synth` emits the synthesized CloudFormation template +![Architecture Diagram](architecture.png) + +The solution implements a function chaining pattern where a durable orchestrator function coordinates multiple worker functions to process orders through a multi-step workflow. + +### Components + +- **ValidateOrder Function (Durable)**: Orchestrates the entire workflow with automatic checkpointing +- **AuthorizePayment Function**: Validates and authorizes payment methods +- **AllocateInventory Function**: Checks product availability and reserves inventory +- **FulfillOrder Function**: Creates shipments and generates tracking information +- **ProductCatalog Table**: DynamoDB table storing product information and stock levels + +### Workflow Steps + +The order processing workflow consists of 4 checkpointed steps: + +1. **validate-order** - Validates order data, items, addresses, payment method, and order total +2. **authorize-payment** - Authorizes payment with the payment gateway +3. **allocate-inventory** - Checks inventory availability and reserves items +4. **fulfill-order** - Creates shipments and generates tracking numbers + +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 +- ✅ **Function Chaining** - Orchestrator invokes worker functions sequentially +- ✅ **State Persistence** - Workflow state maintained across executions +- ✅ **Error Handling** - Graceful handling of validation, payment, and inventory failures +- ✅ **DynamoDB Integration** - Product catalog with stock level tracking + +## Prerequisites + +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html) installed +* [Node.js 18+](https://nodejs.org/) installed +* [TypeScript](https://www.typescriptlang.org/) installed + +## Deployment + +1. Navigate to the pattern directory: + ```bash + cd lambda-durable-function-chaining-cdk + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +3. Bootstrap CDK (if not already done): + ```bash + cdk bootstrap + ``` + +4. Deploy the stack: + ```bash + cdk deploy + ``` + +5. Note the `ProductCatalogTableName` from the outputs. + +## Testing + +### Step 1: Populate Product Catalog + +Add test products to the DynamoDB table: + +```bash +TABLE_NAME=$(aws cloudformation describe-stacks \ + --stack-name LambdaDurableFunctionChainingCdkStack \ + --query 'Stacks[0].Outputs[?OutputKey==`ProductCatalogTableName`].OutputValue' \ + --output text) + +# Add sample products with pricing +aws dynamodb put-item \ + --table-name ${TABLE_NAME} \ + --item '{ + "productId": {"S": "LAPTOP-001"}, + "name": {"S": "Gaming Laptop"}, + "price": {"N": "1299.99"}, + "stockLevel": {"N": "50"}, + "warehouseLocation": {"S": "WAREHOUSE-A"} + }' + +aws dynamodb put-item \ + --table-name ${TABLE_NAME} \ + --item '{ + "productId": {"S": "MOUSE-001"}, + "name": {"S": "Wireless Mouse"}, + "price": {"N": "29.99"}, + "stockLevel": {"N": "200"}, + "warehouseLocation": {"S": "WAREHOUSE-A"} + }' + +aws dynamodb put-item \ + --table-name ${TABLE_NAME} \ + --item '{ + "productId": {"S": "KEYBOARD-001"}, + "name": {"S": "Mechanical Keyboard"}, + "price": {"N": "149.99"}, + "stockLevel": {"N": "20"}, + "warehouseLocation": {"S": "WAREHOUSE-B"} + }' +``` + +### Step 2: Get Function Name + +```bash +FUNCTION_NAME=$(aws cloudformation describe-stacks \ + --stack-name LambdaDurableFunctionChainingCdkStack \ + --query 'Stacks[0].Outputs[?OutputKey==`ValidateOrderFunctionName`].OutputValue' \ + --output text) + +echo "Function Name: $FUNCTION_NAME" +``` + +### Step 3: Monitor Lambda Logs (Optional) + +In a separate terminal, monitor the logs to see real-time execution: + +```bash +FUNCTION_NAME=$(aws cloudformation describe-stacks \ + --stack-name LambdaDurableFunctionChainingCdkStack \ + --query 'Stacks[0].Outputs[?OutputKey==`ValidateOrderFunctionName`].OutputValue' \ + --output text) + +aws logs tail /aws/lambda/${FUNCTION_NAME} --follow +``` + +Look for checkpoint and step execution messages showing the workflow progression through each step. + +### Step 4: Test Order Processing + +**Note**: Product pricing is retrieved from the database. Orders only need to specify productId and quantity. + +**Test 1: Successful order** +```bash +aws lambda invoke \ + --function-name ${FUNCTION_NAME}:\$LATEST \ + --invocation-type Event \ + --cli-binary-format raw-in-base64-out \ + --payload '{ + "orderId": "ORDER-001", + "customerId": "CUST-001", + "items": [ + { + "productId": "LAPTOP-001", + "quantity": 1 + }, + { + "productId": "MOUSE-001", + "quantity": 2 + } + ], + "shippingAddress": "123 Main St, Seattle, WA 98101", + "billingAddress": "123 Main St, Seattle, WA 98101", + "paymentMethod": { + "type": "credit", + "cardNumber": 4532123456789012, + "cardBrand": "Visa" + } + }' \ + response.json + +echo "Order submitted asynchronously. Check logs for execution status." +``` + +**Test 2: Payment declined (10 laptops = $12,999.90, exceeds $10,000 limit)** +```bash +aws lambda invoke \ + --function-name ${FUNCTION_NAME}:\$LATEST \ + --invocation-type Event \ + --cli-binary-format raw-in-base64-out \ + --payload '{ + "orderId": "ORDER-002", + "customerId": "CUST-002", + "items": [ + { + "productId": "LAPTOP-001", + "quantity": 10 + } + ], + "shippingAddress": "456 Oak Ave, Portland, OR 97201", + "billingAddress": "456 Oak Ave, Portland, OR 97201", + "paymentMethod": { + "type": "credit", + "cardNumber": 5412345678901234, + "cardBrand": "Mastercard" + } + }' \ + response.json + +echo "Order submitted asynchronously. Check logs for execution status." +``` + +**Test 3: Insufficient inventory (60 keyboards, only 20 in stock)** +```bash +aws lambda invoke \ + --function-name ${FUNCTION_NAME}:\$LATEST \ + --invocation-type Event \ + --cli-binary-format raw-in-base64-out \ + --payload '{ + "orderId": "ORDER-003", + "customerId": "CUST-003", + "items": [ + { + "productId": "KEYBOARD-001", + "quantity": 60 + } + ], + "shippingAddress": "789 Pine Rd, Austin, TX 78701", + "billingAddress": "789 Pine Rd, Austin, TX 78701", + "paymentMethod": { + "type": "debit", + "cardNumber": 4111111111111111, + "cardBrand": "Visa" + } + }' \ + response.json + +echo "Order submitted asynchronously. Check logs for execution status." +``` + +**Test 4: Validation failure (empty items)** +```bash +aws lambda invoke \ + --function-name ${FUNCTION_NAME}:\$LATEST \ + --invocation-type Event \ + --cli-binary-format raw-in-base64-out \ + --payload '{ + "orderId": "ORDER-004", + "customerId": "CUST-004", + "items": [], + "shippingAddress": "321 Elm St, Boston, MA 02101", + "billingAddress": "321 Elm St, Boston, MA 02101", + "paymentMethod": { + "type": "credit", + "cardNumber": 378282246310005, + "cardBrand": "Amex" + } + }' \ + response.json + +echo "Order submitted asynchronously. Check logs for execution status." +``` + +### Expected Test Results + +- ✅ **Successful orders**: Complete all 4 steps and return full order details +- ✅ **Payment declined**: Fail at payment authorization step +- ✅ **Insufficient inventory**: Fail at inventory allocation step +- ✅ **Validation failures**: Reject immediately with error details +- ✅ **Checkpointing**: Function resumes from last checkpoint on retry + +## How It Works + +### Durable Execution with Function Chaining + +The ValidateOrder function uses the Durable Execution SDK to orchestrate worker functions: + +```typescript +import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js"; + +export const handler = withDurableExecution(async (event: Order, context: DurableContext) => { + // Step 1: Validate order (inline) + const validation = await context.step("validate-order", async () => { + // Validation logic + }); + + // Step 2: Authorize payment (invoke worker function) + const authorization = await context.step("authorize-payment", async () => { + const response = await lambdaClient.send( + new InvokeCommand({ + FunctionName: process.env.AUTHORIZE_PAYMENT_FUNCTION, + Payload: JSON.stringify({ orderId, paymentMethod, amount }) + }) + ); + return JSON.parse(new TextDecoder().decode(response.Payload)); + }); + + // Step 3: Allocate inventory (invoke worker function) + const allocation = await context.step("allocate-inventory", async () => { + // Invoke AllocateInventory function + }); + + // Step 4: Fulfill order (invoke worker function) + const fulfillment = await context.step("fulfill-order", async () => { + // Invoke FulfillOrder function + }); + + return { orderId, status: "completed", ... }; +}); +``` + +### Checkpoint Behavior + +When the durable function executes: +1. Each context.step creates a checkpoint before execution +2. If 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 + +### Worker Functions + +**AuthorizePayment**: Simulates payment gateway authorization with amount limits + +**AllocateInventory**: Queries DynamoDB for product availability and reserves inventory + +**FulfillOrder**: Generates shipment details and tracking numbers + +## Configuration + +### Durable Execution Settings + +The durable function is configured in the CDK stack: + +```typescript +const validateOrderFunction = new nodejs.NodejsFunction(this, "ValidateOrderFunction", { + runtime: lambda.Runtime.NODEJS_24_X, + entry: path.join(__dirname, "functions", "validateOrder", "index.ts"), + durableConfig: { + executionTimeout: cdk.Duration.hours(1), + retentionPeriod: cdk.Duration.days(7), + }, +}); +``` + +- **executionTimeout**: 1 hour maximum workflow duration +- **retentionPeriod**: 7 days checkpoint retention + +### IAM Permissions + +The orchestrator function requires permissions to: +- Invoke worker Lambda functions +- Create and retrieve durable execution checkpoints + +Worker functions require: +- DynamoDB read access (AllocateInventory) + +## Customization + +### Add More Workflow Steps + +Add additional steps to the orchestrator: + +```typescript +const notification = await context.step("send-notification", async () => { + // Send order confirmation email + return await sesClient.send(new SendEmailCommand({...})); +}); +``` + +### Modify Validation Rules + +Update validation logic in the validate-order step: + +```typescript +const validation = await context.step("validate-order", async () => { + const errors = []; + + // Add custom validation + if (orderTotal < 10) { + errors.push("Minimum order amount is $10"); + } + + return { isValid: errors.length === 0, errors }; +}); +``` + +### Integrate Real Payment Gateway + +Replace mock payment logic with actual payment service: + +```typescript +const authorization = await context.step("authorize-payment", async () => { + const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); + return await stripe.paymentIntents.create({ + amount: orderTotal * 100, + currency: "usd", + customer: customerId + }); +}); +``` + +### Adjust Execution Timeout + +Modify the durable configuration in the CDK stack: + +```typescript +durableConfig: { + executionTimeout: cdk.Duration.hours(24), // 24 hours + retentionPeriod: cdk.Duration.days(14), // 14 days +} +``` + +## 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 +- Function invocations + +## Cleanup + +Delete the stack: + +```bash +cdk destroy +``` + +Or via AWS CLI: + +```bash +aws cloudformation delete-stack --stack-name LambdaDurableFunctionChainingCdkStack +``` + +## Cost Considerations + +- **Lambda**: Pay per invocation and execution time +- **DynamoDB**: On-demand pricing for product catalog reads +- **Durable Execution**: Checkpoint storage costs (minimal) +- **No charges during wait states**: Function suspended between steps + +## 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 CDK Documentation](https://docs.aws.amazon.com/cdk/) +- [Function Chaining Pattern](https://docs.aws.amazon.com/lambda/latest/dg/durable-functions-patterns.html) + +--- + +Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 From 7bb1f2fbce515cef4f8c3be081d52975d0b48519 Mon Sep 17 00:00:00 2001 From: smyvens Date: Thu, 5 Feb 2026 13:39:21 -0500 Subject: [PATCH 5/5] add more realistic logic --- .../lib/functions/AllocateInventory/index.ts | 45 ++++++++++++++++- .../lib/functions/FulfillOrder/index.ts | 21 +++++--- .../lib/functions/authorizePayment/index.ts | 48 ++++++++++++++++-- .../lib/functions/validateOrder/index.ts | 49 +++++++++++++------ ...bda-durable-function-chaining-cdk-stack.ts | 21 ++++++-- .../package.json | 1 + 6 files changed, 154 insertions(+), 31 deletions(-) diff --git a/lambda-durable-function-chaining-cdk/lib/functions/AllocateInventory/index.ts b/lambda-durable-function-chaining-cdk/lib/functions/AllocateInventory/index.ts index f99576f59e..3a378f6839 100644 --- a/lambda-durable-function-chaining-cdk/lib/functions/AllocateInventory/index.ts +++ b/lambda-durable-function-chaining-cdk/lib/functions/AllocateInventory/index.ts @@ -1,4 +1,4 @@ -import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb"; +import { DynamoDBClient, GetItemCommand, UpdateItemCommand } from "@aws-sdk/client-dynamodb"; interface InventoryEvent { orderId: string; @@ -6,7 +6,9 @@ interface InventoryEvent { productId: string; quantity: number; unitPrice: number; + itemTotal: number; }[]; + restore?: boolean; } const dynamoClient = new DynamoDBClient(); @@ -14,7 +16,34 @@ const dynamoClient = new DynamoDBClient(); export async function handler(event: InventoryEvent) { console.log("Allocating inventory for order:", event.orderId); - const { orderId, items } = event; + const { orderId, items, restore } = event; + + // If restore flag is set, add inventory back + if (restore) { + console.log("Restoring inventory for order:", orderId); + + for (const item of items) { + const { productId, quantity } = item; + + await dynamoClient.send( + new UpdateItemCommand({ + TableName: process.env.PRODUCT_CATALOG_TABLE, + Key: { productId: { S: productId } }, + UpdateExpression: "SET stockLevel = stockLevel + :quantity", + ExpressionAttributeValues: { + ":quantity": { N: String(quantity) }, + }, + }), + ); + } + + return { + orderId, + status: "restored", + timestamp: new Date().toISOString(), + }; + } + const allocations = []; for (const item of items) { @@ -55,6 +84,18 @@ export async function handler(event: InventoryEvent) { const allocationId = `ALLOC-${productId}-${crypto.randomUUID()}`; + // Update stock level in DynamoDB + await dynamoClient.send( + new UpdateItemCommand({ + TableName: process.env.PRODUCT_CATALOG_TABLE, + Key: { productId: { S: productId } }, + UpdateExpression: "SET stockLevel = :newStock", + ExpressionAttributeValues: { + ":newStock": { N: String(stockLevel - quantity) }, + }, + }), + ); + allocations.push({ allocationId, productId, diff --git a/lambda-durable-function-chaining-cdk/lib/functions/FulfillOrder/index.ts b/lambda-durable-function-chaining-cdk/lib/functions/FulfillOrder/index.ts index d557109fbd..55feef8de4 100644 --- a/lambda-durable-function-chaining-cdk/lib/functions/FulfillOrder/index.ts +++ b/lambda-durable-function-chaining-cdk/lib/functions/FulfillOrder/index.ts @@ -5,6 +5,7 @@ interface FulfillmentEvent { productId: string; quantity: number; unitPrice: number; + itemTotal: number; }[]; shippingAddress: string; allocations: { @@ -29,13 +30,19 @@ export async function handler(event: FulfillmentEvent) { const estimatedDeliveryDays = Math.floor(crypto.getRandomValues(new Uint32Array(1))[0] / 0x100000000 * 5) + 3; const estimatedDeliveryDate = new Date(Date.now() + estimatedDeliveryDays * 24 * 60 * 60 * 1000); - const shipments = allocations.map((allocation) => ({ - shipmentId: `SHIP-${allocation.warehouseLocation}-${crypto.randomUUID()}`, - warehouseLocation: allocation.warehouseLocation, - productId: allocation.productId, - quantity: allocation.quantity, - status: "preparing", - })); + // Create shipments with pricing information + const shipments = allocations.map((allocation) => { + const item = items.find(i => i.productId === allocation.productId); + return { + shipmentId: `SHIP-${allocation.warehouseLocation}-${crypto.randomUUID()}`, + warehouseLocation: allocation.warehouseLocation, + productId: allocation.productId, + quantity: allocation.quantity, + unitPrice: item?.unitPrice || 0, + itemTotal: item?.itemTotal || 0, + status: "preparing", + }; + }); // Mock order fulfillment details return { diff --git a/lambda-durable-function-chaining-cdk/lib/functions/authorizePayment/index.ts b/lambda-durable-function-chaining-cdk/lib/functions/authorizePayment/index.ts index d55e0a0662..3db71fef23 100644 --- a/lambda-durable-function-chaining-cdk/lib/functions/authorizePayment/index.ts +++ b/lambda-durable-function-chaining-cdk/lib/functions/authorizePayment/index.ts @@ -1,19 +1,59 @@ +import { DynamoDBClient, GetItemCommand } from "@aws-sdk/client-dynamodb"; + interface PaymentEvent { orderId: string; customerId: string; + items: { + productId: string; + quantity: number; + }[]; paymentMethod: { type: "credit" | "debit"; cardNumber: string; cardBrand: "Visa" | "Mastercard" | "Amex"; }; - amount: number; } +const dynamoClient = new DynamoDBClient(); + export async function handler(event: PaymentEvent) { console.log("Authorizing payment for order:", event.orderId); - // Mock payment authorization logic - const { paymentMethod, amount, orderId, customerId } = event; + const { paymentMethod, orderId, customerId, items } = event; + + // Fetch pricing from DynamoDB and calculate total + let amount = 0; + const itemsWithPricing = []; + + for (const item of items) { + const result = await dynamoClient.send( + new GetItemCommand({ + TableName: process.env.PRODUCT_CATALOG_TABLE, + Key: { productId: { S: item.productId } }, + }), + ); + + if (!result.Item) { + return { + orderId, + status: "declined", + reason: "product_not_found", + productId: item.productId, + timestamp: new Date().toISOString(), + }; + } + + const price = parseFloat(result.Item.price.N!); + const itemTotal = price * item.quantity; + amount += itemTotal; + + itemsWithPricing.push({ + productId: item.productId, + quantity: item.quantity, + unitPrice: price, + itemTotal, + }); + } // Simulate payment gateway call const authorizationId = `AUTH-${Date.now()}-${crypto.randomUUID()}`; @@ -24,6 +64,7 @@ export async function handler(event: PaymentEvent) { orderId, status: "declined", reason: "amount_exceeds_limit", + amount, timestamp: new Date().toISOString(), }; } @@ -37,6 +78,7 @@ export async function handler(event: PaymentEvent) { status: "authorized", authorizationId, amount, + items: itemsWithPricing, cardBrand: paymentMethod.cardBrand, lastFourDigits, timestamp: new Date().toISOString(), diff --git a/lambda-durable-function-chaining-cdk/lib/functions/validateOrder/index.ts b/lambda-durable-function-chaining-cdk/lib/functions/validateOrder/index.ts index 42e1398132..f0c5205c6a 100644 --- a/lambda-durable-function-chaining-cdk/lib/functions/validateOrder/index.ts +++ b/lambda-durable-function-chaining-cdk/lib/functions/validateOrder/index.ts @@ -1,5 +1,5 @@ +import { InvokeCommand, LambdaClient } from "@aws-sdk/client-lambda"; import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js"; -import { LambdaClient, InvokeCommand } from "@aws-sdk/client-lambda"; interface Order { orderId: string; @@ -7,7 +7,6 @@ interface Order { items: { productId: string; quantity: number; - unitPrice: number; }[]; shippingAddress: string; billingAddress: string; @@ -16,14 +15,12 @@ interface Order { cardNumber: number; cardBrand: "Visa" | "Mastercard" | "Amex"; }; - orderTotal: number; - orderTimestamp: string; } const lambdaClient = new LambdaClient(); export const handler = withDurableExecution(async (event: Order, context: DurableContext) => { - const { orderId, items, shippingAddress, billingAddress, paymentMethod, orderTotal } = event; + const { orderId, items, shippingAddress, billingAddress, paymentMethod } = event; const validation = await context.step("validate-order", async () => { // Mock order validation logic @@ -48,12 +45,6 @@ export const handler = withDurableExecution(async (event: Order, context: Durabl errors.push("Invalid payment method"); } - // Validate order total - const calculatedTotal = items.reduce((sum, item) => sum + item.quantity * item.unitPrice, 0); - if (Math.abs(calculatedTotal - orderTotal) > 0.01) { - errors.push("Order total mismatch"); - } - return { isValid: errors.length === 0, errors, @@ -78,8 +69,8 @@ export const handler = withDurableExecution(async (event: Order, context: Durabl Payload: JSON.stringify({ orderId, customerId: event.customerId, + items, paymentMethod, - amount: orderTotal, }), }), ); @@ -93,6 +84,7 @@ export const handler = withDurableExecution(async (event: Order, context: Durabl orderId, status: "payment_declined", reason: authorization.reason, + amount: authorization.amount, timestamp: new Date().toISOString(), }; } @@ -103,7 +95,7 @@ export const handler = withDurableExecution(async (event: Order, context: Durabl FunctionName: process.env.ALLOCATE_INVENTORY_FUNCTION, Payload: JSON.stringify({ orderId, - items, + items: authorization.items, }), }), ); @@ -118,6 +110,7 @@ export const handler = withDurableExecution(async (event: Order, context: Durabl status: "inventory_unavailable", reason: allocation.reason, productId: allocation.productId, + orderTotal: authorization.amount, timestamp: new Date().toISOString(), }; } @@ -129,7 +122,7 @@ export const handler = withDurableExecution(async (event: Order, context: Durabl Payload: JSON.stringify({ orderId, customerId: event.customerId, - items, + items: authorization.items, shippingAddress, allocations: allocation.allocations, }), @@ -137,6 +130,31 @@ export const handler = withDurableExecution(async (event: Order, context: Durabl ); const payload = JSON.parse(new TextDecoder().decode(response.Payload)); + + // If fulfillment fails, restore inventory + if (payload.status === "failed") { + await context.step("restore-inventory", async () => { + await lambdaClient.send( + new InvokeCommand({ + FunctionName: process.env.ALLOCATE_INVENTORY_FUNCTION, + Payload: JSON.stringify({ + orderId, + items: authorization.items, + restore: true, + }), + }), + ); + }); + + return { + orderId, + status: "fulfillment_failed", + reason: payload.reason, + orderTotal: authorization.amount, + timestamp: new Date().toISOString(), + }; + } + return payload; }); @@ -144,7 +162,8 @@ export const handler = withDurableExecution(async (event: Order, context: Durabl orderId, customerId: event.customerId, status: "completed", - orderTotal, + orderTotal: authorization.amount, + items: authorization.items, validation, payment: { authorizationId: authorization.authorizationId, diff --git a/lambda-durable-function-chaining-cdk/lib/lambda-durable-function-chaining-cdk-stack.ts b/lambda-durable-function-chaining-cdk/lib/lambda-durable-function-chaining-cdk-stack.ts index b9f2928acc..4902d6df5b 100644 --- a/lambda-durable-function-chaining-cdk/lib/lambda-durable-function-chaining-cdk-stack.ts +++ b/lambda-durable-function-chaining-cdk/lib/lambda-durable-function-chaining-cdk-stack.ts @@ -1,8 +1,8 @@ +import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; +import * as lambda from "aws-cdk-lib/aws-lambda"; +import * as nodejs from "aws-cdk-lib/aws-lambda-nodejs"; import * as cdk from "aws-cdk-lib/core"; import { Construct } from "constructs"; -import * as nodejs from "aws-cdk-lib/aws-lambda-nodejs"; -import * as lambda from "aws-cdk-lib/aws-lambda"; -import * as dynamodb from "aws-cdk-lib/aws-dynamodb"; import path from "path"; export class LambdaDurableFunctionChainingCdkStack extends cdk.Stack { @@ -28,11 +28,15 @@ export class LambdaDurableFunctionChainingCdkStack extends cdk.Stack { const authorizePaymentFunction = new nodejs.NodejsFunction(this, "AuthorizePaymentFunction", { runtime: lambda.Runtime.NODEJS_24_X, entry: path.join(__dirname, "functions", "authorizePayment", "index.ts"), + environment: { + PRODUCT_CATALOG_TABLE: productCatalogTable.tableName, + }, timeout: cdk.Duration.seconds(30), }); validateOrderFunction.addEnvironment("AUTHORIZE_PAYMENT_FUNCTION", authorizePaymentFunction.functionName); authorizePaymentFunction.grantInvoke(validateOrderFunction); + productCatalogTable.grantReadData(authorizePaymentFunction); const allocateInventoryFunction = new nodejs.NodejsFunction(this, "AllocateInventoryFunction", { runtime: lambda.Runtime.NODEJS_24_X, @@ -55,6 +59,15 @@ export class LambdaDurableFunctionChainingCdkStack extends cdk.Stack { validateOrderFunction.addEnvironment("FULFILL_ORDER_FUNCTION", fulfillOrderFunction.functionName); fulfillOrderFunction.grantInvoke(validateOrderFunction); - productCatalogTable.grantReadData(allocateInventoryFunction); + productCatalogTable.grantReadWriteData(allocateInventoryFunction); + + // Outputs + new cdk.CfnOutput(this, "ProductCatalogTableName", { + value: productCatalogTable.tableName, + }); + + new cdk.CfnOutput(this, "ValidateOrderFunctionName", { + value: validateOrderFunction.functionName, + }) } } diff --git a/lambda-durable-function-chaining-cdk/package.json b/lambda-durable-function-chaining-cdk/package.json index ad72c7cbe1..d9ba2d1d31 100644 --- a/lambda-durable-function-chaining-cdk/package.json +++ b/lambda-durable-function-chaining-cdk/package.json @@ -10,6 +10,7 @@ "cdk": "cdk" }, "devDependencies": { + "@types/aws-lambda": "^8.10.160", "@types/jest": "^30", "@types/node": "^24.10.1", "aws-cdk": "2.1104.0",