Runtime: Node.js 22 (TypeScript)
AWS Services: API Gateway, Lambda (with Durable Execution), SQS, DynamoDB (with Streams), EventBridge
The loyalty point service manages user loyalty accounts and points. It reacts to events from other services and exposes an API for querying and spending points.
Components:
- API -- REST endpoints for retrieving a user's loyalty account (
GET /loyalty) and spending points (POST /loyalty). Both endpoints require a JWT bearer token whosesubclaim identifies the user. - ACL (Anti-Corruption Layer) -- Consumes
users.userCreated.v1andorders.orderCompleted.v1/v2events from EventBridge via SQS queues, translates them into internal operations (create account with 100 points, add 50 points per completed order). - Stream Handler -- Listens to DynamoDB Streams on the loyalty table and publishes
loyalty.pointsAdded.v2events to EventBridge whenever a user's point total changes. - Tier Upgrade Workflow -- A durable multi-step workflow (using AWS Lambda Durable Execution) that evaluates whether a user has crossed a loyalty tier threshold and, if so, gathers context, saves the new tier, publishes a notification event, and waits for external acknowledgement before completing.
EventBridge ──> SQS ──> HandleUserCreated Lambda ──> DynamoDB
EventBridge ──> SQS ──> HandleOrderCompleted Lambda ──> DynamoDB
│
DynamoDB Stream
│
HandleLoyaltyPointsUpdated Lambda ──> EventBridge (loyalty.pointsAdded.v2)
│
┌─────────────────────────────┘
▼
SQS ──> TierUpgradeTrigger Lambda
│ (async invoke)
▼
TierUpgradeOrchestrator Lambda (Durable)
│ │ │ │
│ │ │ └── wait for callback ──> EventBridge (loyalty.tierUpgraded.v1)
│ │ │ │
│ │ │ SQS ──> NotificationAcknowledger Lambda
│ │ │ │ (SendDurableExecutionCallbackSuccess)
│ │ │ └──────────────────────────────────────┐
│ │ └── context.invoke ──> FetchOrderHistoryActivity Lambda │
│ └── context.step ──> Product Service (HTTP) │
└── context.step ──> DynamoDB (read/write tier) <──────────────────────────────────┘
API Gateway ──> GetLoyaltyPoints Lambda ──> DynamoDB
API Gateway ──> SpendLoyaltyPoints Lambda ──> DynamoDB
This is the most complex component of the service. It uses AWS Lambda Durable Execution via the @aws/durable-execution-sdk-js package to run a stateful, multi-step workflow entirely within Lambda — no Step Functions required.
When a user's loyalty points are updated, the workflow evaluates whether they have crossed a tier threshold and, if so, runs the following steps in order:
| Step | Description |
|---|---|
read-account |
Reads the user's current tier and version from DynamoDB |
evaluate-tier |
Computes the new tier based on current points (Bronze → Silver → Gold → Platinum) |
gather-context (parallel) |
Fetches product recommendations from the Product Service and Product Search Service simultaneously |
fetch-order-history |
Invokes the FetchOrderHistoryActivity Lambda (pinned to a specific version) to retrieve the user's order history from the Order Service |
upgrade-tier |
Writes the new tier to DynamoDB with optimistic locking (conditional write on TierVersion) |
await-notification-ack |
Publishes a loyalty.tierUpgraded.v1 event to EventBridge and suspends execution, waiting up to 5 minutes for an acknowledgement callback |
record-completion |
Marks the tier record as notified in DynamoDB |
If the user has not yet reached the next tier threshold, the workflow exits early after evaluate-tier with no writes or events.
| Tier | Points required |
|---|---|
| Bronze | 0 (default) |
| Silver | 500 |
| Gold | 1500 |
| Platinum | 3000 |
Tiers only upgrade — a decrease in points (hypothetically) will not trigger a downgrade.
Optimistic locking on TierVersion in DynamoDB prevents duplicate upgrades. If a ConditionalCheckFailedException is thrown at the upgrade-tier step (e.g., because multiple loyalty.pointsAdded.v2 events fired concurrently), the write is rejected and the workflow stops. Durable execution's built-in replay semantics ensure completed steps are never re-executed.
After publishing the loyalty.tierUpgraded.v1 event, the orchestrator suspends using context.waitForCallback. The event payload includes a callbackId. A separate NotificationAcknowledger Lambda subscribes to loyalty.tierUpgraded.v1 via SQS and calls the Lambda SendDurableExecutionCallbackSuccess API with that callbackId, which resumes the suspended orchestrator.
| Function | CDK construct ID | Purpose |
|---|---|---|
TierUpgradeTrigger |
LoyaltyTierWorkflow/TierUpgradeTrigger |
SQS consumer that receives loyalty.pointsAdded.v2 and asynchronously invokes the orchestrator |
TierUpgradeOrchestrator |
LoyaltyTierWorkflow/TierUpgradeOrchestrator |
Durable workflow function; orchestrates all steps |
FetchOrderHistoryActivity |
LoyaltyTierWorkflow/FetchOrderHistoryActivity |
Activity Lambda invoked by the orchestrator to call the Order Service |
NotificationAcknowledger |
LoyaltyTierWorkflow/NotificationAcknowledger |
SQS consumer that receives loyalty.tierUpgraded.v1 and sends the durable callback |
The following SSM Parameter Store values must exist before deploying (they are read at runtime, not synthesis):
| SSM Parameter | Consumer function | Description |
|---|---|---|
/<ENV>/shared/secret-access-key |
FetchOrderHistoryActivity |
JWT signing secret shared with the Order Service |
/<ENV>/OrderService/api-endpoint |
FetchOrderHistoryActivity |
Base URL of the Order Service API |
/<ENV>/ProductService/api-endpoint |
TierUpgradeOrchestrator |
Base URL of the Product Service API |
/<ENV>/ProductSearchService/api-endpoint |
TierUpgradeOrchestrator |
Base URL of the Product Search Service API |
The orchestrator function also requires these IAM actions (granted automatically by the CDK construct):
lambda:CheckpointDurableExecution— to save and replay workflow statelambda:GetDurableExecutionState— to read current workflow state on replaylambda:SendDurableExecutionCallbackSuccess/lambda:SendDurableExecutionCallbackFailure— granted to theNotificationAcknowledgerto resume the suspended orchestrator
SendDurableExecutionCallbackSuccessCommand requires @aws-sdk/client-lambda >= 3.1004.0. This is declared as a devDependency in package.json and is included in the bundled Lambda deployment packages via esbuild.
src/loyalty-tier-workflow/
├── trigger/ # TierUpgradeTrigger handler + esbuild config
├── orchestrator/ # TierUpgradeOrchestrator handler + esbuild config
├── activities/ # FetchOrderHistoryActivity handler + esbuild config
├── acknowledger/ # NotificationAcknowledger handler + esbuild config
└── core/
├── tier.ts # Tier enum, thresholds, and evaluateTierChange logic
├── tierRepository.ts # TierRepository interface
└── adapters/
├── dynamoDbTierRepository.ts # DynamoDB read/write with optimistic locking
├── eventBridgeTierPublisher.ts # Publishes loyalty.tierUpgraded.v1
├── orderServiceClient.ts # HTTP client for Order Service
├── productServiceClient.ts # HTTP client for Product Service
└── productSearchClient.ts # HTTP client for Product Search Service
- Node.js >= 22
- npm
- AWS CLI configured with appropriate credentials
- One of: AWS CDK, AWS SAM CLI, Terraform, Serverless Framework, or SST v2
| Variable | Description |
|---|---|
DD_API_KEY or DD_API_KEY_SECRET_ARN |
Datadog API key (plain text or Secrets Manager ARN, depending on tool) |
DD_SITE |
Datadog site (e.g. datadoghq.com, datadoghq.eu) |
AWS_REGION |
AWS region to deploy to |
ENV |
Environment name (e.g. dev, prod, or a personal stage name) |
npm install
npm run build # Compile TypeScript
npm run typecheck # Type-check without emitting
npm run watch # Watch mode for TypeScript compilationThe project includes integration tests that deploy against a live AWS environment. Tests exercise the full event-driven flow: creating a user, completing an order, and verifying loyalty point totals via the API.
Tests auto-discover the API endpoint and EventBridge bus name from SSM Parameter Store (/<ENV>/LoyaltyService/api-endpoint and /<ENV>/shared/event-bus-name). You can override these by setting API_ENDPOINT and EVENT_BUS_NAME environment variables.
npm run testThe service supports five deployment tools. Each deploys the same set of Lambda functions, DynamoDB table, SQS queues, EventBridge rules, and API Gateway.
The CDK stack is in lib/loyalty-api/ with a custom InstrumentedFunction L3 construct that ensures consistent Datadog instrumentation via the Datadog CDK Construct.
Entry point: bin/loyalty-point-service.ts
export DD_API_KEY=<YOUR_DATADOG_API_KEY>
export DD_SITE=<YOUR_DATADOG_SITE>
export ENV=dev
cdk deploy --all --require-approval neverOr using the Makefile:
export DD_API_KEY=<YOUR_DATADOG_API_KEY>
export DD_SITE=<YOUR_DATADOG_SITE>
export ENV=dev
export AWS_REGION=us-east-1
make cdk-deploycdk destroy --all --forceUses the Datadog CloudFormation Macro for auto-instrumentation. Ensure the macro is installed in your account before deploying.
Template: template.yaml
Before deploying with SAM, build the Lambda deployment packages:
./package.shexport DD_API_KEY=<YOUR_DATADOG_API_KEY>
export DD_SITE=<YOUR_DATADOG_SITE>
export ENV=dev
export AWS_REGION=us-east-1
make samOr directly:
sam build
sam deploy --stack-name LoyaltyService-dev \
--parameter-overrides ParameterKey=DDApiKey,ParameterValue="$DD_API_KEY" ParameterKey=DDSite,ParameterValue="$DD_SITE" \
--resolve-s3 --capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND --region $AWS_REGIONmake sam-destroyTerraform requires pre-built ZIP artifacts. The package.sh script transpiles TypeScript via esbuild and creates ZIP files in the out/ directory.
Configuration is in infra/, using a custom lambda_function module that wraps the Datadog Lambda Terraform module.
-
Build deployment packages:
./package.sh
-
Create
infra/dev.tfvars:dd_api_key = "<YOUR_DATADOG_API_KEY>" dd_site = "<YOUR_DATADOG_SITE>" env = "dev" region = "us-east-1"
-
Deploy:
export ENV=dev export AWS_REGION=us-east-1 make tf-apply-local
For remote state, set
TF_STATE_BUCKET_NAMEand usemake tf-applyinstead.
make tf-destroyUses the Datadog Serverless Plugin. Configuration is in serverless.yml.
Note: The
serverless.ymlin this repo is configured for a product API service, not the loyalty service. It is included as a reference for the Serverless Framework deployment pattern.
export DD_API_KEY_SECRET_ARN=<YOUR_SECRET_ARN>
export DD_SITE=<YOUR_DATADOG_SITE>
export AWS_REGION=us-east-1
serverless deploy --param="DD_API_KEY_SECRET_ARN=${DD_API_KEY_SECRET_ARN}" --param="DD_SITE=${DD_SITE}" --stage dev --region=${AWS_REGION}serverless remove --param="DD_API_KEY_SECRET_ARN=${DD_API_KEY_SECRET_ARN}" --param="DD_SITE=${DD_SITE}" --stage dev --region=${AWS_REGION}Uses SST v2 with AWS CDK under the hood. Configuration is in sst.config.ts, which reuses the same CDK stack definitions.
export DD_API_KEY=<YOUR_DATADOG_API_KEY>
export DD_SITE=<YOUR_DATADOG_SITE>
npm run dev:sstThis runs Lambda functions locally while interacting with remote AWS resources. Use the API URL printed on the terminal for testing.
npm run deploy:sstnpm run remove:sst # Remove dev stage
npm run remove:sst:personal # Remove personal stageThe service demonstrates several Datadog observability patterns for asynchronous, event-driven architectures.
Instead of creating deep parent-child trace hierarchies across service boundaries, this service uses Span Links to connect causally related spans. See src/observability/observability.ts for the implementation.
This requires disabling automatic trace propagation on the consumer Lambda functions:
DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=none
DD_TRACE_PROPAGATION_STYLE_EXTRACT=false
Message processing and publishing spans follow the OTel Semantic Conventions for Messaging, including attributes like messaging.system, messaging.operation.type, messaging.message.type, and messaging.destination.name.
Data Streams Monitoring (DSM) checkpoints are recorded manually for both consume and produce paths, since DSM does not automatically support all messaging transports. See the setConsumeCheckpoint and setProduceCheckpoint calls in observability.ts.
.
├── bin/ # CDK app entry point
├── lib/
│ ├── constructs/ # Reusable CDK constructs (InstrumentedFunction, ResilientQueue)
│ ├── loyalty-api/ # CDK stack definitions (API, ACL, props)
│ └── loyalty-tier-workflow/ # CDK construct for the durable tier upgrade workflow
├── src/
│ ├── loyalty-api/
│ │ ├── adapters/ # Lambda handler entry points and infrastructure adapters
│ │ └── core/ # Domain logic, DTOs, event definitions
│ ├── loyalty-tier-workflow/
│ │ ├── trigger/ # TierUpgradeTrigger handler + esbuild config
│ │ ├── orchestrator/ # TierUpgradeOrchestrator (durable) handler + esbuild config
│ │ ├── activities/ # FetchOrderHistoryActivity handler + esbuild config
│ │ ├── acknowledger/ # NotificationAcknowledger handler + esbuild config
│ │ └── core/ # Tier domain logic, repository interface, and adapters
│ └── observability/ # Shared tracing and DSM helpers
├── tests/
│ └── loyalty-service-tests/ # Integration tests (including tier upgrade workflow tests)
├── infra/ # Terraform configuration
├── template.yaml # SAM template
├── serverless.yml # Serverless Framework config
├── sst.config.ts # SST configuration
├── package.sh # Build script for SAM/Terraform deployments
├── Makefile # Shortcuts for build, deploy, and destroy
└── cdk.json # CDK project configuration