<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Renaldi</title>
    <description>The latest articles on DEV Community by Renaldi (@rengond).</description>
    <link>https://dev.to/rengond</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1152783%2F3f1b3ae1-987c-4daf-ac65-9d70e45a36aa.jpg</url>
      <title>DEV Community: Renaldi</title>
      <link>https://dev.to/rengond</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rengond"/>
    <language>en</language>
    <item>
      <title>Cost-Aware Serverless Architecture Reviews: A Practical Framework</title>
      <dc:creator>Renaldi</dc:creator>
      <pubDate>Sun, 12 Apr 2026 23:00:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/cost-aware-serverless-architecture-reviews-a-practical-framework-5agl</link>
      <guid>https://dev.to/aws-builders/cost-aware-serverless-architecture-reviews-a-practical-framework-5agl</guid>
      <description>&lt;p&gt;Cost optimization in serverless is often discussed as a pricing exercise. In practice, I have found it is much more effective when treated as an &lt;strong&gt;architecture review discipline&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is the lens I use in this post.&lt;/p&gt;

&lt;p&gt;This is a practical framework I use to review serverless systems for cost efficiency &lt;strong&gt;without weakening reliability, operability, or delivery speed&lt;/strong&gt;. It is designed for engineers, architects, and technical leads who want to go beyond “reduce Lambda memory” and make better design decisions across services.&lt;/p&gt;

&lt;p&gt;I will focus on AWS serverless building blocks and the tradeoffs between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AWS Lambda&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AWS Step Functions&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Amazon EventBridge Pipes&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct service integrations&lt;/strong&gt; (for example, Step Functions SDK/service integrations)&lt;/li&gt;
&lt;li&gt;Supporting services like &lt;strong&gt;SQS, EventBridge, CloudWatch Logs, DynamoDB, and S3&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I will cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cost hotspots in serverless (invocations, state transitions, logs, data transfer)&lt;/li&gt;
&lt;li&gt;Architecture changes that reduce cost without harming reliability&lt;/li&gt;
&lt;li&gt;Tradeoffs: Lambda vs Pipes vs service integrations vs Step Functions&lt;/li&gt;
&lt;li&gt;Workload profiling and right-sizing patterns&lt;/li&gt;
&lt;li&gt;End-to-end walkthrough and implementation discussion&lt;/li&gt;
&lt;li&gt;Architecture and code examples you can adapt&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why this topic stands out
&lt;/h2&gt;

&lt;p&gt;I like this topic because it is senior-level but immediately useful.&lt;/p&gt;

&lt;p&gt;Almost every serverless system starts cost-efficient, then gradually accumulates “cost drag” as features are added:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extra orchestration steps&lt;/li&gt;
&lt;li&gt;Helper Lambdas that only transform payloads&lt;/li&gt;
&lt;li&gt;Verbose logging left on in production&lt;/li&gt;
&lt;li&gt;Duplicate event fan-out and reprocessing&lt;/li&gt;
&lt;li&gt;Cross-AZ / cross-region / internet egress paths that nobody revisits&lt;/li&gt;
&lt;li&gt;Overbuilt retries and polling loops&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are necessarily “wrong.” In many cases they are the fastest way to ship.&lt;/p&gt;

&lt;p&gt;But once traffic grows, it becomes worth doing a structured review.&lt;/p&gt;

&lt;p&gt;The goal is not to squeeze every cent out of the system. The goal is to &lt;strong&gt;spend intentionally&lt;/strong&gt;, with a clear understanding of where cost comes from and what reliability/maintainability benefits we are buying with that spend.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I mean by a cost-aware serverless architecture review
&lt;/h2&gt;

&lt;p&gt;A cost-aware review is a design review that asks four questions for each path in the workload:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What is the unit of work?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One event, one request, one file, one state transition, one batch, one customer action&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;What services does that unit of work touch?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lambda, Step Functions, EventBridge, SQS, DynamoDB, logs, storage, network paths&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Which of those touches scales linearly with traffic, and which scales with payload size, duration, or retries?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This is where the real hotspots show up&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Can I reduce cost by changing the architecture instead of only tuning a single service?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Example: removing three “glue Lambdas” with native integrations&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last question is the most important one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Framework Overview
&lt;/h2&gt;

&lt;p&gt;This is the practical framework I use in reviews:&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Profile the workload
&lt;/h3&gt;

&lt;p&gt;Understand &lt;strong&gt;traffic shape&lt;/strong&gt; and &lt;strong&gt;execution shape&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request rate, concurrency, burstiness&lt;/li&gt;
&lt;li&gt;payload sizes&lt;/li&gt;
&lt;li&gt;durations&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;fan-out counts&lt;/li&gt;
&lt;li&gt;error rates&lt;/li&gt;
&lt;li&gt;cold-start sensitivity&lt;/li&gt;
&lt;li&gt;async vs sync paths&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Phase 2: Map the cost path
&lt;/h3&gt;

&lt;p&gt;Trace the cost of one unit of work across services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invocations&lt;/li&gt;
&lt;li&gt;transitions&lt;/li&gt;
&lt;li&gt;messages&lt;/li&gt;
&lt;li&gt;logs ingested/stored&lt;/li&gt;
&lt;li&gt;storage reads/writes&lt;/li&gt;
&lt;li&gt;data transfer&lt;/li&gt;
&lt;li&gt;optional downstream analytics/monitoring copies&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Phase 3: Identify hotspots
&lt;/h3&gt;

&lt;p&gt;Rank cost drivers by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;total monthly impact&lt;/li&gt;
&lt;li&gt;growth rate with scale&lt;/li&gt;
&lt;li&gt;ease of remediation&lt;/li&gt;
&lt;li&gt;risk of change&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Phase 4: Apply architecture changes
&lt;/h3&gt;

&lt;p&gt;Prefer changes that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;remove unnecessary compute hops&lt;/li&gt;
&lt;li&gt;reduce chatty orchestration&lt;/li&gt;
&lt;li&gt;batch operations safely&lt;/li&gt;
&lt;li&gt;right-size compute from measured data&lt;/li&gt;
&lt;li&gt;preserve observability and failure isolation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Phase 5: Re-measure
&lt;/h3&gt;

&lt;p&gt;I treat cost optimization as an engineering loop:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;baseline&lt;/li&gt;
&lt;li&gt;change&lt;/li&gt;
&lt;li&gt;compare&lt;/li&gt;
&lt;li&gt;document tradeoffs&lt;/li&gt;
&lt;li&gt;repeat&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F29621dle73xl2tqxdq9f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F29621dle73xl2tqxdq9f.png" alt=" " width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  End-to-End Walkthrough
&lt;/h2&gt;

&lt;p&gt;To make this concrete, I will walk through a realistic example I often see:&lt;/p&gt;

&lt;h3&gt;
  
  
  Example workload: Event-driven order processing pipeline
&lt;/h3&gt;

&lt;p&gt;A retail platform emits an &lt;code&gt;OrderCreated&lt;/code&gt; event. The serverless pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validates the payload&lt;/li&gt;
&lt;li&gt;Enriches customer metadata&lt;/li&gt;
&lt;li&gt;Writes the order to DynamoDB&lt;/li&gt;
&lt;li&gt;Sends a fulfillment request to an internal API&lt;/li&gt;
&lt;li&gt;Publishes notifications&lt;/li&gt;
&lt;li&gt;Updates operational dashboards&lt;/li&gt;
&lt;li&gt;Triggers async follow-up workflows&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Baseline architecture (common version)
&lt;/h3&gt;

&lt;p&gt;A common initial design looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EventBridge rule receives &lt;code&gt;OrderCreated&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Step Functions orchestrates the flow&lt;/li&gt;
&lt;li&gt;Multiple Lambda functions perform:

&lt;ul&gt;
&lt;li&gt;input validation&lt;/li&gt;
&lt;li&gt;JSON transformation&lt;/li&gt;
&lt;li&gt;service API calls&lt;/li&gt;
&lt;li&gt;retries / status polling&lt;/li&gt;
&lt;li&gt;notification dispatch&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Every function logs full payloads&lt;/li&gt;

&lt;li&gt;Step Functions uses many small states to represent each transformation&lt;/li&gt;

&lt;li&gt;Synchronous calls are used where async could work&lt;/li&gt;

&lt;li&gt;Duplicate events are emitted for multiple downstream consumers&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;This is usually functional and maintainable, but over time I see four cost hotspots appear.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost hotspots in serverless (what I look for first)
&lt;/h2&gt;

&lt;h2&gt;
  
  
  1) Invocation-driven hotspots (Lambda, messaging, API calls)
&lt;/h2&gt;

&lt;p&gt;This is the most obvious one, but not always where the biggest savings are.&lt;/p&gt;

&lt;p&gt;I look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;High-frequency helper Lambdas that do only simple mapping/filtering&lt;/li&gt;
&lt;li&gt;Repeated invocations caused by retries at multiple layers (client, service, workflow)&lt;/li&gt;
&lt;li&gt;Tiny per-item processing when batching is safe&lt;/li&gt;
&lt;li&gt;Polling loops implemented in Lambda instead of orchestration or event callbacks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Common anti-pattern
&lt;/h3&gt;

&lt;p&gt;A Lambda is invoked just to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rename fields&lt;/li&gt;
&lt;li&gt;add a constant&lt;/li&gt;
&lt;li&gt;route based on one attribute&lt;/li&gt;
&lt;li&gt;pass the payload to another service&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In many cases this can be replaced with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EventBridge input transformation&lt;/li&gt;
&lt;li&gt;EventBridge Pipes filtering/enrichment&lt;/li&gt;
&lt;li&gt;Step Functions &lt;code&gt;Parameters&lt;/code&gt; / &lt;code&gt;ResultSelector&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Step Functions direct service integrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That removes both compute cost and operational surface area.&lt;/p&gt;




&lt;h2&gt;
  
  
  2) State transition hotspots (Step Functions)
&lt;/h2&gt;

&lt;p&gt;I see this frequently in mature systems.&lt;/p&gt;

&lt;p&gt;Step Functions is excellent, but cost can grow when workflows become overly chatty:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One state per trivial transformation&lt;/li&gt;
&lt;li&gt;Frequent polling loops with short waits&lt;/li&gt;
&lt;li&gt;Repetitive branching for small decisions&lt;/li&gt;
&lt;li&gt;Using Standard workflows for high-volume, very short paths without reviewing fit&lt;/li&gt;
&lt;li&gt;Nested workflows for steps that could be in one integration&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What I optimize carefully
&lt;/h3&gt;

&lt;p&gt;I do &lt;strong&gt;not&lt;/strong&gt; optimize by blindly collapsing everything into one Lambda. That usually hurts observability and retry control.&lt;/p&gt;

&lt;p&gt;Instead, I look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;eliminating trivial pass/transform states&lt;/li&gt;
&lt;li&gt;replacing glue Lambdas with direct integrations&lt;/li&gt;
&lt;li&gt;increasing polling intervals or switching to callback/event-driven completions&lt;/li&gt;
&lt;li&gt;splitting hot short-lived paths from long-running orchestration&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3) Logging hotspots (CloudWatch Logs)
&lt;/h2&gt;

&lt;p&gt;This one is often underestimated because logging is “cheap enough” until scale increases.&lt;/p&gt;

&lt;p&gt;Cost growth comes from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Logging full request/response payloads for every invocation&lt;/li&gt;
&lt;li&gt;Debug-level logs enabled permanently&lt;/li&gt;
&lt;li&gt;Duplicate logs across Lambda, workflow, and application layers&lt;/li&gt;
&lt;li&gt;Large structured objects written multiple times&lt;/li&gt;
&lt;li&gt;High-cardinality logs that are rarely queried&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Practical rule I use
&lt;/h3&gt;

&lt;p&gt;I keep logs &lt;strong&gt;useful and minimal&lt;/strong&gt; in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;log identifiers, not entire payloads&lt;/li&gt;
&lt;li&gt;sample verbose payload logs&lt;/li&gt;
&lt;li&gt;gate debug logs by environment/feature flag&lt;/li&gt;
&lt;li&gt;separate audit records from debug logs&lt;/li&gt;
&lt;li&gt;set explicit retention periods&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In serverless, “just logging more” can become a real cost driver.&lt;/p&gt;




&lt;h2&gt;
  
  
  4) Data transfer hotspots (often missed in reviews)
&lt;/h2&gt;

&lt;p&gt;Data transfer is easy to miss because the architecture diagram looks “serverless,” but network paths still matter.&lt;/p&gt;

&lt;p&gt;I review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cross-region calls between Lambda and downstream services&lt;/li&gt;
&lt;li&gt;Internet egress to SaaS APIs&lt;/li&gt;
&lt;li&gt;Repeated large payload movement between services&lt;/li&gt;
&lt;li&gt;Download/re-upload patterns (for example, pulling S3 objects through Lambda when direct processing is possible)&lt;/li&gt;
&lt;li&gt;VPC-attached Lambda traffic patterns if applicable (including NAT-related patterns)&lt;/li&gt;
&lt;li&gt;Sending oversized event payloads when a pointer (S3 key/object reference) would work&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pattern I prefer
&lt;/h3&gt;

&lt;p&gt;Move &lt;strong&gt;references&lt;/strong&gt; more often than &lt;strong&gt;full payloads&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Put large content in S3&lt;/li&gt;
&lt;li&gt;Pass object keys / IDs in events&lt;/li&gt;
&lt;li&gt;Fetch only where necessary&lt;/li&gt;
&lt;li&gt;Trim event payloads to fields required for the next step&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This reduces both transfer and execution overhead.&lt;/p&gt;




&lt;h1&gt;
  
  
  A Practical Review Framework I Use in Real Projects
&lt;/h1&gt;

&lt;p&gt;Below is the review structure I use during architecture reviews. It is intentionally simple so teams can repeat it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Define the unit economics
&lt;/h2&gt;

&lt;p&gt;I start with a unit of work, for example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“One &lt;code&gt;OrderCreated&lt;/code&gt; event processed successfully”&lt;/li&gt;
&lt;li&gt;“One failed event retried and sent to DLQ”&lt;/li&gt;
&lt;li&gt;“One batch of 100 events processed”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I estimate or measure the service touches for that unit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example (baseline, illustrative)
&lt;/h3&gt;

&lt;p&gt;For one order:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EventBridge ingress: 1 event&lt;/li&gt;
&lt;li&gt;Step Functions states: 22 transitions&lt;/li&gt;
&lt;li&gt;Lambda invocations: 8&lt;/li&gt;
&lt;li&gt;DynamoDB writes: 2&lt;/li&gt;
&lt;li&gt;CloudWatch log lines: ~120&lt;/li&gt;
&lt;li&gt;One external API call&lt;/li&gt;
&lt;li&gt;One notification event&lt;/li&gt;
&lt;li&gt;Occasional retries on fulfillment call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The exact numbers vary, but this gives me a concrete path to improve.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Build a cost-path table (without overcomplicating it)
&lt;/h2&gt;

&lt;p&gt;I do not start with exact pricing spreadsheets. I start with a ranked table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Driver&lt;/strong&gt; (Lambda, Step Functions, logs, transfer, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scaling factor&lt;/strong&gt; (requests, duration, payload size, retries)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Observed value&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Risk/benefit if changed&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optimization candidate&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This quickly identifies which optimizations are architectural vs micro-tuning.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3: Profile the workload (shape matters more than averages)
&lt;/h2&gt;

&lt;p&gt;Averages hide the truth in serverless systems.&lt;/p&gt;

&lt;p&gt;I profile:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;p50/p95/p99 duration&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;payload size distribution&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;retry rate by dependency&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;burst concurrency&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;fan-out ratio&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;failure modes&lt;/strong&gt; (timeouts, throttles, validation errors)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;cold-start frequency&lt;/strong&gt; (only when relevant to SLOs)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why this matters
&lt;/h3&gt;

&lt;p&gt;A system with the same monthly volume can have very different costs if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one workload is smooth and small-payload&lt;/li&gt;
&lt;li&gt;the other is spiky, retry-heavy, and large-payload&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Architecture decisions should follow the shape, not just total count.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: Apply architecture changes in this order
&lt;/h2&gt;

&lt;p&gt;This is the sequence I use because it usually yields high value with lower risk:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Remove unnecessary service hops&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Replace glue compute with native integrations&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Batch where safe&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Right-size Lambda from measured duration/memory&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tune logs and retention&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Revisit orchestration granularity&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Optimize transfer/payload strategy&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Only then consider deeper redesigns&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h1&gt;
  
  
  Architecture Changes That Reduce Cost Without Harming Reliability
&lt;/h1&gt;

&lt;p&gt;Here are the changes I most commonly recommend, with the tradeoffs explained.&lt;/p&gt;

&lt;h2&gt;
  
  
  1) Replace glue Lambdas with service integrations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Before
&lt;/h3&gt;

&lt;p&gt;A Lambda receives a payload and only:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;transforms JSON&lt;/li&gt;
&lt;li&gt;calls DynamoDB / SQS / EventBridge&lt;/li&gt;
&lt;li&gt;returns a small result&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  After
&lt;/h3&gt;

&lt;p&gt;Use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step Functions AWS SDK/service integrations&lt;/li&gt;
&lt;li&gt;EventBridge input transformers&lt;/li&gt;
&lt;li&gt;EventBridge Pipes filter/enrichment steps (where appropriate)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why this helps
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Removes invocation and duration cost&lt;/li&gt;
&lt;li&gt;Reduces operational burden (fewer deployments, alarms, IAM roles)&lt;/li&gt;
&lt;li&gt;Improves determinism for simple operations&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Reliability note
&lt;/h3&gt;

&lt;p&gt;This can &lt;strong&gt;improve&lt;/strong&gt; reliability because fewer components means fewer failure points. The key is preserving observability and explicit error handling.&lt;/p&gt;




&lt;h2&gt;
  
  
  2) Use Pipes for simple event movement and filtering
&lt;/h2&gt;

&lt;p&gt;EventBridge Pipes is a strong fit when the path is mostly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;source -&amp;gt; filter -&amp;gt; optional transform/enrichment -&amp;gt; target&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Good fit examples
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;SQS -&amp;gt; target Lambda (with filtering)&lt;/li&gt;
&lt;li&gt;DynamoDB stream -&amp;gt; EventBridge bus&lt;/li&gt;
&lt;li&gt;Kinesis -&amp;gt; SQS&lt;/li&gt;
&lt;li&gt;SQS -&amp;gt; Step Functions (selective forwarding)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  When I do not force Pipes
&lt;/h3&gt;

&lt;p&gt;If the workflow needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;complex branching&lt;/li&gt;
&lt;li&gt;human approvals&lt;/li&gt;
&lt;li&gt;multi-step retries with business semantics&lt;/li&gt;
&lt;li&gt;long waits&lt;/li&gt;
&lt;li&gt;compensation logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then Step Functions is usually the better fit, even if the raw “per event movement” cost might not be the lowest.&lt;/p&gt;




&lt;h2&gt;
  
  
  3) Reduce Step Functions chatty orchestration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What I change
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Collapse trivial transform-only states&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;Parameters&lt;/code&gt;, &lt;code&gt;ResultSelector&lt;/code&gt;, and JSONPath/JSONata features instead of helper Lambdas&lt;/li&gt;
&lt;li&gt;Shift from polling to callback/event completion where feasible&lt;/li&gt;
&lt;li&gt;Split short high-volume flows from long-running exception flows&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What I keep
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Distinct states for failure boundaries&lt;/li&gt;
&lt;li&gt;States that need targeted retries/catches&lt;/li&gt;
&lt;li&gt;Business-visible milestones (auditability)&lt;/li&gt;
&lt;li&gt;Human review / approval checkpoints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not “fewer states at any cost.” The goal is &lt;strong&gt;intentional state design&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  4) Batch small units where the business semantics allow it
&lt;/h2&gt;

&lt;p&gt;Batching can reduce cost significantly across:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lambda invocations&lt;/li&gt;
&lt;li&gt;downstream API calls&lt;/li&gt;
&lt;li&gt;logs&lt;/li&gt;
&lt;li&gt;orchestration overhead&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Examples
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Process 10 records per Lambda invocation instead of 1&lt;/li&gt;
&lt;li&gt;Aggregate notifications&lt;/li&gt;
&lt;li&gt;Batch writes to DynamoDB when access patterns permit&lt;/li&gt;
&lt;li&gt;Buffer events briefly for downstream efficiency&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Caution
&lt;/h3&gt;

&lt;p&gt;Batching changes failure semantics. I only use it when I can answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What happens if item 7 of 10 fails?&lt;/li&gt;
&lt;li&gt;Do I need partial success reporting?&lt;/li&gt;
&lt;li&gt;Can the downstream system handle duplicates on retries?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reliability comes first. Cost savings that break recovery are not real savings.&lt;/p&gt;




&lt;h2&gt;
  
  
  5) Right-size Lambda with measured data, not guesswork
&lt;/h2&gt;

&lt;p&gt;Right-sizing is more than memory reduction. Since Lambda allocates CPU proportionally to memory, a lower-memory function can become slower and more expensive overall.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I measure
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;p50/p95 duration&lt;/li&gt;
&lt;li&gt;memory usage&lt;/li&gt;
&lt;li&gt;init duration (where relevant)&lt;/li&gt;
&lt;li&gt;timeout headroom&lt;/li&gt;
&lt;li&gt;downstream latency contribution&lt;/li&gt;
&lt;li&gt;retry behavior after timeouts/throttles&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What I look for
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Functions with high duration and low memory usage (often downstream-bound)&lt;/li&gt;
&lt;li&gt;Functions with CPU-bound work that benefit from more memory&lt;/li&gt;
&lt;li&gt;Functions with large package sizes causing slower init&lt;/li&gt;
&lt;li&gt;Functions using &lt;code&gt;/tmp&lt;/code&gt; heavily (may benefit from memory/storage review)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I optimize for &lt;strong&gt;cost per successful unit of work&lt;/strong&gt;, not cost per invocation in isolation.&lt;/p&gt;




&lt;h2&gt;
  
  
  6) Tune logging intentionally (not minimally)
&lt;/h2&gt;

&lt;p&gt;I do not recommend “turn off logs.” I recommend &lt;strong&gt;structured, useful, cost-conscious logging&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Practical changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Move full payload dumps behind a sampled debug flag&lt;/li&gt;
&lt;li&gt;Log IDs and metrics by default&lt;/li&gt;
&lt;li&gt;Use explicit log retention (for example, shorter for noisy operational logs)&lt;/li&gt;
&lt;li&gt;Avoid duplicate payload logging at every layer&lt;/li&gt;
&lt;li&gt;Emit metrics for counts/latency instead of reconstructing them from logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This usually reduces cost while improving signal-to-noise.&lt;/p&gt;




&lt;h2&gt;
  
  
  7) Pass references, not large payloads
&lt;/h2&gt;

&lt;p&gt;Instead of pushing large documents or objects through multiple services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store payloads in S3&lt;/li&gt;
&lt;li&gt;Pass a pointer in the event/workflow&lt;/li&gt;
&lt;li&gt;Fetch on demand&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lower transfer volume&lt;/li&gt;
&lt;li&gt;smaller event payloads&lt;/li&gt;
&lt;li&gt;faster orchestration payload handling&lt;/li&gt;
&lt;li&gt;cleaner replayability&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This pattern is especially helpful in document, media, and analytics workflows.&lt;/p&gt;




&lt;h1&gt;
  
  
  Tradeoffs: Lambda vs Pipes vs Service Integrations vs Step Functions
&lt;/h1&gt;

&lt;p&gt;This is the part teams usually want summarized, so here is the practical version I use in reviews.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lambda is best when:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I need custom code/business logic&lt;/li&gt;
&lt;li&gt;I need third-party SDKs or protocols&lt;/li&gt;
&lt;li&gt;I need non-trivial validation/transformation&lt;/li&gt;
&lt;li&gt;I need specialized retry behavior inside the function&lt;/li&gt;
&lt;li&gt;I need to encapsulate logic reused across multiple flows&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Lambda is overkill when:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I am only mapping fields&lt;/li&gt;
&lt;li&gt;I am just forwarding to another AWS service&lt;/li&gt;
&lt;li&gt;I can use native service integration safely and clearly&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  EventBridge Pipes is best when:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The path is primarily source-to-target movement&lt;/li&gt;
&lt;li&gt;I need filtering and simple enrichment&lt;/li&gt;
&lt;li&gt;I want low operational overhead for plumbing&lt;/li&gt;
&lt;li&gt;I want to avoid writing a “routing Lambda”&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pipes is not the best fit when:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I need complex multi-step orchestration&lt;/li&gt;
&lt;li&gt;I need long waits or human callbacks&lt;/li&gt;
&lt;li&gt;I need rich compensation logic across steps&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step Functions is best when:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I need explicit orchestration and auditability&lt;/li&gt;
&lt;li&gt;I need retries/catches at step boundaries&lt;/li&gt;
&lt;li&gt;I need long-running workflows&lt;/li&gt;
&lt;li&gt;I need human-in-the-loop or callback patterns&lt;/li&gt;
&lt;li&gt;I need clear operational visualization for a business process&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step Functions can become costly when:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I model every trivial transform as a state&lt;/li&gt;
&lt;li&gt;I use frequent polling instead of event-driven completion&lt;/li&gt;
&lt;li&gt;I keep hot paths in Standard without reviewing alternatives&lt;/li&gt;
&lt;li&gt;I use nested workflows where direct integrations would do&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Direct service integrations are best when:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The step is simple and deterministic&lt;/li&gt;
&lt;li&gt;I can express the call declaratively&lt;/li&gt;
&lt;li&gt;I want fewer moving parts&lt;/li&gt;
&lt;li&gt;I want to remove glue code and reduce operational burden&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Direct integrations may be a poor fit when:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I need complex custom serialization/validation&lt;/li&gt;
&lt;li&gt;I need cross-service custom logic not represented well declaratively&lt;/li&gt;
&lt;li&gt;The readability of the workflow would suffer too much&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  End-to-End Optimization Walkthrough (Baseline -&amp;gt; Improved)
&lt;/h1&gt;

&lt;p&gt;Now I will apply the framework to the example order pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Baseline (illustrative)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;8 Lambda invocations per order&lt;/li&gt;
&lt;li&gt;22 Step Functions transitions&lt;/li&gt;
&lt;li&gt;verbose logs in every function&lt;/li&gt;
&lt;li&gt;large payload passed between multiple steps&lt;/li&gt;
&lt;li&gt;polling Lambda for fulfillment status&lt;/li&gt;
&lt;li&gt;duplicate notification paths&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Review findings
&lt;/h2&gt;

&lt;p&gt;Top cost hotspots:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Step Functions state transitions&lt;/strong&gt; from chatty orchestration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda invocations&lt;/strong&gt; for glue transforms and simple service calls&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudWatch Logs ingestion&lt;/strong&gt; due to payload dumps&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data transfer/execution overhead&lt;/strong&gt; from oversized payloads&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retries&lt;/strong&gt; multiplying costs during downstream API flakiness&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Improved architecture (same reliability goals)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Changes I make
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Replace 3 glue Lambdas with:

&lt;ul&gt;
&lt;li&gt;Step Functions &lt;code&gt;Parameters&lt;/code&gt; / &lt;code&gt;ResultSelector&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;direct DynamoDB/EventBridge integrations&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Replace one routing Lambda with &lt;strong&gt;EventBridge Pipes&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Move large enrichment blobs to S3 and pass object references&lt;/li&gt;
&lt;li&gt;Replace polling Lambda loop with callback/event-driven completion&lt;/li&gt;
&lt;li&gt;Keep Step Functions states at clear failure boundaries only&lt;/li&gt;
&lt;li&gt;Reduce default log verbosity and set retention explicitly&lt;/li&gt;
&lt;li&gt;Batch non-critical notifications&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What I do &lt;strong&gt;not&lt;/strong&gt; change
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Idempotency controls&lt;/li&gt;
&lt;li&gt;DLQs and retry policies&lt;/li&gt;
&lt;li&gt;Key audit checkpoints&lt;/li&gt;
&lt;li&gt;Alarms and traces for critical dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is important. I want lower cost &lt;strong&gt;without&lt;/strong&gt; creating an opaque, fragile pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture pattern (review-ready version)
&lt;/h2&gt;

&lt;p&gt;The optimized pattern I recommend for many teams looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ingress:&lt;/strong&gt; EventBridge&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plumbing/filtering:&lt;/strong&gt; EventBridge Pipes (where simple)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orchestration:&lt;/strong&gt; Step Functions for business process + callbacks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compute:&lt;/strong&gt; Lambda only for genuine custom logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; S3 for large payloads, DynamoDB for metadata/state&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability:&lt;/strong&gt; metrics first, targeted logs, traces on critical paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is cost-aware because each service is doing the kind of work it is best suited for.&lt;/p&gt;




&lt;h1&gt;
  
  
  Implementation Discussion
&lt;/h1&gt;

&lt;p&gt;Below are code examples that demonstrate how I apply the framework in practice.&lt;/p&gt;




&lt;h2&gt;
  
  
  1) Step Functions workflow using service integrations instead of glue Lambdas (ASL fragment)
&lt;/h2&gt;

&lt;p&gt;This fragment shows a flow that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;validates and prepares payload once&lt;/li&gt;
&lt;li&gt;writes to DynamoDB via a direct integration&lt;/li&gt;
&lt;li&gt;emits an event via EventBridge integration&lt;/li&gt;
&lt;li&gt;keeps Lambda only for actual custom logic
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Comment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Cost-aware serverless orchestration example"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"StartAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ValidateAndEnrich"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"States"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ValidateAndEnrich"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::lambda:invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FunctionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${ValidateAndEnrichFnArn}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Payload.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"OutputPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.Payload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PutOrderMetadata"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Retry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ErrorEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Lambda.ServiceException"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Lambda.TooManyRequestsException"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"IntervalSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"BackoffRate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"MaxAttempts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PutOrderMetadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::aws-sdk:dynamodb:putItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"TableName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${OrdersTable}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"pk"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"S.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"States.Format('ORDER#{}', $.orderId)"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"sk"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"S"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"META"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"S"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RECEIVED"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"S.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.customerId"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"payloadRef"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"S.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.payloadRef.s3Uri"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"S.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.timestamps.receivedAt"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ConditionExpression"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"attribute_not_exists(pk)"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PublishOrderReceived"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PublishOrderReceived"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::events:putEvents"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Entries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.example.orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"DetailType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"order.received"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"EventBusName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${EventBusName}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"orderId.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.orderId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"customerId.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.customerId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"payloadRef.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.payloadRef"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"correlationId.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.correlationId"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WaitForFulfillmentCallback"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"WaitForFulfillmentCallback"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::lambda:invoke.waitForTaskToken"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FunctionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${CreateFulfillmentRequestFnArn}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"taskToken.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$$.Task.Token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"orderId.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.orderId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"payloadRef.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.payloadRef"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"correlationId.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.correlationId"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"TimeoutSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ResultPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.fulfillment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FinalizeOrder"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"FinalizeOrder"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::aws-sdk:dynamodb:updateItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"TableName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${OrdersTable}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"pk"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"S.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"States.Format('ORDER#{}', $.orderId)"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"sk"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"S"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"META"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"UpdateExpression"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SET #s = :s, fulfillmentId = :f, fulfilledAt = :t"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ExpressionAttributeNames"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"#s"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ExpressionAttributeValues"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;":s"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"S"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FULFILLED"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;":f"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"S.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.fulfillment.fulfillmentId"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;":t"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"S.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.fulfillment.completedAt"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"End"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why this is cost-aware
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;I removed two common glue Lambdas (&lt;code&gt;put item&lt;/code&gt;, &lt;code&gt;publish event&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;I use callback instead of chatty polling&lt;/li&gt;
&lt;li&gt;I keep orchestration only where it adds reliability/visibility&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2) EventBridge Pipes (CDK TypeScript) for low-overhead routing/filtering
&lt;/h2&gt;

&lt;p&gt;This example shows a common “plumbing” path I do not want to solve with a custom Lambda.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-cdk-lib&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pipes&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-cdk-lib/aws-pipes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-cdk-lib/aws-iam&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pipeRole&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Role&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OrdersPipeRole&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;assumedBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ServicePrincipal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pipes.amazonaws.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;pipeRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PolicyStatement&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sqs:ReceiveMessage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sqs:DeleteMessage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sqs:GetQueueAttributes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sqs:ChangeMessageVisibility&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ordersQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queueArn&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="nx"&gt;pipeRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addToPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;PolicyStatement&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;states:StartExecution&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;orderWorkflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stateMachineArn&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;pipes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnPipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FilteredOrdersPipe&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;roleArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pipeRole&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;roleArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ordersQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queueArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;orderWorkflow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stateMachineArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sourceParameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;sqsQueueParameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;batchSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;maximumBatchingWindowInSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;filterCriteria&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;eventType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OrderCreated&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
              &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;standard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;targetParameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;stepFunctionStateMachineParameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;invocationType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FIRE_AND_FORGET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;inputTemplate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;$.body.orderId&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;$.body.customerId&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;$.body.priority&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;payloadRef&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;s3Uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;$.body.payloadRef.s3Uri&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;correlationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;$.body.correlationId&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why this is cost-aware
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;No routing Lambda to maintain&lt;/li&gt;
&lt;li&gt;Built-in filtering avoids unnecessary workflow starts&lt;/li&gt;
&lt;li&gt;Batching reduces per-message overhead while preserving throughput&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3) Lambda right-sizing profiling helper (Python)
&lt;/h2&gt;

&lt;p&gt;I use a simple embedded metric approach to log what matters for right-sizing reviews. This is not a full observability solution, but it is enough to drive architecture decisions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;emit_emf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;emf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_aws&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CloudWatchMetrics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Namespace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Dimensions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;())],&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Metrics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;None&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()],&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;emf&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;payload_size_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# ... custom logic here ...
&lt;/span&gt;    &lt;span class="c1"&gt;# Keep this function for genuine business logic, not simple routing
&lt;/span&gt;
    &lt;span class="n"&gt;duration_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
    &lt;span class="n"&gt;configured_memory_mb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS_LAMBDA_FUNCTION_MEMORY_SIZE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;128&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="nf"&gt;emit_emf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;namespace&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ServerlessReview&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FunctionName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Environment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STAGE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DurationMs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;duration_ms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PayloadSizeBytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payload_size_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ConfiguredMemoryMB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;configured_memory_mb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processedAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How I use this data in reviews
&lt;/h3&gt;

&lt;p&gt;I compare:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;duration by memory setting&lt;/li&gt;
&lt;li&gt;duration by payload size bucket&lt;/li&gt;
&lt;li&gt;timeout proximity&lt;/li&gt;
&lt;li&gt;retry rates correlated with duration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I decide whether to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;increase memory (reduce duration and total cost)&lt;/li&gt;
&lt;li&gt;decrease memory (if clearly overprovisioned)&lt;/li&gt;
&lt;li&gt;batch inputs&lt;/li&gt;
&lt;li&gt;move logic out of Lambda entirely&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4) Production-safe logging pattern (Python)
&lt;/h2&gt;

&lt;p&gt;This pattern reduces log cost without sacrificing debuggability.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;

&lt;span class="n"&gt;LOG_LEVEL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LOG_LEVEL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;DEBUG_SAMPLE_RATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEBUG_SAMPLE_RATE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# 1% default
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;should_log_debug_payload&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;LOG_LEVEL&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEBUG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;DEBUG_SAMPLE_RATE&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;log_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;level&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;log_debug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;LOG_LEVEL&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEBUG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;level&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEBUG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;correlation_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correlationId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orderId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;log_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processing_order&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;correlationId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;correlation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;should_log_debug_payload&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;level&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DEBUG_SAMPLED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_payload_sample&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correlationId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;correlation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orderId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;
        &lt;span class="p"&gt;}))&lt;/span&gt;

    &lt;span class="c1"&gt;# ... business logic ...
&lt;/span&gt;
    &lt;span class="nf"&gt;log_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_processed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;correlationId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;correlation_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why I like this pattern
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Production logs stay useful and cheaper&lt;/li&gt;
&lt;li&gt;I still have sampled payload visibility for troubleshooting&lt;/li&gt;
&lt;li&gt;It works well with structured log queries and metrics extraction&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Workload Profiling and Right-Sizing Patterns
&lt;/h1&gt;

&lt;p&gt;This is the part that makes the review credible. I do not want architecture recommendations based on intuition alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  1) Profile by workload class, not just service
&lt;/h2&gt;

&lt;p&gt;I segment workloads into classes, for example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Latency-sensitive sync API path&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Async event processing path&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Burst batch path&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Low-frequency admin path&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each class has different priorities and cost levers.&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sync API path may justify higher Lambda memory for lower latency&lt;/li&gt;
&lt;li&gt;Async batch path may prioritize batching and reduced transitions&lt;/li&gt;
&lt;li&gt;Admin path may not be worth heavy optimization effort&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2) Build a “cost to reliability” tradeoff table
&lt;/h2&gt;

&lt;p&gt;For each proposed change, I document:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Change&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expected cost impact&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reliability impact&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operability impact&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Rollback plan&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measurement plan&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps reviews grounded and avoids “optimize everything” behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example entries
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Replace routing Lambda with Pipe -&amp;gt; lower cost, lower ops burden, neutral reliability, easy rollback&lt;/li&gt;
&lt;li&gt;Collapse 5 workflow states into 2 -&amp;gt; moderate cost savings, possible loss of debug granularity, test carefully&lt;/li&gt;
&lt;li&gt;Batch writes by 25 -&amp;gt; potentially high savings, changes failure semantics, requires idempotency validation&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  3) Right-size from p95 and timeout headroom, not p50 alone
&lt;/h2&gt;

&lt;p&gt;A function that looks fine at p50 may:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;time out at p99&lt;/li&gt;
&lt;li&gt;retry frequently during bursts&lt;/li&gt;
&lt;li&gt;create downstream contention that increases total cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I tune using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;p95/p99 duration&lt;/li&gt;
&lt;li&gt;timeout headroom&lt;/li&gt;
&lt;li&gt;retry counts&lt;/li&gt;
&lt;li&gt;dependency latency distribution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This avoids false savings that increase retries and operational incidents.&lt;/p&gt;




&lt;h2&gt;
  
  
  4) Profile payload size and serialization overhead
&lt;/h2&gt;

&lt;p&gt;In event-driven systems, large payloads can create hidden costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;more transfer&lt;/li&gt;
&lt;li&gt;longer execution time&lt;/li&gt;
&lt;li&gt;larger logs&lt;/li&gt;
&lt;li&gt;larger Step Functions state payloads&lt;/li&gt;
&lt;li&gt;more memory pressure in Lambda&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I explicitly bucket payload sizes and look for outliers.&lt;/p&gt;

&lt;p&gt;A common improvement is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;move large optional fields to S3&lt;/li&gt;
&lt;li&gt;pass references&lt;/li&gt;
&lt;li&gt;fetch lazily only in the steps that need them&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5) Profile retries as a cost multiplier
&lt;/h2&gt;

&lt;p&gt;Retries are often the hidden multiplier in “unexpected” serverless cost growth.&lt;/p&gt;

&lt;p&gt;I measure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retry rate per dependency&lt;/li&gt;
&lt;li&gt;retry amplification across layers (SDK retry + Lambda retry + workflow retry)&lt;/li&gt;
&lt;li&gt;cost of failed attempts vs successful attempts&lt;/li&gt;
&lt;li&gt;DLQ rate and replay volume&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I redesign retry placement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retry at the layer with the best context&lt;/li&gt;
&lt;li&gt;avoid stacking retries unnecessarily&lt;/li&gt;
&lt;li&gt;use backoff and circuit-breaking patterns where appropriate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This improves both cost and stability.&lt;/p&gt;




&lt;h1&gt;
  
  
  Review Checklist I Actually Use
&lt;/h1&gt;

&lt;p&gt;Here is the short version of the checklist I use in architecture reviews.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hotspots
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Which cost components scale with request count?&lt;/li&gt;
&lt;li&gt;[ ] Which scale with duration, payload size, retries, or fan-out?&lt;/li&gt;
&lt;li&gt;[ ] Where are we logging too much?&lt;/li&gt;
&lt;li&gt;[ ] Where are we moving data unnecessarily?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Architecture fit
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Is Lambda only used where custom logic is truly needed?&lt;/li&gt;
&lt;li&gt;[ ] Can any glue Lambda be replaced with Pipes or direct integration?&lt;/li&gt;
&lt;li&gt;[ ] Is Step Functions state granularity intentional?&lt;/li&gt;
&lt;li&gt;[ ] Are polling loops justified, or can callbacks/events replace them?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Profiling
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Do we have p95/p99 duration data?&lt;/li&gt;
&lt;li&gt;[ ] Do we know payload size distribution?&lt;/li&gt;
&lt;li&gt;[ ] Do we know retry rate by dependency?&lt;/li&gt;
&lt;li&gt;[ ] Do we know burst concurrency patterns?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reliability guardrails
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Will the optimization change failure semantics?&lt;/li&gt;
&lt;li&gt;[ ] Is idempotency preserved?&lt;/li&gt;
&lt;li&gt;[ ] Are DLQ/replay paths intact?&lt;/li&gt;
&lt;li&gt;[ ] Is observability still sufficient?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rollout
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Can we canary or phase the change?&lt;/li&gt;
&lt;li&gt;[ ] Do we have before/after metrics defined?&lt;/li&gt;
&lt;li&gt;[ ] Do we have a rollback path?&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Common Mistakes in Cost Reviews
&lt;/h1&gt;

&lt;p&gt;I see these often, so I call them out explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 1: Only tuning Lambda memory
&lt;/h2&gt;

&lt;p&gt;Useful, but incomplete. The biggest cost issue may be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step Functions transitions&lt;/li&gt;
&lt;li&gt;logs&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;data movement&lt;/li&gt;
&lt;li&gt;unnecessary service hops&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Mistake 2: Replacing orchestration with one “mega Lambda”
&lt;/h2&gt;

&lt;p&gt;This can reduce one visible cost but create:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;worse reliability&lt;/li&gt;
&lt;li&gt;harder retries&lt;/li&gt;
&lt;li&gt;worse observability&lt;/li&gt;
&lt;li&gt;slower delivery velocity&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Mistake 3: Optimizing by averages
&lt;/h2&gt;

&lt;p&gt;p50 hides burst pain and retry amplification.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 4: Ignoring developer productivity
&lt;/h2&gt;

&lt;p&gt;A design that is slightly cheaper but much harder to maintain may cost more overall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistake 5: Removing logs indiscriminately
&lt;/h2&gt;

&lt;p&gt;The right answer is better logging design, not blind log suppression.&lt;/p&gt;




&lt;h1&gt;
  
  
  Closing Thoughts
&lt;/h1&gt;

&lt;p&gt;A strong cost-aware serverless review is not about chasing the cheapest architecture on paper. It is about making architecture choices that fit the workload, preserve reliability, and keep operating costs proportional to business value.&lt;/p&gt;

&lt;p&gt;The framework I use is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Profile the workload&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Map the cost path&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Find hotspots&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Apply architecture changes first&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Right-size from measurements&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Re-measure and document tradeoffs&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I am reviewing a production serverless system, I want the outcome to be more than a list of tips. I want a clear plan that the team can implement safely, measure, and iterate on.&lt;/p&gt;

&lt;p&gt;That is what makes the review practical.&lt;/p&gt;




&lt;h1&gt;
  
  
  References
&lt;/h1&gt;

&lt;p&gt;(Consolidated at the end per request)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS Lambda pricing, performance tuning guidance, and memory/CPU behavior documentation&lt;/li&gt;
&lt;li&gt;AWS Step Functions pricing and workflow design guidance (Standard vs Express)&lt;/li&gt;
&lt;li&gt;AWS Step Functions service integrations and callback/task token patterns&lt;/li&gt;
&lt;li&gt;Amazon EventBridge and EventBridge Pipes documentation (filtering, enrichment, targets)&lt;/li&gt;
&lt;li&gt;Amazon SQS retry, DLQ, and batching guidance&lt;/li&gt;
&lt;li&gt;Amazon DynamoDB write/read patterns and idempotency design guidance&lt;/li&gt;
&lt;li&gt;Amazon CloudWatch Logs pricing, retention, and observability best practices&lt;/li&gt;
&lt;li&gt;AWS Well-Architected Framework (Cost Optimization and Reliability pillars)&lt;/li&gt;
&lt;li&gt;Serverless Lens and workload-specific architecture review guidance&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>aws</category>
      <category>architecture</category>
      <category>cloud</category>
    </item>
    <item>
      <title>My Study Guide for the Microsoft Certified SQL AI Developer Associate Beta Exam</title>
      <dc:creator>Renaldi</dc:creator>
      <pubDate>Fri, 10 Apr 2026 23:00:00 +0000</pubDate>
      <link>https://dev.to/rengond/my-study-guide-for-the-microsoft-certified-sql-ai-developer-associate-beta-exam-447o</link>
      <guid>https://dev.to/rengond/my-study-guide-for-the-microsoft-certified-sql-ai-developer-associate-beta-exam-447o</guid>
      <description>&lt;p&gt;I put this guide together for people who want something more useful than a recycled checklist. When a certification is still in beta, the most valuable preparation is not memorizing fragments. It is understanding how Microsoft is defining the role, what real skills sit underneath the blueprint, and how those skills connect to actual work. For this exam, that means designing and developing AI enabled database solutions across Microsoft SQL Server, Azure SQL, and SQL database in Microsoft Fabric, while also being comfortable with T-SQL, CI/CD, security, performance, and modern AI integration patterns.&lt;/p&gt;

&lt;p&gt;What makes this beta especially interesting to me is that it does not treat the database as a passive storage layer. The role itself is framed around building AI enabled database solutions. That is a very different mindset from traditional database development. It means the exam is really about whether you can treat the database as an active part of an intelligent application architecture rather than just the place where rows happen to live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this exam needs a different kind of preparation
&lt;/h2&gt;

&lt;p&gt;Beta exams are different from mature exams in a very practical way. The public prep ecosystem is usually thinner, the patterns are less familiar, and the most useful clue is often the study guide itself. That matters here because the official certification page says the practice assessment is not currently available while the exam is in beta, and beta exams are not scored immediately. In other words, this is not the kind of exam where you can rely on a polished prep routine and coast through it.&lt;/p&gt;

&lt;p&gt;My approach would be simple. Study this as a role, not as a quiz. If you approach it like a collection of disconnected SQL facts, you will miss what makes it valuable. If you approach it like a role centered on application design, database engineering, delivery discipline, and AI capability, the blueprint starts to make a lot more sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this guide is for
&lt;/h2&gt;

&lt;p&gt;This guide is for you if you already know basic T SQL and database development, but you want a more structured way to prepare for a beta that blends modern SQL engineering with AI features. It is especially useful if any of these feel true for you.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are comfortable with databases but less comfortable with AI features in SQL platforms&lt;/li&gt;
&lt;li&gt;You know SQL development but want a clearer CI CD and deployment mindset&lt;/li&gt;
&lt;li&gt;You are strong in Azure SQL or SQL Server but have not yet thought deeply about how AI changes database design choices&lt;/li&gt;
&lt;li&gt;You want a study plan that is grounded in the official exam scope without being dry and mechanical&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are already experienced in SQL development, application data design, and delivery automation, this exam should feel relevant. If your experience is mostly limited to writing ad hoc queries, you will likely need to spend more time on lifecycle thinking, deployment practices, and AI integration patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the exam is really testing
&lt;/h2&gt;

&lt;p&gt;The current study guide breaks the exam into three major skill areas.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Design and develop database solutions with a weight of 35 to 40 percent&lt;/li&gt;
&lt;li&gt;Secure, optimize, and deploy database solutions with a weight of 35 to 40 percent&lt;/li&gt;
&lt;li&gt;Implement AI capabilities in database solutions with a weight of 25 to 30 percent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That distribution tells you a lot. This is not an AI first exam that only lightly touches databases. It is still a database developer exam at its core. The AI section matters, but it sits on top of strong expectations around schema design, query logic, programmability, security, deployment, and performance engineering.&lt;/p&gt;

&lt;p&gt;That is why I would prepare for this in layers.&lt;/p&gt;

&lt;p&gt;First, make sure your database engineering fundamentals are genuinely strong.&lt;/p&gt;

&lt;p&gt;Second, make sure you can think about database delivery as code, not just as changes made manually in a tool.&lt;/p&gt;

&lt;p&gt;Third, add the AI layer on top so that you can reason clearly about vector search, embeddings, RAG style scenarios, and model integration without losing your grounding in database design.&lt;/p&gt;

&lt;h2&gt;
  
  
  My study philosophy for this beta
&lt;/h2&gt;

&lt;p&gt;My rule for this exam would be to study every objective from four angles.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What the feature does&lt;/li&gt;
&lt;li&gt;Why a team would choose it in a real application&lt;/li&gt;
&lt;li&gt;What tradeoffs it introduces&lt;/li&gt;
&lt;li&gt;What can go wrong in production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is where a lot of real understanding lives. Many candidates can explain what a feature is. Fewer can explain when it is a good choice, when it is a risky choice, or how it changes operational behavior after deployment. This exam feels much more valuable when you prepare for that deeper level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain one
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Design and develop database solutions
&lt;/h2&gt;

&lt;p&gt;This domain carries the heaviest weight and sets the foundation for everything else. The study guide includes database objects, specialized tables, JSON columns and indexes, constraints, sequences, stored procedures, functions, triggers, views, transactions, query logic, window functions, common table expressions, error handling, and support for semi structured data.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this domain really means
&lt;/h3&gt;

&lt;p&gt;This domain is about whether you can design a database that supports application behavior cleanly, predictably, and at scale. It is not just about knowing syntax. It is about choosing the right structures and patterns so the data model works for real workloads.&lt;/p&gt;

&lt;p&gt;The more I look at this blueprint, the more I think this section rewards engineers who can connect schema design with application behavior. A table definition is not just a table definition. It affects performance, maintainability, integrity, and how easy it is to add AI driven features later.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I would make sure I can do
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Design tables with sensible keys, data types, defaults, and constraints&lt;/li&gt;
&lt;li&gt;Explain when specialized table types such as temporal, graph, ledger, in memory, or external tables make sense&lt;/li&gt;
&lt;li&gt;Work confidently with JSON data and know how it affects design and querying&lt;/li&gt;
&lt;li&gt;Write solid stored procedures, functions, and views without relying on fragile shortcuts&lt;/li&gt;
&lt;li&gt;Use transactions intentionally and know when isolation and consistency matter most&lt;/li&gt;
&lt;li&gt;Read complex query logic without getting lost when joins, window functions, and common table expressions are involved&lt;/li&gt;
&lt;li&gt;Think of schema design as application design, not just as data storage&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Hands on tasks I would actually practice
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Create a small schema from scratch for a realistic application and justify each table and constraint&lt;/li&gt;
&lt;li&gt;Add JSON based attributes to one part of the design and compare the tradeoffs against full normalization&lt;/li&gt;
&lt;li&gt;Write queries that use window functions for ranking, running totals, and partitioned logic&lt;/li&gt;
&lt;li&gt;Build a stored procedure that includes error handling and transaction control&lt;/li&gt;
&lt;li&gt;Review a design and ask where future AI style features such as semantic search or hybrid structured and unstructured access might fit&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Traps I would avoid
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Treating schema design as a memorization exercise&lt;/li&gt;
&lt;li&gt;Ignoring JSON and semi structured data because it feels secondary&lt;/li&gt;
&lt;li&gt;Overfocusing on rare features before mastering the core objects and query patterns&lt;/li&gt;
&lt;li&gt;Studying stored procedures and functions only as syntax rather than as maintainable application building blocks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  My view on this domain
&lt;/h3&gt;

&lt;p&gt;This domain is exactly why I think the exam has substance. It still expects you to be a real database developer. That is a good thing. Without strong fundamentals here, the AI portions become superficial very quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain two
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Secure, optimize, and deploy database solutions
&lt;/h2&gt;

&lt;p&gt;The study guide includes authentication and authorization, roles and permissions, data protection, monitoring, performance tuning, indexing, query plan awareness, deployment using SQL database projects, Git based workflows, schema drift, and CI CD practices.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this domain really means
&lt;/h3&gt;

&lt;p&gt;This domain is about whether you can treat database work as part of a modern engineering workflow rather than a one off administrative activity. To me, this is one of the most important parts of the exam because it separates people who can build something from people who can maintain and ship something responsibly.&lt;/p&gt;

&lt;p&gt;The database is often where teams become inconsistent with engineering discipline. Application code gets source control, pipelines, reviews, and repeatable releases. Database changes too often get handled manually or as an afterthought. This domain pushes in the right direction by expecting proper delivery discipline.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I would make sure I can do
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Explain core authentication and authorization choices in Azure SQL and related SQL platforms&lt;/li&gt;
&lt;li&gt;Understand how permissions should be scoped and why least privilege matters&lt;/li&gt;
&lt;li&gt;Read performance problems through the lens of indexing, plan quality, and workload behavior&lt;/li&gt;
&lt;li&gt;Use SQL database projects as a source of truth for schema&lt;/li&gt;
&lt;li&gt;Understand how CI CD pipelines apply to database changes&lt;/li&gt;
&lt;li&gt;Explain schema drift and why it becomes dangerous when teams skip disciplined deployment practices&lt;/li&gt;
&lt;li&gt;Think about deployment safety and repeatability rather than just whether the change works once&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Hands on tasks I would actually practice
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Create a simple SQL database project and put it under source control&lt;/li&gt;
&lt;li&gt;Build and validate a DACPAC based deployment flow&lt;/li&gt;
&lt;li&gt;Walk through a GitHub Actions or Azure DevOps example for database deployment&lt;/li&gt;
&lt;li&gt;Compare a safe controlled deployment process against a manual direct change process&lt;/li&gt;
&lt;li&gt;Review indexing choices on a small schema and explain how they support or hurt workload patterns&lt;/li&gt;
&lt;li&gt;Practice reading execution plans at a conceptual level, especially for common tuning scenarios&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Traps I would avoid
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Thinking that database CI CD is too operational to be important for the exam&lt;/li&gt;
&lt;li&gt;Treating security as a list of terms instead of a design decision&lt;/li&gt;
&lt;li&gt;Memorizing individual performance tips without understanding why they help&lt;/li&gt;
&lt;li&gt;Ignoring schema drift because it sounds niche&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  My view on this domain
&lt;/h3&gt;

&lt;p&gt;I really like that this exam gives so much space to secure delivery and optimization. That makes it feel much more relevant to the actual role. Modern database development is not just about writing objects. It is about shipping them safely and keeping them fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain three
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Implement AI capabilities in database solutions
&lt;/h3&gt;

&lt;p&gt;This is the part that makes the exam stand out. The study guide includes AI integration patterns, embeddings, vector search, retrieval augmented generation style design, intelligent search, and building AI aware data solutions directly on Microsoft SQL platforms.&lt;/p&gt;

&lt;h4&gt;
  
  
  What this domain really means
&lt;/h4&gt;

&lt;p&gt;This domain asks whether you can connect database engineering with modern AI application patterns without losing rigor. That matters because there is a big difference between talking about AI in general and actually designing a database solution that supports intelligent retrieval, vector based matching, model integration, and reliable application behavior.&lt;/p&gt;

&lt;p&gt;What I find most interesting is that Microsoft is clearly positioning SQL platforms as active participants in AI architectures. This is not just a certification about wiring a model to an app. It is about understanding how the database itself can support embeddings, retrieval, structured filtering, hybrid search behavior, and data shaped for AI driven scenarios.&lt;/p&gt;

&lt;h4&gt;
  
  
  What I would make sure I can do
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Explain what embeddings are at a practical level and why they matter for semantic similarity scenarios&lt;/li&gt;
&lt;li&gt;Understand where vector search fits and when it is appropriate&lt;/li&gt;
&lt;li&gt;Compare keyword style retrieval with semantic or hybrid approaches&lt;/li&gt;
&lt;li&gt;Understand how structured data and semi structured data can work together in intelligent applications&lt;/li&gt;
&lt;li&gt;Think clearly about where SQL based AI patterns are a good fit and where a dedicated search or AI service may still be the better choice&lt;/li&gt;
&lt;li&gt;Understand the role of T SQL in orchestrating AI aware retrieval patterns&lt;/li&gt;
&lt;li&gt;Be comfortable talking about RAG style design in database terms rather than only in model terms&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Hands on tasks I would actually practice
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Walk through an Azure SQL vector search sample and explain each step in plain language&lt;/li&gt;
&lt;li&gt;Store and query JSON based data alongside relational data and think about how that supports intelligent application scenarios&lt;/li&gt;
&lt;li&gt;Compare a full text or keyword style search approach against a vector similarity approach&lt;/li&gt;
&lt;li&gt;Read documentation on intelligent applications in Azure SQL and summarize when SQL centric AI design is strong and when it becomes limiting&lt;/li&gt;
&lt;li&gt;Sketch a simple RAG style flow where the database plays an intentional retrieval role&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Traps I would avoid
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Studying AI buzzwords without grounding them in actual SQL platform behavior&lt;/li&gt;
&lt;li&gt;Assuming vector search replaces all other search patterns&lt;/li&gt;
&lt;li&gt;Ignoring the importance of structured filters in intelligent applications&lt;/li&gt;
&lt;li&gt;Treating AI features as magic instead of as design choices with cost, performance, and complexity tradeoffs&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  My view on this domain
&lt;/h4&gt;

&lt;p&gt;This domain is the reason I think the certification is genuinely interesting. It reflects a shift that is happening in the industry right now. Databases are no longer just passive systems underneath AI applications. They are becoming active parts of how those systems retrieve, filter, ground, and serve information.&lt;/p&gt;

&lt;h2&gt;
  
  
  The study resources I would actually use
&lt;/h2&gt;

&lt;p&gt;I would start with the official Microsoft resources and then move into hands on documentation that maps directly to the objectives. Here is the stack I would use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Start here
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/credentials/certifications/developing-ai-enabled-database-solutions/" rel="noopener noreferrer"&gt;Microsoft Certified SQL AI Developer Associate beta&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/credentials/certifications/resources/study-guides/dp-800" rel="noopener noreferrer"&gt;Study guide for Exam DP 800&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/training/courses/dp-800t00" rel="noopener noreferrer"&gt;Course DP 800T00 Develop AI enabled database solutions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Core database development and delivery
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/training/paths/develop-data-driven-app-sql-db/" rel="noopener noreferrer"&gt;Develop data driven applications by using Azure SQL Database&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/training/paths/secure-optimize-deploy-database-solutions/" rel="noopener noreferrer"&gt;Secure optimize and deploy database solutions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/training/modules/develop-azure-sql-database/" rel="noopener noreferrer"&gt;Develop for Azure SQL Database&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/training/modules/implement-cicd-sql-database-projects/" rel="noopener noreferrer"&gt;Implement CI-CD by using SQL Database Projects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/sql/tools/sql-database-projects/sql-database-projects?view=sql-server-ver17" rel="noopener noreferrer"&gt;What are SQL Database Projects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/sql/tools/sql-database-projects/tutorials/create-deploy-sql-project?view=sql-server-ver17" rel="noopener noreferrer"&gt;Tutorial create and deploy a SQL project&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/sql/tools/sql-database-projects/sql-projects-automation?view=sql-server-ver17" rel="noopener noreferrer"&gt;SQL projects automation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  T SQL and query depth
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/training/courses/dp-080t00" rel="noopener noreferrer"&gt;Query and modify data with Transact SQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/sql/relational-databases/query-processing-architecture-guide?view=sql-server-ver17" rel="noopener noreferrer"&gt;Query processing architecture guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  JSON and semi structured data
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/azure-sql/database/json-features?view=azuresql" rel="noopener noreferrer"&gt;Work with JSON data in Azure SQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/sql/relational-databases/json/json-data-sql-server?view=sql-server-ver17" rel="noopener noreferrer"&gt;Work with JSON data in SQL Server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/sql/relational-databases/json/index-json-data?view=sql-server-ver17" rel="noopener noreferrer"&gt;Index JSON data&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  AI features in SQL platforms
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/training/paths/implement-ai-capabilities-database-solutions/" rel="noopener noreferrer"&gt;Implement AI capabilities in database solutions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/azure-sql/database/ai-artificial-intelligence-intelligent-applications?view=azuresql" rel="noopener noreferrer"&gt;Intelligent applications and AI in Azure SQL Database&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/sql/sql-server/ai/artificial-intelligence-intelligent-applications?view=sql-server-ver17" rel="noopener noreferrer"&gt;Intelligent applications and AI in SQL Server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/sql/sql-server/ai/vectors?view=sql-server-ver17" rel="noopener noreferrer"&gt;Vector search and vector index&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/data-types/vector-data-type?view=sql-server-ver17" rel="noopener noreferrer"&gt;Vector data type&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/sql/t-sql/functions/vector-functions-transact-sql?view=sql-server-ver17" rel="noopener noreferrer"&gt;Vector functions in Transact SQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/samples/azure-samples/azure-sql-db-openai/azure-sql-db-openai/" rel="noopener noreferrer"&gt;Vector similarity search with Azure SQL and Azure OpenAI sample&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Platform context that helps
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/fabric/database/sql/overview" rel="noopener noreferrer"&gt;SQL database in Microsoft Fabric overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/azure-sql/database/features-comparison?view=azuresql" rel="noopener noreferrer"&gt;Compare Azure SQL Database and Azure SQL Managed Instance features&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/azure-sql/database/failover-group-sql-db?view=azuresql" rel="noopener noreferrer"&gt;Failover groups overview and best practices&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A four week study plan that I think actually works
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Week one
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Build the map
&lt;/h3&gt;

&lt;p&gt;Read the official study guide closely and create your own notes under the three domains. Mark each topic as strong, medium, or weak. Do not study everything with equal intensity. This exam has a very clear weighting, so your time should follow it.&lt;/p&gt;

&lt;p&gt;Your goal in week one is not mastery. Your goal is clarity.&lt;/p&gt;

&lt;p&gt;You should finish the week knowing exactly where your risk areas are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Week two
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Go deep on database design and programmability
&lt;/h3&gt;

&lt;p&gt;Spend the week on schema design, specialized tables, JSON, views, procedures, functions, transactions, and query logic. Build one small realistic schema and keep improving it as you learn.&lt;/p&gt;

&lt;p&gt;Ask yourself practical questions such as these.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is this design flexible enough for application change&lt;/li&gt;
&lt;li&gt;Where would semi structured data belong here&lt;/li&gt;
&lt;li&gt;Which constraints actually protect integrity&lt;/li&gt;
&lt;li&gt;What query patterns will this application need most often&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Week three
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Go deep on delivery security and performance
&lt;/h3&gt;

&lt;p&gt;Focus on SQL database projects, CI CD, source control, schema drift, permissions, authentication, indexing, and plan awareness. This is the week where you move from being someone who can write database code to someone who can ship it well.&lt;/p&gt;

&lt;p&gt;If you skip this part, you risk underestimating a huge portion of the exam.&lt;/p&gt;

&lt;h2&gt;
  
  
  Week four
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Add the AI layer and consolidate
&lt;/h3&gt;

&lt;p&gt;Now focus on embeddings, vector search, intelligent application patterns, RAG style thinking, and hybrid retrieval logic. Connect these back to the database fundamentals you already studied.&lt;/p&gt;

&lt;p&gt;The key question for this final week is not simply how an AI feature works. It is why that feature belongs in this database design and what tradeoffs come with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical weekend sprint if you are short on time
&lt;/h2&gt;

&lt;p&gt;If all you have is a weekend, I would still avoid random study.&lt;/p&gt;

&lt;h3&gt;
  
  
  Day one
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Read the certification page and study guide end to end&lt;/li&gt;
&lt;li&gt;Focus first on domain one and domain two&lt;/li&gt;
&lt;li&gt;Review schema design, SQL projects, CI CD, permissions, indexing, and deployment flow&lt;/li&gt;
&lt;li&gt;Build one page notes for design, one page notes for deployment, and one page notes for tuning&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Day two
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Focus on domain three&lt;/li&gt;
&lt;li&gt;Review intelligent applications, embeddings, vector search, and SQL centric AI patterns&lt;/li&gt;
&lt;li&gt;Walk through at least one official sample and one or two docs pages in detail&lt;/li&gt;
&lt;li&gt;End by explaining each domain out loud in your own words&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last step sounds simple, but it is one of the best checks of whether you understand the role or are just recognizing keywords.&lt;/p&gt;

&lt;h2&gt;
  
  
  The notes format I would use
&lt;/h2&gt;

&lt;p&gt;I would keep notes in a format that forces decision making.&lt;/p&gt;

&lt;h3&gt;
  
  
  Feature or concept
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;What it is&lt;/li&gt;
&lt;li&gt;Why it exists&lt;/li&gt;
&lt;li&gt;When I would choose it&lt;/li&gt;
&lt;li&gt;What tradeoffs it creates&lt;/li&gt;
&lt;li&gt;What could go wrong in production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That format is much more useful than copying definitions into a document you never read again.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would not waste time on
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Blind memorization of isolated syntax without application context&lt;/li&gt;
&lt;li&gt;Treating AI features as separate from core database design&lt;/li&gt;
&lt;li&gt;Ignoring CI CD because it feels less technical than query writing&lt;/li&gt;
&lt;li&gt;Overfocusing on rare edge features before mastering the heavily weighted fundamentals&lt;/li&gt;
&lt;li&gt;Reading only summaries without opening the real documentation and samples&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final 48 hours
&lt;/h2&gt;

&lt;p&gt;In the last two days, I would stop collecting new material and start tightening judgment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Review the three domains and their weightings again&lt;/li&gt;
&lt;li&gt;Revisit schema design, query logic, deployment, permissions, and performance tuning&lt;/li&gt;
&lt;li&gt;Revisit embeddings, vector search, and intelligent application patterns&lt;/li&gt;
&lt;li&gt;Review where SQL is the right home for AI style retrieval and where another service may be stronger&lt;/li&gt;
&lt;li&gt;Sleep properly and keep your thinking sharp&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the practice assessment is not currently available in beta, your ability to explain decisions clearly becomes even more important. This is one of those exams where maturity of reasoning is likely to matter more than pattern memorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  My closing take
&lt;/h2&gt;

&lt;p&gt;If I had to summarize this beta in one sentence, I would say it is testing whether you can think like a modern SQL application engineer whose database is part of the intelligence layer, not just the persistence layer.&lt;/p&gt;

&lt;p&gt;That is why I think this certification is worth paying attention to. It still respects strong database fundamentals, but it also points toward where the role is heading. For me, that is what makes a beta worth studying. It is not just an exam. It is an early signal.&lt;/p&gt;

&lt;p&gt;If you are preparing for this one, I would study it like a role that sits at the intersection of database engineering, delivery discipline, and AI aware application design. That is the mindset this guide is built to support.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>azure</category>
      <category>sql</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a Document Processing Pipeline with S3, Textract, Step Functions and EventBridge</title>
      <dc:creator>Renaldi</dc:creator>
      <pubDate>Sun, 05 Apr 2026 23:00:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/building-a-document-processing-pipeline-with-s3-textract-step-functions-and-eventbridge-3m3l</link>
      <guid>https://dev.to/aws-builders/building-a-document-processing-pipeline-with-s3-textract-step-functions-and-eventbridge-3m3l</guid>
      <description>&lt;p&gt;This is one of my favorite AWS patterns to demo because it is both visually compelling and production-relevant. It shows event-driven architecture, orchestration, asynchronous AI/ML service integration, scale-out processing, human-in-the-loop review, and operational discipline in one workflow.&lt;/p&gt;

&lt;p&gt;In this post, I will walk through an end-to-end implementation of a document processing pipeline built with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon S3&lt;/strong&gt; for document ingress and result storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Textract&lt;/strong&gt; for OCR and structured extraction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Step Functions&lt;/strong&gt; for orchestration (including &lt;strong&gt;Distributed Map&lt;/strong&gt; for batch scale)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon EventBridge&lt;/strong&gt; for event routing and downstream integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I will also cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Async Textract orchestration&lt;/li&gt;
&lt;li&gt;Batch scaling with Distributed Map&lt;/li&gt;
&lt;li&gt;Result storage and audit trail&lt;/li&gt;
&lt;li&gt;Human review step&lt;/li&gt;
&lt;li&gt;Cost and throughput tuning&lt;/li&gt;
&lt;li&gt;Architecture and code walkthrough&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why this pattern is so effective
&lt;/h2&gt;

&lt;p&gt;In real teams, document processing is rarely just “OCR a file and store JSON.” We usually need to handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multi-page PDFs and asynchronous processing&lt;/li&gt;
&lt;li&gt;Bursty uploads (for example, end-of-day batch drops)&lt;/li&gt;
&lt;li&gt;Traceability for compliance and audits&lt;/li&gt;
&lt;li&gt;Human review for low-confidence or ambiguous fields&lt;/li&gt;
&lt;li&gt;Clean integration points for downstream systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This architecture solves those concerns in a way that is easy to demonstrate and scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we are building
&lt;/h2&gt;

&lt;p&gt;I am designing the pipeline around two operating modes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Single-document event-driven processing&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A file lands in S3&lt;/li&gt;
&lt;li&gt;EventBridge triggers the workflow&lt;/li&gt;
&lt;li&gt;The workflow runs end-to-end for that document&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Batch processing mode&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A manifest (JSON/CSV/list of document keys) is provided&lt;/li&gt;
&lt;li&gt;Step Functions uses &lt;strong&gt;Distributed Map&lt;/strong&gt; to fan out child workflows&lt;/li&gt;
&lt;li&gt;Each child workflow processes one document independently&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This keeps the core processing logic consistent while letting me scale from demos to production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;At a high level, the flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A document is uploaded to an S3 input bucket&lt;/li&gt;
&lt;li&gt;S3 emits an event to EventBridge&lt;/li&gt;
&lt;li&gt;EventBridge starts a Step Functions &lt;strong&gt;parent workflow&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The parent workflow prepares a manifest (single item or batch list)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Distributed Map&lt;/strong&gt; launches child workflows for each document&lt;/li&gt;
&lt;li&gt;Each child workflow:

&lt;ul&gt;
&lt;li&gt;validates input&lt;/li&gt;
&lt;li&gt;starts async Textract&lt;/li&gt;
&lt;li&gt;waits/polls for completion&lt;/li&gt;
&lt;li&gt;retrieves and normalizes results&lt;/li&gt;
&lt;li&gt;stores raw + normalized outputs&lt;/li&gt;
&lt;li&gt;writes audit records&lt;/li&gt;
&lt;li&gt;routes low-confidence cases to human review&lt;/li&gt;
&lt;li&gt;emits a processed event to EventBridge&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Downstream systems consume the processed event&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz3glhl3ub0p6gaw3cu3j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz3glhl3ub0p6gaw3cu3j.png" alt=" " width="800" height="1190"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  End-to-End Walkthrough
&lt;/h2&gt;

&lt;h2&gt;
  
  
  1) Document ingress with S3 + EventBridge
&lt;/h2&gt;

&lt;p&gt;I use S3 as the system of record for inbound documents. The upload path usually looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;s3://my-docs-incoming/raw/YYYY/MM/DD/&amp;lt;tenant&amp;gt;/&amp;lt;document&amp;gt;.pdf&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I prefer a structured key naming convention because it helps with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lifecycle policies&lt;/li&gt;
&lt;li&gt;tenant scoping&lt;/li&gt;
&lt;li&gt;troubleshooting&lt;/li&gt;
&lt;li&gt;cost attribution&lt;/li&gt;
&lt;li&gt;replay operations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;S3 can publish object events to EventBridge, and I use an EventBridge rule to filter only the prefixes and file types that should trigger processing (for example, PDFs in &lt;code&gt;raw/&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Example EventBridge rule pattern (S3 object created)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"aws.s3"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail-type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Object Created"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bucket"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"my-docs-incoming"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"raw/"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, EventBridge starts the Step Functions parent state machine.&lt;/p&gt;




&lt;h2&gt;
  
  
  2) Parent orchestration with Step Functions
&lt;/h2&gt;

&lt;p&gt;I use a &lt;strong&gt;Standard workflow&lt;/strong&gt; for the parent because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;document jobs can run for a while&lt;/li&gt;
&lt;li&gt;I may have retries and waits&lt;/li&gt;
&lt;li&gt;I may pause for human review (callback pattern)&lt;/li&gt;
&lt;li&gt;I want richer execution history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The parent workflow does a few things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Parses the incoming event (or batch request)&lt;/li&gt;
&lt;li&gt;Creates a manifest for processing (even if only one document)&lt;/li&gt;
&lt;li&gt;Sets concurrency controls (important for Textract/Lambda quotas)&lt;/li&gt;
&lt;li&gt;Launches a &lt;strong&gt;Distributed Map&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why Distributed Map matters
&lt;/h3&gt;

&lt;p&gt;For real batches, a normal Map state can become limiting because of concurrency and history size. Distributed Map gives me child workflow executions, better scaling, and cleaner observability for each document.&lt;/p&gt;




&lt;h2&gt;
  
  
  3) Child workflow per document (core pipeline)
&lt;/h2&gt;

&lt;p&gt;Each child workflow processes a single document from start to finish.&lt;/p&gt;

&lt;h3&gt;
  
  
  Core steps in the child workflow
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Validate input&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;file type&lt;/li&gt;
&lt;li&gt;bucket/key exists&lt;/li&gt;
&lt;li&gt;metadata (tenant, doc type, correlation ID)&lt;/li&gt;
&lt;li&gt;idempotency key check&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Start Textract asynchronously&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;call &lt;code&gt;StartDocumentAnalysis&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;capture &lt;code&gt;JobId&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;store correlation in audit trail&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Wait for completion&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;polling loop (simple and explicit for Step Functions)&lt;/li&gt;
&lt;li&gt;or callback/SNS-based completion (production optimization)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Retrieve paginated results&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;call &lt;code&gt;GetDocumentAnalysis&lt;/code&gt; until &lt;code&gt;NextToken&lt;/code&gt; is exhausted&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Normalize + enrich&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;convert Textract blocks into domain JSON&lt;/li&gt;
&lt;li&gt;score confidence thresholds&lt;/li&gt;
&lt;li&gt;apply business rules&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Store outputs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;raw output&lt;/li&gt;
&lt;li&gt;normalized output&lt;/li&gt;
&lt;li&gt;processing summary&lt;/li&gt;
&lt;li&gt;audit entries&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Human review (if needed)&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pause with task token callback&lt;/li&gt;
&lt;li&gt;reviewer approves/edits/rejects&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Emit completion event&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;publish a custom EventBridge event&lt;/li&gt;
&lt;li&gt;downstream systems subscribe independently&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  4) Async Textract orchestration (practical implementation)
&lt;/h2&gt;

&lt;p&gt;Textract async processing is the correct choice for multi-page documents and larger workloads. The key point is that I do &lt;strong&gt;not&lt;/strong&gt; try to synchronously block a Lambda while waiting for Textract.&lt;/p&gt;

&lt;p&gt;Instead, I let Step Functions own the waiting and retry logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two patterns I use in practice
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Pattern A (implemented in this post): Step Functions polling loop
&lt;/h4&gt;

&lt;p&gt;This is the simplest pattern to explain and demo.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;StartDocumentAnalysis&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Wait&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GetDocumentAnalysis&lt;/code&gt; (status check)&lt;/li&gt;
&lt;li&gt;loop until &lt;code&gt;SUCCEEDED&lt;/code&gt; / &lt;code&gt;FAILED&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pros:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Easy to understand&lt;/li&gt;
&lt;li&gt;No extra callback plumbing&lt;/li&gt;
&lt;li&gt;Great for demos and many production cases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More state transitions&lt;/li&gt;
&lt;li&gt;Not the most efficient if jobs are very long&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Pattern B (production optimization): SNS/SQS + callback
&lt;/h4&gt;

&lt;p&gt;Textract async operations notify completion via SNS. I can correlate &lt;code&gt;JobId&lt;/code&gt;, then call &lt;code&gt;SendTaskSuccess&lt;/code&gt; to resume the Step Functions execution.&lt;/p&gt;

&lt;p&gt;Pros:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fewer polling transitions&lt;/li&gt;
&lt;li&gt;More event-driven&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More moving parts (SNS/SQS/Lambda/token correlation)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this article, I am implementing &lt;strong&gt;Pattern A&lt;/strong&gt; for clarity, and I will still preserve auditability and scalability.&lt;/p&gt;




&lt;h2&gt;
  
  
  5) Result storage and audit trail (what I store and why)
&lt;/h2&gt;

&lt;p&gt;This is where many demos stop too early. I do not want a pipeline that only prints extraction results to logs.&lt;/p&gt;

&lt;p&gt;I store &lt;strong&gt;four classes of artifacts&lt;/strong&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  A. Raw input (immutable)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Original document in S3 (&lt;code&gt;raw/&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  B. Raw Textract result snapshot
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Either Textract output JSON (if using &lt;code&gt;OutputConfig&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Or consolidated raw blocks written by my retrieval Lambda&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  C. Normalized business result
&lt;/h3&gt;

&lt;p&gt;A clean JSON model that downstream systems can actually use, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"documentId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INV-2026-000123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"documentType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invoice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"vendorName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Pty Ltd"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"invoiceDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-25"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"totalAmount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1285.40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AUD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"invoice_number"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INV-2026-000123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;98.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"textract"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"review"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  D. Audit trail (DynamoDB + events)
&lt;/h3&gt;

&lt;p&gt;I keep an audit table with entries like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;correlation ID&lt;/li&gt;
&lt;li&gt;S3 bucket/key/version&lt;/li&gt;
&lt;li&gt;execution ARN&lt;/li&gt;
&lt;li&gt;Textract JobId&lt;/li&gt;
&lt;li&gt;timestamps (started/completed)&lt;/li&gt;
&lt;li&gt;status transitions&lt;/li&gt;
&lt;li&gt;retry count&lt;/li&gt;
&lt;li&gt;reviewer ID + decision (if reviewed)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes troubleshooting and compliance conversations much easier.&lt;/p&gt;




&lt;h2&gt;
  
  
  6) Human review step (callback pattern)
&lt;/h2&gt;

&lt;p&gt;I like to add a human review step because it turns the demo into a real workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  When I trigger human review
&lt;/h3&gt;

&lt;p&gt;I send documents for human review if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;confidence is below threshold for required fields&lt;/li&gt;
&lt;li&gt;required fields are missing&lt;/li&gt;
&lt;li&gt;document type classification is ambiguous&lt;/li&gt;
&lt;li&gt;business validations fail (for example, total does not match line items)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How the callback works
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Step Functions reaches &lt;code&gt;HumanReviewRequired&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;It invokes a Lambda using &lt;code&gt;waitForTaskToken&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;That Lambda stores a review task (with the task token) in DynamoDB and optionally emits a notification event&lt;/li&gt;
&lt;li&gt;A reviewer UI or ops tool loads the pending task&lt;/li&gt;
&lt;li&gt;Reviewer approves/edits/rejects&lt;/li&gt;
&lt;li&gt;API Gateway + Lambda calls:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SendTaskSuccess&lt;/code&gt; (approve / approved edits)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SendTaskFailure&lt;/code&gt; (reject / unrecoverable issue)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This lets me keep the workflow paused cleanly without polling the UI.&lt;/p&gt;




&lt;h2&gt;
  
  
  7) EventBridge for decoupled downstream integration
&lt;/h2&gt;

&lt;p&gt;I use EventBridge in two places:&lt;/p&gt;

&lt;h3&gt;
  
  
  Ingress routing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;S3 object-created events trigger the parent workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Egress publishing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The child workflow publishes custom events like:

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;document.processed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;document.review.required&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;document.failed&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;This is a huge win because downstream systems can subscribe independently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;search indexing&lt;/li&gt;
&lt;li&gt;analytics pipelines&lt;/li&gt;
&lt;li&gt;case management&lt;/li&gt;
&lt;li&gt;notifications&lt;/li&gt;
&lt;li&gt;data quality dashboards&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No tight coupling to the core document processing workflow.&lt;/p&gt;




&lt;h1&gt;
  
  
  Implementation Discussion
&lt;/h1&gt;

&lt;p&gt;Below is a concrete implementation outline with code.&lt;/p&gt;




&lt;h2&gt;
  
  
  State machine design (parent + child)
&lt;/h2&gt;

&lt;p&gt;I prefer splitting the logic into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parent workflow&lt;/strong&gt;: batching and fan-out&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Child workflow&lt;/strong&gt;: per-document processing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps each workflow easier to reason about and test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parent workflow (ASL fragment with Distributed Map)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Comment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Parent workflow for document batch orchestration"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"StartAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BuildManifest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"States"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"BuildManifest"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::lambda:invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FunctionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${BuildManifestFnArn}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Payload.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"OutputPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.Payload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ProcessDocumentsDistributed"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ProcessDocumentsDistributed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Map"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ProcessDocuments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ItemsPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.documents"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"MaxConcurrencyPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.maxConcurrency"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ItemProcessor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ProcessorConfig"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DISTRIBUTED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ExecutionType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"STANDARD"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StartAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"StartChildExecution"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"States"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"StartChildExecution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::states:startExecution.sync:2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"StateMachineArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${ChildStateMachineArn}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"Input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"document.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$$.Map.Item.Value"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"batchContext.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.batchContext"&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"End"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BatchSummary"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"BatchSummary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::lambda:invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FunctionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${BatchSummaryFnArn}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Payload.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"OutputPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.Payload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"End"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Notes on this design
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;MaxConcurrencyPath&lt;/code&gt; lets me tune concurrency dynamically per batch.&lt;/li&gt;
&lt;li&gt;I can set lower concurrency for production if Textract quotas are tighter than my desired fan-out.&lt;/li&gt;
&lt;li&gt;Child executions give me better isolation and clearer failure analysis.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Child workflow (ASL fragment for async Textract + human review + EventBridge)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Comment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Child workflow for processing one document"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"StartAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ValidateDocument"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"States"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ValidateDocument"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::lambda:invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FunctionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${ValidateDocumentFnArn}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Payload.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"OutputPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.Payload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"StartTextract"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"StartTextract"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::aws-sdk:textract:startDocumentAnalysis"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"DocumentLocation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"S3Object"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Bucket.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.document.bucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Name.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.document.key"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FeatureTypes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"FORMS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TABLES"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ClientRequestToken.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.idempotencyKey"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"JobTag.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.jobTag"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ResultPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.textractStart"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WaitBeforeStatusCheck"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Retry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ErrorEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"Textract.ThrottlingException"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"Textract.ProvisionedThroughputExceededException"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"States.TaskFailed"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"IntervalSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"BackoffRate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"MaxAttempts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"WaitBeforeStatusCheck"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Wait"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Seconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CheckTextractStatus"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CheckTextractStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::aws-sdk:textract:getDocumentAnalysis"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"JobId.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.textractStart.JobId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"MaxResults"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ResultPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.textractStatus"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TextractComplete?"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"TextractComplete?"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Choice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Choices"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Variable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.textractStatus.JobStatus"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SUCCEEDED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CollectTextractPages"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Variable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.textractStatus.JobStatus"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FAILED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MarkFailed"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Variable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.textractStatus.JobStatus"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PARTIAL_SUCCESS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CollectTextractPages"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"WaitBeforeStatusCheck"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CollectTextractPages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::lambda:invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FunctionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${CollectTextractPagesFnArn}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"jobId.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.textractStart.JobId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"document.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.document"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"executionArn.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$$.Execution.Id"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"OutputPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.Payload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NormalizeAndScore"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"NormalizeAndScore"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::lambda:invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FunctionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${NormalizeAndScoreFnArn}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Payload.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"OutputPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.Payload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NeedsHumanReview?"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"NeedsHumanReview?"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Choice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Choices"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Variable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.review.required"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"BooleanEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CreateHumanReviewTask"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PublishProcessedEvent"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"CreateHumanReviewTask"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::lambda:invoke.waitForTaskToken"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FunctionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${CreateHumanReviewTaskFnArn}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"taskToken.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$$.Task.Token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"document.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.document"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"result.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"executionArn.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$$.Execution.Id"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"TimeoutSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;604800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ResultPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.humanReviewDecision"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ApplyHumanReviewDecision"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ApplyHumanReviewDecision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::lambda:invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FunctionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${ApplyHumanReviewDecisionFnArn}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"pipelineResult.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"reviewDecision.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.humanReviewDecision.Payload"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"OutputPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.Payload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PublishProcessedEvent"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PublishProcessedEvent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::events:putEvents"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Entries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.example.documents"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"DetailType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"document.processed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"EventBusName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${EventBusName}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"documentId.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.document.id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"bucket.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.document.bucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"key.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.document.key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PROCESSED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"review.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.review"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"output.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.output"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FinalizeAudit"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"FinalizeAudit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::lambda:invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FunctionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${FinalizeAuditFnArn}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Payload.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"OutputPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.Payload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"End"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"MarkFailed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::lambda:invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FunctionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${MarkFailedFnArn}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Payload.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"OutputPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.Payload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"End"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Lambda: Collect paginated Textract results and store raw output (Python)
&lt;/h2&gt;

&lt;p&gt;This Lambda consolidates paginated &lt;code&gt;GetDocumentAnalysis&lt;/code&gt; responses, writes a raw artifact to S3, and returns a pointer for downstream normalization.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;textract&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;textract&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;RAW_RESULTS_BUCKET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RAW_RESULTS_BUCKET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;RAW_RESULTS_PREFIX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RAW_RESULTS_PREFIX&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;textract-raw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;job_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jobId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;document&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;execution_arn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;executionArn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;blocks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;next_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;first_page_metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;final_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;warnings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;kwargs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JobId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MaxResults&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;next_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NextToken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;next_token&lt;/span&gt;

        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;textract&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_document_analysis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;final_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JobStatus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;final_status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;first_page_metadata&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;first_page_metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DocumentMetadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DocumentMetadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AnalyzeDocumentModelVersion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AnalyzeDocumentModelVersion&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Blocks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]))&lt;/span&gt;
        &lt;span class="n"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Warnings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]))&lt;/span&gt;

        &lt;span class="n"&gt;next_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NextToken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;next_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;raw_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;RAW_RESULTS_PREFIX&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jobId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;executionArn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;execution_arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;collectedAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jobStatus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;final_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;first_page_metadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warnings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;RAW_RESULTS_BUCKET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;raw_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;ContentType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;textract&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jobId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;job_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jobStatus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;final_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rawResultsS3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RAW_RESULTS_BUCKET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;raw_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blockCount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warningsCount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;warnings&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Lambda: Normalize and score extracted data (Python)
&lt;/h2&gt;

&lt;p&gt;This is where I transform Textract’s generic block model into a domain result that downstream systems can consume.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;NORMALIZED_BUCKET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NORMALIZED_BUCKET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;NORMALIZED_PREFIX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NORMALIZED_PREFIX&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;normalized&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_load_json_from_s3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_extract_lines_and_words&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Minimal example; in production I usually build maps by block Id and relationships.
&lt;/span&gt;    &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BlockType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LINE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_simple_invoice_heuristics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Demo heuristics only. Replace with rule engine / ML classifier as needed.
&lt;/span&gt;    &lt;span class="n"&gt;text_blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;avg_conf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documentType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fields&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text_blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documentType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# Example field extraction heuristic placeholder
&lt;/span&gt;    &lt;span class="c1"&gt;# In production, I'd parse key-value pairs and tables from FORM/TABLE blocks
&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fields&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;avg_line_confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;avg_conf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;avg_conf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;textract&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;# Review policy example
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;avg_conf&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Average OCR confidence below threshold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;document&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;textract_ptr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;textract&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rawResultsS3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_load_json_from_s3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;textract_ptr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;textract_ptr&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;blocks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_extract_lines_and_words&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;derived&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_simple_invoice_heuristics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;normalized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;textract&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jobId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jobId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jobStatus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jobStatus&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warnings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warnings&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blockCount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classification&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documentType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;derived&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documentType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fields&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;derived&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fields&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;derived&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rawResultsS3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;textract_ptr&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;out_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;NORMALIZED_PREFIX&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tenant&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;NORMALIZED_BUCKET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;out_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;ContentType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;normalizedS3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NORMALIZED_BUCKET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;out_key&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;normalized&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Lambda: Create human review task (callback token storage) (Python)
&lt;/h2&gt;

&lt;p&gt;This Lambda stores the task token so a reviewer can resume the workflow later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;ddb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dynamodb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;events&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;REVIEW_TABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REVIEW_TABLE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;EVENT_BUS_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EVENT_BUS_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;task_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;taskToken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;document&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;execution_arn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;executionArn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;review_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;#&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviewId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;review_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PENDING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;createdAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documentId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;executionArn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;execution_arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;taskToken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resultSummary&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documentType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classification&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documentType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;normalizedOutput&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;normalizedS3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;REVIEW_TABLE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Entries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;com.example.documents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DetailType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;document.review.required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EventBusName&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EVENT_BUS_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviewId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;review_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;documentId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# For waitForTaskToken, Lambda can return immediately after storing task metadata.
&lt;/span&gt;    &lt;span class="c1"&gt;# The workflow stays paused until SendTaskSuccess/SendTaskFailure is called.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviewId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;review_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PENDING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Lambda: Reviewer callback endpoint (API Gateway -&amp;gt; Lambda -&amp;gt; Step Functions) (Python)
&lt;/h2&gt;

&lt;p&gt;This Lambda receives the human decision and resumes the waiting execution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;botocore.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ClientError&lt;/span&gt;

&lt;span class="n"&gt;ddb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dynamodb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sfn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stepfunctions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;REVIEW_TABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REVIEW_TABLE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;review_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviewId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# approve | reject
&lt;/span&gt;        &lt;span class="n"&gt;reviewer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviewer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;corrected_fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correctedFields&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;

        &lt;span class="n"&gt;item_resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REVIEW_TABLE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviewId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;review_id&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item_resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Review task not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PENDING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Review task already &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="n"&gt;task_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;taskToken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_task_success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;taskToken&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;task_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviewId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;review_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;decision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APPROVED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviewer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reviewer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviewedAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;correctedFields&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;corrected_fields&lt;/span&gt;
                &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;new_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APPROVED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_task_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;taskToken&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;task_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HumanReviewRejected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;cause&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviewId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;review_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviewer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reviewer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviewedAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;
                &lt;span class="p"&gt;})&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;new_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REJECTED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="n"&gt;REVIEW_TABLE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reviewId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;review_id&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;UpdateExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET #s = :s, reviewer = :r, reviewedAt = :t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ExpressionAttributeNames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;ExpressionAttributeValues&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reviewer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:t&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Review decision recorded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new_status&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;KeyError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Missing field: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ClientError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unexpected error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  CDK example (Python) for S3 -&amp;gt; EventBridge -&amp;gt; Step Functions start
&lt;/h2&gt;

&lt;p&gt;This is a simplified snippet to show the core wiring. In a production stack, I would break this into constructs (ingress, orchestration, review, storage, observability).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_cdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aws_s3&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aws_events&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aws_events_targets&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aws_stepfunctions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aws_iam&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;constructs&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Construct&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DocumentPipelineIngressStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Construct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;construct_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;construct_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;input_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;InputBucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;event_bridge_enabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;parent_state_machine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StateMachine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_state_machine_arn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ParentStateMachine&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;arn:aws:states:ap-southeast-2:123456789012:stateMachine:doc-parent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;rule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S3ObjectCreatedRule&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;event_pattern&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EventPattern&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws.s3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;detail_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Object Created&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;input_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bucket_name&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prefix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Start execution on matching events
&lt;/span&gt;        &lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SfnStateMachine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parent_state_machine&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;In a real deployment, I also add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;explicit execution input transformation&lt;/li&gt;
&lt;li&gt;IAM least privilege policies&lt;/li&gt;
&lt;li&gt;DLQs / retry policies on targets where appropriate&lt;/li&gt;
&lt;li&gt;environment-based prefix filtering (dev/test/prod)&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Data model and audit design (recommended)
&lt;/h2&gt;

&lt;p&gt;I strongly recommend treating auditability as a first-class feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  DynamoDB audit table (example keys)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;PK&lt;/strong&gt;: &lt;code&gt;DOC#&amp;lt;documentId&amp;gt;&lt;/code&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;SK&lt;/strong&gt;: &lt;code&gt;EVENT#&amp;lt;timestamp&amp;gt;#&amp;lt;eventType&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Example audit events
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;INGESTED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WORKFLOW_STARTED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TEXTRACT_STARTED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TEXTRACT_SUCCEEDED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NORMALIZED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HUMAN_REVIEW_REQUIRED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HUMAN_REVIEW_APPROVED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PUBLISHED&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FAILED&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This pattern gives me an append-only lineage timeline per document.&lt;/p&gt;




&lt;h2&gt;
  
  
  Error handling and retries (what I do on purpose)
&lt;/h2&gt;

&lt;p&gt;This pipeline is asynchronous and distributed, so failures are normal. I design for them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where I retry
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Textract throttling / throughput exceptions&lt;/li&gt;
&lt;li&gt;transient Lambda errors&lt;/li&gt;
&lt;li&gt;EventBridge &lt;code&gt;PutEvents&lt;/code&gt; partial failures (Step Functions can fail the task and retry)&lt;/li&gt;
&lt;li&gt;downstream S3 write failures&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where I do &lt;em&gt;not&lt;/em&gt; blindly retry
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;invalid file format&lt;/li&gt;
&lt;li&gt;missing required metadata&lt;/li&gt;
&lt;li&gt;corrupted documents&lt;/li&gt;
&lt;li&gt;deterministic business-rule violations&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Failure routing
&lt;/h3&gt;

&lt;p&gt;I emit a &lt;code&gt;document.failed&lt;/code&gt; event and write a terminal audit entry with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;failure type&lt;/li&gt;
&lt;li&gt;step name&lt;/li&gt;
&lt;li&gt;error code&lt;/li&gt;
&lt;li&gt;correlation IDs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes operations and replay much easier.&lt;/p&gt;




&lt;h1&gt;
  
  
  Cost and Throughput Tuning
&lt;/h1&gt;

&lt;p&gt;This is where the architecture becomes production-grade.&lt;/p&gt;

&lt;h2&gt;
  
  
  1) Choose Step Functions workflow type intentionally
&lt;/h2&gt;

&lt;p&gt;I use &lt;strong&gt;Standard&lt;/strong&gt; for the main orchestration because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;async waits&lt;/li&gt;
&lt;li&gt;human callback pauses&lt;/li&gt;
&lt;li&gt;richer history and control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I want to optimize cost for very high-volume short preprocessing tasks, I may split out a lightweight Express workflow in front, but I keep the long-running document path in Standard.&lt;/p&gt;

&lt;h2&gt;
  
  
  2) Tune Distributed Map concurrency to downstream capacity
&lt;/h2&gt;

&lt;p&gt;Distributed Map can scale very high, but the bottleneck is usually downstream service quotas (Textract, Lambda concurrency, DynamoDB write capacity, etc.).&lt;/p&gt;

&lt;p&gt;What I do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;make concurrency configurable (&lt;code&gt;MaxConcurrencyPath&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;start conservatively in production&lt;/li&gt;
&lt;li&gt;load test with realistic document sizes and page counts&lt;/li&gt;
&lt;li&gt;request quota increases before large launches&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3) Reduce unnecessary state transitions
&lt;/h2&gt;

&lt;p&gt;Polling is simple, but excessive polling increases state transitions and cost.&lt;/p&gt;

&lt;p&gt;Practical tuning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;increase wait interval after the first few polls (progressive backoff)&lt;/li&gt;
&lt;li&gt;use doc-size-aware polling intervals (longer waits for large PDFs)&lt;/li&gt;
&lt;li&gt;consider SNS/SQS callback for long-running jobs&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4) Store outputs once, reuse many times
&lt;/h2&gt;

&lt;p&gt;I avoid re-running Textract when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a replay is only needed for normalization logic changes&lt;/li&gt;
&lt;li&gt;a downstream consumer fails&lt;/li&gt;
&lt;li&gt;a human review needs to re-open a prior result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By storing raw output + normalized output + audit events, I can replay downstream steps without paying for re-extraction.&lt;/p&gt;

&lt;h2&gt;
  
  
  5) Separate hot path and review path
&lt;/h2&gt;

&lt;p&gt;I do not force all documents through human review.&lt;br&gt;
Most documents should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;extract&lt;/li&gt;
&lt;li&gt;validate&lt;/li&gt;
&lt;li&gt;publish&lt;/li&gt;
&lt;li&gt;finish&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only low-confidence or exception cases enter the review path. This keeps cost and latency predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  6) Add lifecycle policies
&lt;/h2&gt;

&lt;p&gt;For S3 cost management, I usually apply different retention windows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;raw uploads: longer retention (compliance dependent)&lt;/li&gt;
&lt;li&gt;raw Textract artifacts: medium retention&lt;/li&gt;
&lt;li&gt;normalized results: longer (business value)&lt;/li&gt;
&lt;li&gt;debug-only artifacts: shorter&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Security, Compliance, and Operational Notes
&lt;/h1&gt;

&lt;h2&gt;
  
  
  IAM least privilege
&lt;/h2&gt;

&lt;p&gt;Give each component only what it needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step Functions can call specific Lambdas / Textract / EventBridge&lt;/li&gt;
&lt;li&gt;Lambdas can read/write only the intended buckets/prefixes&lt;/li&gt;
&lt;li&gt;Reviewer API can only access review table + Step Functions callback APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Encryption
&lt;/h2&gt;

&lt;p&gt;I enable encryption at rest for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;S3 buckets&lt;/li&gt;
&lt;li&gt;DynamoDB tables&lt;/li&gt;
&lt;li&gt;logs (when applicable)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If needed, I also use KMS for tighter key control and auditability.&lt;/p&gt;

&lt;h2&gt;
  
  
  PII considerations
&lt;/h2&gt;

&lt;p&gt;Document pipelines often contain sensitive data. I plan for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;access controls by tenant&lt;/li&gt;
&lt;li&gt;minimal logging (no full document contents in logs)&lt;/li&gt;
&lt;li&gt;review UI redaction where needed&lt;/li&gt;
&lt;li&gt;retention policies aligned to policy/legal requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Observability
&lt;/h2&gt;

&lt;p&gt;I instrument:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step Functions execution failure alarms&lt;/li&gt;
&lt;li&gt;Lambda error/throttle alarms&lt;/li&gt;
&lt;li&gt;custom metrics (documents processed, review rate, average pages/doc, latency)&lt;/li&gt;
&lt;li&gt;audit events for business-level tracing&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Demo Tips (if you are presenting this live)
&lt;/h1&gt;

&lt;p&gt;This pattern demos extremely well if you show all three views:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Upload a document&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;drag-and-drop into S3 (or app UI)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Workflow progress&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step Functions execution graph&lt;/li&gt;
&lt;li&gt;Distributed Map child runs for batches&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Outputs&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;raw Textract JSON pointer&lt;/li&gt;
&lt;li&gt;normalized JSON in S3&lt;/li&gt;
&lt;li&gt;audit trail item(s)&lt;/li&gt;
&lt;li&gt;human review queue (triggered by a low-confidence sample)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If I want a smooth live demo, I prepare:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one “clean” invoice (no review needed)&lt;/li&gt;
&lt;li&gt;one noisy/scanned doc (human review path)&lt;/li&gt;
&lt;li&gt;one small batch manifest (3 to 10 docs) to show Distributed Map fan-out&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Extensions I would add next
&lt;/h1&gt;

&lt;p&gt;Once this base pipeline is running, it becomes a great platform for extensions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Document classification&lt;/strong&gt; before extraction (route to different extraction templates)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema validation&lt;/strong&gt; per document type&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenSearch indexing&lt;/strong&gt; for searchable archives&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comprehend / Bedrock post-processing&lt;/strong&gt; for summaries and entity normalization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tenant-specific extraction configs&lt;/strong&gt; (thresholds, required fields, routing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay tooling&lt;/strong&gt; for partial reprocessing from audit trail checkpoints&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Closing Thoughts
&lt;/h1&gt;

&lt;p&gt;I like this architecture because it is not just a toy OCR demo. It demonstrates a proper event-driven, auditable, scalable workflow that teams can actually build on.&lt;/p&gt;

&lt;p&gt;The combination of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;S3 for durable ingress&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Textract for extraction&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Step Functions for orchestration&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Distributed Map for batch scale&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EventBridge for decoupling&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;callback-based human review for exceptions&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;gives a strong foundation for production document intelligence systems.&lt;/p&gt;

&lt;p&gt;If I were publishing this as a developer advocacy post, I would also include a sample repository with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CDK deployment&lt;/li&gt;
&lt;li&gt;test documents&lt;/li&gt;
&lt;li&gt;reviewer UI mock&lt;/li&gt;
&lt;li&gt;load test script&lt;/li&gt;
&lt;li&gt;dashboards/alarms&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That turns the article into something readers can run and evolve.&lt;/p&gt;




&lt;h1&gt;
  
  
  References
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt;Amazon Textract async APIs and async processing overview (start/get patterns, async job model, supported file types, SNS notification model for async operations)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GetDocumentAnalysis&lt;/code&gt; pagination (&lt;code&gt;MaxResults&lt;/code&gt;, &lt;code&gt;NextToken&lt;/code&gt;) and paginated retrieval behavior&lt;/li&gt;
&lt;li&gt;Textract output storage and &lt;code&gt;OutputConfig&lt;/code&gt; (custom S3 output and optional KMS for output)&lt;/li&gt;
&lt;li&gt;Textract quotas and Service Quotas guidance&lt;/li&gt;
&lt;li&gt;S3 -&amp;gt; EventBridge integration and S3 EventBridge event structure&lt;/li&gt;
&lt;li&gt;Step Functions Distributed Map (when to use it, high concurrency, child workflows, map runs, input/history limits)&lt;/li&gt;
&lt;li&gt;Step Functions callback/task token pattern and long waits for external/human steps&lt;/li&gt;
&lt;li&gt;Step Functions optimized EventBridge integration (&lt;code&gt;events:putEvents&lt;/code&gt;) and failure handling behavior&lt;/li&gt;
&lt;li&gt;Step Functions pricing model (state transitions for Standard workflows)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>aws</category>
      <category>python</category>
      <category>programming</category>
    </item>
    <item>
      <title>My Study Guide for the Microsoft Certified Machine Learning Operations MLOps Engineer Associate Beta Exam</title>
      <dc:creator>Renaldi</dc:creator>
      <pubDate>Fri, 03 Apr 2026 22:00:00 +0000</pubDate>
      <link>https://dev.to/rengond/my-study-guide-for-the-microsoft-certified-machine-learning-operations-mlops-engineer-associate-beta-31g3</link>
      <guid>https://dev.to/rengond/my-study-guide-for-the-microsoft-certified-machine-learning-operations-mlops-engineer-associate-beta-31g3</guid>
      <description>&lt;p&gt;When I study beta exams, I do not treat them like ordinary certification exams; I treat them like signals. They usually show where Microsoft thinks the role is heading next, what skills are becoming core, and which responsibilities are moving from specialist territory into mainstream expectations. That is exactly how I approached this one.&lt;/p&gt;

&lt;p&gt;What stood out to me most is that this exam is not only about shipping a model. It is about owning the full operational system around machine learning and generative AI on Azure. That means infrastructure, repeatability, deployment, monitoring, observability, evaluation, safety, retrieval quality, versioning, and optimization. If you study this as a narrow model training exam, I think you will undershoot what the role is really asking for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who I think this exam is really for
&lt;/h2&gt;

&lt;p&gt;I think this beta makes the most sense for people who already have some machine learning familiarity and now want to prove they can think operationally. If you have trained models before but have not spent enough time on automation, deployment, rollout strategy, monitoring, or generative AI quality assurance, this exam will probably stretch you in exactly the right areas.&lt;/p&gt;

&lt;p&gt;I would also say this guide is useful for people who sit somewhere between data science and platform engineering. That feels like the real audience. Not purely academic machine learning practitioners. Not purely infrastructure engineers. People who want to understand how AI systems actually become production systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I believe the exam is really testing
&lt;/h2&gt;

&lt;p&gt;The current official study guide breaks the exam into five major domains. I would not memorize the domain names only. I would translate them into practical questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Domain 1
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Design and implement an MLOps infrastructure
&lt;/h4&gt;

&lt;p&gt;This is really asking whether you can build the operational foundation for machine learning on Azure in a way that a team could actually reuse.&lt;/p&gt;

&lt;h3&gt;
  
  
  Domain 2
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Implement machine learning model lifecycle and operations
&lt;/h4&gt;

&lt;p&gt;This is really asking whether you understand that the model is only one piece of the job, and that lifecycle discipline matters just as much as model quality.&lt;/p&gt;

&lt;h3&gt;
  
  
  Domain 3
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Design and implement a GenAIOps infrastructure
&lt;/h4&gt;

&lt;p&gt;This is really asking whether you can treat generative AI systems with the same seriousness that strong teams already apply to software delivery and MLOps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Domain 4
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Implement generative AI quality assurance and observability
&lt;/h4&gt;

&lt;p&gt;This is really asking whether you know how to judge if a generative AI system is good, safe, stable, and supportable after release.&lt;/p&gt;

&lt;h3&gt;
  
  
  Domain 5
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Optimize generative AI systems and model performance
&lt;/h4&gt;

&lt;p&gt;This is really asking whether you know how to improve a system after the first version works.&lt;/p&gt;

&lt;h2&gt;
  
  
  My overall study philosophy
&lt;/h2&gt;

&lt;p&gt;My rule for this beta is simple. Study every topic from three different angles.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What the service or capability does&lt;/li&gt;
&lt;li&gt;Why a team would choose it in real production work&lt;/li&gt;
&lt;li&gt;What can go wrong after deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That third point is where I think a lot of people fall short. It is easy to say you know how to create a workspace, deploy a model, or run an evaluation. It is much harder to explain how you would operate that system safely over time, what you would monitor, what you would version, what you would lock down, and how you would know that something is getting worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain 1 in depth
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Design and implement an MLOps infrastructure
&lt;/h3&gt;

&lt;p&gt;This domain covers the platform foundations for traditional machine learning on Azure. I see it as the domain that separates casual Azure Machine Learning usage from real MLOps thinking.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I would focus on
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Azure Machine Learning workspace design&lt;/li&gt;
&lt;li&gt;Datastores and data assets&lt;/li&gt;
&lt;li&gt;Compute targets and their purpose&lt;/li&gt;
&lt;li&gt;Environments and reusable components&lt;/li&gt;
&lt;li&gt;Registries and cross workspace reuse&lt;/li&gt;
&lt;li&gt;Identity and access control&lt;/li&gt;
&lt;li&gt;Private networking and secure access&lt;/li&gt;
&lt;li&gt;Git integration&lt;/li&gt;
&lt;li&gt;Azure CLI and Bicep for repeatable setup&lt;/li&gt;
&lt;li&gt;GitHub Actions for automation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What I think matters most here
&lt;/h3&gt;

&lt;p&gt;The most important mindset shift is to stop thinking about a workspace as a place where you click around in the portal and start thinking about it as a controlled platform foundation. The exam focus here feels relevant because this is where operational maturity begins. A lot of AI teams still treat infrastructure as something they sort out later. This exam clearly does not.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I would actually practice
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Create a workspace and understand the role of related resources&lt;/li&gt;
&lt;li&gt;Compare different compute options and know why you would choose each one&lt;/li&gt;
&lt;li&gt;Create data assets and environments that can be reused&lt;/li&gt;
&lt;li&gt;Understand when a registry helps with scale and team collaboration&lt;/li&gt;
&lt;li&gt;Read Bicep and Azure CLI examples until they stop feeling unfamiliar&lt;/li&gt;
&lt;li&gt;Set up a simple GitHub Actions workflow that touches the machine learning lifecycle&lt;/li&gt;
&lt;li&gt;Review a secure networking setup and be able to explain why it matters&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Red flags during study
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Only learning portal steps&lt;/li&gt;
&lt;li&gt;Ignoring identity and networking&lt;/li&gt;
&lt;li&gt;Treating source control as optional&lt;/li&gt;
&lt;li&gt;Memorizing syntax without understanding the operational reason behind it&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Domain 2 in depth
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Implement machine learning model lifecycle and operations
&lt;/h3&gt;

&lt;p&gt;This is the domain I would spend the most time on. It is also the one that feels closest to the day to day reality of MLOps work.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I would focus on
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Experiment tracking with MLflow&lt;/li&gt;
&lt;li&gt;Automated machine learning&lt;/li&gt;
&lt;li&gt;Notebooks and jobs&lt;/li&gt;
&lt;li&gt;Hyperparameter tuning&lt;/li&gt;
&lt;li&gt;Distributed training&lt;/li&gt;
&lt;li&gt;Training pipelines&lt;/li&gt;
&lt;li&gt;Model registration and lifecycle management&lt;/li&gt;
&lt;li&gt;Responsible AI checks&lt;/li&gt;
&lt;li&gt;Batch inference and real time inference&lt;/li&gt;
&lt;li&gt;Rollout strategy and rollback strategy&lt;/li&gt;
&lt;li&gt;Drift detection and response&lt;/li&gt;
&lt;li&gt;Monitoring and retraining triggers&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What I think matters most here
&lt;/h3&gt;

&lt;p&gt;To me, this domain tests whether you understand that training a model is just the beginning. Strong candidates should be able to think in versions, runs, promotion paths, failure handling, deployment strategy, and operational feedback loops.&lt;/p&gt;

&lt;p&gt;I also think this is where many people who call themselves familiar with MLOps get exposed. A lot of people really mean that they have deployed a model once. That is not the same as managing the lifecycle of a model in production. This domain seems much more aligned with the real role.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I would actually practice
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Track experiments in MLflow and compare runs with intention&lt;/li&gt;
&lt;li&gt;Run a simple sweep job and understand what problem it solves&lt;/li&gt;
&lt;li&gt;Register a model and think carefully about version control&lt;/li&gt;
&lt;li&gt;Compare batch inference and online inference through real scenarios&lt;/li&gt;
&lt;li&gt;Study what safe rollout and rollback mean for business risk&lt;/li&gt;
&lt;li&gt;Review how drift shows up and what an operational response should look like&lt;/li&gt;
&lt;li&gt;Think through what should trigger retraining and when retraining should not be automatic&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Questions I would ask myself
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;How do I know which model version is live&lt;/li&gt;
&lt;li&gt;What would I monitor after deployment&lt;/li&gt;
&lt;li&gt;When would I prefer batch over online&lt;/li&gt;
&lt;li&gt;What would make me roll back quickly&lt;/li&gt;
&lt;li&gt;What signals tell me performance is degrading&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Domain 3 in depth
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Design and implement a GenAIOps infrastructure
&lt;/h2&gt;

&lt;p&gt;This domain is one of the clearest signs that Microsoft is treating generative AI operations as a first class engineering discipline, not just an experimental side topic.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I would focus on
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Microsoft Foundry setup and project structure&lt;/li&gt;
&lt;li&gt;Managed identity and access control&lt;/li&gt;
&lt;li&gt;Network security&lt;/li&gt;
&lt;li&gt;Azure CLI and Bicep for deployment&lt;/li&gt;
&lt;li&gt;Model selection strategy&lt;/li&gt;
&lt;li&gt;Serverless deployment options&lt;/li&gt;
&lt;li&gt;Managed compute options&lt;/li&gt;
&lt;li&gt;Prompt design as an engineering asset&lt;/li&gt;
&lt;li&gt;Prompt variants and versioning&lt;/li&gt;
&lt;li&gt;Source control for prompts and application logic&lt;/li&gt;
&lt;li&gt;Capacity and throughput planning&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What I think matters most here
&lt;/h3&gt;

&lt;p&gt;I really like that this domain does not reduce generative AI work to clever prompting. That would have made the exam feel shallow. Instead, the domain focus suggests that Microsoft wants candidates to think about generative AI as an operational system with infrastructure, security, versioning, and delivery discipline.&lt;/p&gt;

&lt;p&gt;That feels much more useful and much closer to what strong teams are actually trying to build.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I would actually practice
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Create a small Foundry project and inspect how it is organized&lt;/li&gt;
&lt;li&gt;Compare serverless access with managed compute and explain the tradeoff&lt;/li&gt;
&lt;li&gt;Practice thinking through model choice based on latency, cost, scale, and task fit&lt;/li&gt;
&lt;li&gt;Store prompt variants in Git and treat them as production assets&lt;/li&gt;
&lt;li&gt;Compare prompt versions with a repeatable test set instead of instinct alone&lt;/li&gt;
&lt;li&gt;Review how access, networking, and deployment decisions affect operational safety&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Common mistake I would avoid
&lt;/h3&gt;

&lt;p&gt;Do not study generative AI operations as if it is just prompt engineering plus model selection. The exam focus appears broader than that. I would expect the stronger candidates to understand the surrounding engineering system as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain 4 in depth
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Implement generative AI quality assurance and observability
&lt;/h3&gt;

&lt;p&gt;This domain is smaller by weighting but huge in practical value. In real production work, this is where weak systems often reveal themselves.&lt;/p&gt;

&lt;h4&gt;
  
  
  What I would focus on
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Test datasets for evaluation&lt;/li&gt;
&lt;li&gt;Quality metrics such as groundedness relevance coherence and fluency&lt;/li&gt;
&lt;li&gt;Safety evaluation&lt;/li&gt;
&lt;li&gt;Harmful content monitoring&lt;/li&gt;
&lt;li&gt;Automated evaluations&lt;/li&gt;
&lt;li&gt;Continuous monitoring in Foundry&lt;/li&gt;
&lt;li&gt;Latency and throughput tracking&lt;/li&gt;
&lt;li&gt;Token and cost visibility&lt;/li&gt;
&lt;li&gt;Logging tracing and debugging&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  What I think matters most here
&lt;/h4&gt;

&lt;p&gt;Generative AI systems do not fail in only one way. They can become ungrounded, irrelevant, too expensive, too slow, inconsistent, or unsafe. That means quality assurance for generative AI has to be broader than a simple accuracy mindset.&lt;/p&gt;

&lt;p&gt;This domain feels very relevant because it pushes candidates toward that broader view. I would not treat it as a side topic just because the weighting is lower. In real projects, a surprising amount of pain starts here.&lt;/p&gt;

&lt;h4&gt;
  
  
  What I would actually practice
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Build a small evaluation set for a realistic use case&lt;/li&gt;
&lt;li&gt;Compare outputs against several quality dimensions&lt;/li&gt;
&lt;li&gt;Think about safety and harmful output as operational concerns&lt;/li&gt;
&lt;li&gt;Review token consumption and latency as first class engineering metrics&lt;/li&gt;
&lt;li&gt;Explore tracing and debugging ideas for multi step AI application flows&lt;/li&gt;
&lt;li&gt;Practice describing what good monitoring should actually tell you&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  A practical mindset that helps here
&lt;/h4&gt;

&lt;p&gt;When I study this domain, I try to move beyond asking whether an answer looks good. I ask whether the system is measurable, debuggable, supportable, and safe enough to own over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain 5 in depth
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Optimize generative AI systems and model performance
&lt;/h3&gt;

&lt;p&gt;This domain is about moving from a working prototype to a stronger production system.&lt;/p&gt;

&lt;h4&gt;
  
  
  What I would focus on
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Retrieval augmented generation tuning&lt;/li&gt;
&lt;li&gt;Chunking strategy&lt;/li&gt;
&lt;li&gt;Similarity thresholds&lt;/li&gt;
&lt;li&gt;Embedding model choice&lt;/li&gt;
&lt;li&gt;Hybrid retrieval&lt;/li&gt;
&lt;li&gt;Relevance evaluation&lt;/li&gt;
&lt;li&gt;A B style testing&lt;/li&gt;
&lt;li&gt;Fine tuning strategy&lt;/li&gt;
&lt;li&gt;Synthetic data management&lt;/li&gt;
&lt;li&gt;Monitoring customized models in production&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  What I think matters most here
&lt;/h4&gt;

&lt;p&gt;I find this domain especially realistic because the hard part of generative AI is often not making something work once. The hard part is making it work better in a controlled and measurable way.&lt;/p&gt;

&lt;p&gt;The focus here suggests that Microsoft expects candidates to understand optimization as an ongoing engineering process. That feels right to me.&lt;/p&gt;

&lt;h4&gt;
  
  
  What I would actually practice
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Compare different chunk sizes and review how retrieval quality changes&lt;/li&gt;
&lt;li&gt;Think about what happens when similarity thresholds are too strict or too loose&lt;/li&gt;
&lt;li&gt;Compare semantic retrieval and hybrid retrieval for a realistic use case&lt;/li&gt;
&lt;li&gt;Practice explaining when fine tuning is justified and when retrieval or prompt work is enough&lt;/li&gt;
&lt;li&gt;Think about how you would promote a customized model into production safely&lt;/li&gt;
&lt;li&gt;Review relevance tradeoffs alongside latency and cost tradeoffs&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The resources I would use first
&lt;/h3&gt;

&lt;p&gt;I would not study from random blogs first. I would start with the Microsoft material that best matches the beta and then layer my own hands on practice on top.&lt;/p&gt;

&lt;h3&gt;
  
  
  Official exam page
&lt;/h3&gt;

&lt;p&gt;Use this first to confirm the role focus and linked preparation material.&lt;/p&gt;

&lt;p&gt;learn.microsoft.com/en-us/credentials/certifications/operationalizing-machine-learning-and-generative-ai-solutions/&lt;/p&gt;

&lt;h3&gt;
  
  
  Official study guide for AI 300
&lt;/h3&gt;

&lt;p&gt;This is the single most important resource for mapping your preparation. I would keep coming back to it while studying.&lt;/p&gt;

&lt;p&gt;learn.microsoft.com/en-us/credentials/certifications/resources/study-guides/ai-300&lt;/p&gt;

&lt;h3&gt;
  
  
  Official course for operationalizing machine learning and generative AI solutions
&lt;/h3&gt;

&lt;p&gt;This is a very good structured backbone if you want guided preparation.&lt;/p&gt;

&lt;p&gt;learn.microsoft.com/en-us/training/courses/ai-300t00&lt;/p&gt;

&lt;h3&gt;
  
  
  Learning path for operationalize machine learning models
&lt;/h3&gt;

&lt;p&gt;This is especially useful for the traditional MLOps parts of the exam such as Azure Machine Learning workflows, pipelines, GitHub Actions, environments, and deployment.&lt;/p&gt;

&lt;p&gt;learn.microsoft.com/en-us/training/paths/build-first-machine-operations-workflow/&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Actions for Azure Machine Learning
&lt;/h3&gt;

&lt;p&gt;I would study this if CI and CD for machine learning still feels a bit fuzzy.&lt;/p&gt;

&lt;p&gt;learn.microsoft.com/en-us/azure/machine-learning/how-to-github-actions-machine-learning&lt;/p&gt;

&lt;h3&gt;
  
  
  Set up MLOps with GitHub and Azure Machine Learning
&lt;/h3&gt;

&lt;p&gt;This is useful if you want a more end to end sample oriented view.&lt;/p&gt;

&lt;p&gt;learn.microsoft.com/en-us/azure/machine-learning/how-to-setup-mlops-github-azure-ml&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy machine learning models to online endpoints
&lt;/h3&gt;

&lt;p&gt;A key resource for real time inference preparation.&lt;/p&gt;

&lt;p&gt;learn.microsoft.com/en-us/azure/machine-learning/how-to-deploy-online-endpoints&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy MLflow models to online endpoints
&lt;/h3&gt;

&lt;p&gt;Important if you want your MLflow understanding to connect directly to Azure deployment patterns.&lt;/p&gt;

&lt;p&gt;learn.microsoft.com/en-us/azure/machine-learning/how-to-deploy-mlflow-models-online-endpoints&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy MLflow models in batch deployments
&lt;/h3&gt;

&lt;p&gt;Helpful for understanding batch inference patterns and where they fit.&lt;/p&gt;

&lt;p&gt;learn.microsoft.com/en-us/azure/machine-learning/how-to-mlflow-batch&lt;/p&gt;

&lt;h3&gt;
  
  
  Microsoft Foundry documentation
&lt;/h3&gt;

&lt;p&gt;This is the starting point for the generative AI side of the exam.&lt;/p&gt;

&lt;p&gt;learn.microsoft.com/en-us/azure/foundry/&lt;/p&gt;

&lt;h3&gt;
  
  
  Foundry observability
&lt;/h3&gt;

&lt;p&gt;Very important for the quality assurance and monitoring domains.&lt;/p&gt;

&lt;p&gt;learn.microsoft.com/en-us/azure/foundry/concepts/observability&lt;/p&gt;

&lt;h3&gt;
  
  
  Evaluate generative AI applications in Foundry
&lt;/h3&gt;

&lt;p&gt;A strong resource for learning how evaluations actually work in practice.&lt;/p&gt;

&lt;p&gt;learn.microsoft.com/en-us/azure/foundry/how-to/evaluate-generative-ai-app&lt;/p&gt;

&lt;h3&gt;
  
  
  Evaluate AI agents in Foundry
&lt;/h3&gt;

&lt;p&gt;Worth reviewing if you want to understand how Microsoft is framing agent evaluation in operational terms.&lt;/p&gt;

&lt;p&gt;learn.microsoft.com/en-us/azure/foundry/observability/how-to/evaluate-agent&lt;/p&gt;

&lt;h2&gt;
  
  
  My recommended study sequence
&lt;/h2&gt;

&lt;p&gt;If I were starting from scratch, this is the order I would follow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Build the map
&lt;/h4&gt;

&lt;p&gt;Read the official study guide carefully and create your own notes under the five domains. Mark every topic as strong medium or weak.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Go deep on core MLOps
&lt;/h4&gt;

&lt;p&gt;Spend focused time on experiments pipelines MLflow model registration endpoints rollout rollback and drift. This is where I think the exam has the most practical depth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Add the GenAIOps layer
&lt;/h4&gt;

&lt;p&gt;Move into Foundry setup model choice prompt versioning evaluations observability and optimization. Do not treat this as a separate world. Connect it back to operational discipline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Practice decision making
&lt;/h4&gt;

&lt;p&gt;The exam is likely to reward people who can choose between approaches, not just define features. Practice reasoning through tradeoffs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Revisit weak areas with hands on reinforcement
&lt;/h4&gt;

&lt;p&gt;When a topic feels vague, do not just reread it. Build a tiny example, sketch a workflow, or explain it aloud.&lt;/p&gt;

&lt;h2&gt;
  
  
  A four week plan that I think is realistic
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Week 1
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Build your domain map
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Read the exam page and study guide slowly&lt;/li&gt;
&lt;li&gt;Build notes for all five domains&lt;/li&gt;
&lt;li&gt;Mark weak areas honestly&lt;/li&gt;
&lt;li&gt;Start with the official course or learning path&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Week 2
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Go deep on machine learning lifecycle work
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Focus on experiments MLflow sweeps pipelines registration deployment and monitoring&lt;/li&gt;
&lt;li&gt;Practice thinking through real scenarios&lt;/li&gt;
&lt;li&gt;Learn to describe rollout rollback and drift clearly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Week 3
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Go deep on generative AI operations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Focus on Foundry setup model choice prompt versioning evaluations observability and optimization&lt;/li&gt;
&lt;li&gt;Build a small comparison mindset for prompts and models&lt;/li&gt;
&lt;li&gt;Think in terms of quality safety latency and cost together&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Week 4
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Consolidate and rehearse
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Revisit all weak areas&lt;/li&gt;
&lt;li&gt;Rebuild key workflows from memory&lt;/li&gt;
&lt;li&gt;Review official resources again&lt;/li&gt;
&lt;li&gt;Practice explaining why one approach is better than another&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you only have a weekend
&lt;/h2&gt;

&lt;p&gt;If time is short, I would not try to cover everything equally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Day 1
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Read the study guide carefully&lt;/li&gt;
&lt;li&gt;Focus first on Domain 2 and Domain 3&lt;/li&gt;
&lt;li&gt;Build one page notes for traditional MLOps and GenAIOps&lt;/li&gt;
&lt;li&gt;Review lifecycle thinking more than portal details&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Day 2
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Cover Domain 1 Domain 4 and Domain 5&lt;/li&gt;
&lt;li&gt;Review security automation observability evaluation and optimization&lt;/li&gt;
&lt;li&gt;End by testing whether you can explain each domain in your own words&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would avoid during preparation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Passive reading without building a domain map&lt;/li&gt;
&lt;li&gt;Studying only portal click paths&lt;/li&gt;
&lt;li&gt;Ignoring GitHub Actions and infrastructure setup&lt;/li&gt;
&lt;li&gt;Treating prompt work as separate from engineering discipline&lt;/li&gt;
&lt;li&gt;Overfocusing on one familiar area and neglecting the rest&lt;/li&gt;
&lt;li&gt;Memorizing terms without understanding why they matter in production&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My final take
&lt;/h2&gt;

&lt;p&gt;If I had to sum up the beta in one sentence, I would say this exam is trying to validate whether you can think like an operator of AI systems on Azure, not just a user of AI tools.&lt;/p&gt;

&lt;p&gt;That is why I find it interesting. It feels like a certification for people who want to move from experimentation into ownership.&lt;/p&gt;

&lt;p&gt;If you are preparing for it, I would study it like a role, not like a quiz. That mindset alone will make your preparation much stronger.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>azure</category>
      <category>machinelearning</category>
      <category>programming</category>
    </item>
    <item>
      <title>Serverless ETL/ELT Architecture with S3, EventBridge, Lambda, Step Functions, and Glue</title>
      <dc:creator>Renaldi</dc:creator>
      <pubDate>Sun, 29 Mar 2026 22:00:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/serverless-etlelt-architecture-with-s3-eventbridge-lambda-step-functions-and-glue-31l1</link>
      <guid>https://dev.to/aws-builders/serverless-etlelt-architecture-with-s3-eventbridge-lambda-step-functions-and-glue-31l1</guid>
      <description>&lt;p&gt;In this post, I will walk through a production-style &lt;strong&gt;serverless ETL/ELT architecture&lt;/strong&gt; on AWS using &lt;strong&gt;Amazon S3, Amazon EventBridge, AWS Lambda, AWS Step Functions, and AWS Glue&lt;/strong&gt;. I will cover the full flow from event-driven ingestion to validation, quarantine handling, orchestration, schema drift handling, data quality checks, and replay.&lt;/p&gt;

&lt;p&gt;I am intentionally designing this as a pattern that can support both &lt;strong&gt;ETL&lt;/strong&gt; and &lt;strong&gt;ELT&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ETL&lt;/strong&gt; when I perform transformations in Glue before landing curated outputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ELT&lt;/strong&gt; when I land validated/raw data first and defer transformation to downstream query engines or warehouse jobs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This architecture is a strong fit for data lake ingestion pipelines where I want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;event-driven automation&lt;/li&gt;
&lt;li&gt;low operational overhead&lt;/li&gt;
&lt;li&gt;clear failure handling&lt;/li&gt;
&lt;li&gt;replayability&lt;/li&gt;
&lt;li&gt;observability&lt;/li&gt;
&lt;li&gt;and enough flexibility to survive real-world data messiness&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A few implementation choices in this post are deliberate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I use &lt;strong&gt;Step Functions Standard&lt;/strong&gt; (not Express) because I want durable, auditable executions and because the common Glue &lt;code&gt;.sync&lt;/code&gt; integration pattern is not supported in Express workflows. AWS documents the Standard vs Express execution semantics and notes that Express does not support &lt;code&gt;.sync&lt;/code&gt; job-run service integration patterns. (&lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/choosing-workflow-type.html" rel="noopener noreferrer"&gt;docs.aws.amazon.com&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;I use &lt;strong&gt;S3 Event Notifications via EventBridge&lt;/strong&gt; for flexible routing and filtering. AWS also notes that after enabling EventBridge delivery on an S3 bucket, it can take around five minutes for changes to take effect, which is worth remembering during testing. (&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/enable-event-notifications-eventbridge.html" rel="noopener noreferrer"&gt;docs.aws.amazon.com&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why this architecture works well in practice
&lt;/h2&gt;

&lt;p&gt;When I build data ingestion pipelines, the biggest problems are usually not the happy path. They are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;malformed files&lt;/li&gt;
&lt;li&gt;duplicate deliveries&lt;/li&gt;
&lt;li&gt;late files&lt;/li&gt;
&lt;li&gt;schema drift&lt;/li&gt;
&lt;li&gt;partial failures&lt;/li&gt;
&lt;li&gt;silently bad data&lt;/li&gt;
&lt;li&gt;and the inability to replay safely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This pattern addresses those operational concerns directly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;S3 + EventBridge&lt;/strong&gt; gives me flexible event routing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda&lt;/strong&gt; handles fast validation/classification&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step Functions&lt;/strong&gt; gives me explicit orchestration, branching, retries, and auditability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Glue&lt;/strong&gt; handles scalable transformation and schema normalization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Glue Data Quality (or equivalent checks)&lt;/strong&gt; enforces quality gates before data becomes trusted&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS Glue Data Quality is a managed, serverless capability built on DeeQu and uses DQDL (Data Quality Definition Language), which is useful when I want rules-as-code for quality checks. (&lt;a href="https://docs.aws.amazon.com/glue/latest/dg/glue-data-quality.html" rel="noopener noreferrer"&gt;docs.aws.amazon.com&lt;/a&gt;)&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;At a high level, I split the data lake into zones and make each stage explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;raw-ingest bucket&lt;/strong&gt;: original landing location (immutable input)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;validated bucket&lt;/strong&gt;: files that passed lightweight ingest validation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;quarantine bucket&lt;/strong&gt;: invalid files, bad schema changes, or failed quality checks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;curated bucket&lt;/strong&gt;: transformed and query-ready outputs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;dq-results bucket&lt;/strong&gt;: data quality evaluation outputs and reports&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;replay-manifest bucket&lt;/strong&gt;: manifests used to reprocess files safely&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A producer drops a file in &lt;code&gt;raw-ingest&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;S3 emits an event to EventBridge&lt;/li&gt;
&lt;li&gt;EventBridge triggers a validation Lambda&lt;/li&gt;
&lt;li&gt;Lambda validates and stages valid files into &lt;code&gt;validated&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Lambda starts Step Functions with a normalized payload&lt;/li&gt;
&lt;li&gt;Step Functions runs Glue transformation&lt;/li&gt;
&lt;li&gt;Step Functions runs data quality checks&lt;/li&gt;
&lt;li&gt;Pass -&amp;gt; publish success / expose curated data&lt;/li&gt;
&lt;li&gt;Fail -&amp;gt; quarantine and notify&lt;/li&gt;
&lt;li&gt;Replay can be triggered later using replay manifests&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkhfnncaroa2x6gqwu6rn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkhfnncaroa2x6gqwu6rn.png" alt=" " width="800" height="242"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  End-to-End Walkthrough
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Event-driven ingest (S3 -&amp;gt; EventBridge)
&lt;/h3&gt;

&lt;p&gt;I start by enabling &lt;strong&gt;EventBridge notifications&lt;/strong&gt; on the raw ingest bucket. This gives me centralized event filtering/routing in EventBridge rather than hardwiring everything as direct S3 notifications.&lt;/p&gt;

&lt;p&gt;AWS documents S3 EventBridge integration and bucket-level enablement for EventBridge event delivery. (&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/enable-event-notifications-eventbridge.html" rel="noopener noreferrer"&gt;docs.aws.amazon.com&lt;/a&gt;)&lt;/p&gt;

&lt;h4&gt;
  
  
  Example: enable EventBridge on the raw ingest bucket
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws s3api put-bucket-notification-configuration &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bucket&lt;/span&gt; my-raw-ingest-bucket &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--notification-configuration&lt;/span&gt; &lt;span class="s1"&gt;'{
    "EventBridgeConfiguration": {}
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2) Event filtering in EventBridge
&lt;/h3&gt;

&lt;p&gt;I usually create one or more EventBridge rules to target datasets by path prefix and file type. This lets me route different datasets into different workflows without changing the producers.&lt;/p&gt;

&lt;h4&gt;
  
  
  Example EventBridge rule pattern (illustrative)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"aws.s3"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail-type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Object Created"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"bucket"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"my-raw-ingest-bucket"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"landing/orders/"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From here, I send the event to a &lt;strong&gt;Validation Lambda&lt;/strong&gt; (or directly to Step Functions if I want validation inside the state machine).&lt;/p&gt;




&lt;h3&gt;
  
  
  3) Validation and quarantine buckets
&lt;/h3&gt;

&lt;p&gt;I like to separate &lt;strong&gt;lightweight ingest validation&lt;/strong&gt; from heavy transforms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;file naming / suffix checks&lt;/li&gt;
&lt;li&gt;minimum size checks&lt;/li&gt;
&lt;li&gt;header checks (CSV/JSON)&lt;/li&gt;
&lt;li&gt;partition key extraction from path&lt;/li&gt;
&lt;li&gt;duplicate detection / idempotency signal generation&lt;/li&gt;
&lt;li&gt;metadata enrichment (dataset, source, arrival timestamp, run ID)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If validation passes, I stage the object into a &lt;strong&gt;validated bucket&lt;/strong&gt;.&lt;br&gt;
If it fails, I route it to &lt;strong&gt;quarantine&lt;/strong&gt; with a reason code.&lt;/p&gt;

&lt;p&gt;This gives me two important benefits:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I preserve the original raw object for traceability&lt;/li&gt;
&lt;li&gt;I avoid sending obviously bad data into expensive Glue runs&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;
  
  
  Example validation Lambda (Python)
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sfn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stepfunctions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;VALIDATED_BUCKET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VALIDATED_BUCKET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;STATE_MACHINE_ARN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STATE_MACHINE_ARN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;ALLOWED_SUFFIXES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;MIN_BYTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;  &lt;span class="c1"&gt;# example threshold
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_extract_dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Example key: landing/orders/2026/02/25/orders_001.csv
&lt;/span&gt;    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;landing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid_key_prefix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_validate_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;head_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ContentLength&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ALLOWED_SUFFIXES&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unsupported_extension&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;MIN_BYTES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;file_too_small&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_extract_dataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Optional: inspect a small header/sample for CSV/JSON sanity
&lt;/span&gt;    &lt;span class="n"&gt;sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Range&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bytes=0-1023&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;empty_sample&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dataset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;size_bytes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ContentType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/octet-stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;etag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ETag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'"'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_stage_validated_copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src_bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# I usually add a run/date prefix for traceability
&lt;/span&gt;    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;dst_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;validated/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ingest_date=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;Y&lt;/span&gt;&lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;-%&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_id=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;src_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copy_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;VALIDATED_BUCKET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dst_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CopySource&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;src_bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;src_key&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;MetadataDirective&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REPLACE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Metadata&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;src_bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;src_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dataset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;validated_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dst_key&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Supports direct EventBridge S3 events.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;detail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;src_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;src_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unquote_plus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;validation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_validate_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src_bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;src_bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;src_key&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;validation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;receivedAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;dst_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_stage_validated_copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src_bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dataset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;validated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;VALIDATED_BUCKET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dst_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3Uri&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;VALIDATED_BUCKET&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;dst_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Start orchestration (Step Functions Standard)
&lt;/span&gt;    &lt;span class="n"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_execution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;stateMachineArn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;STATE_MACHINE_ARN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;})}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Implementation notes for validation
&lt;/h3&gt;

&lt;p&gt;A few things I recommend in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Do not rely on ETag as a universal checksum&lt;/strong&gt; for multipart uploads&lt;/li&gt;
&lt;li&gt;Keep validation Lambda &lt;strong&gt;fast&lt;/strong&gt; and &lt;strong&gt;deterministic&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Record a consistent &lt;strong&gt;run ID&lt;/strong&gt; and propagate it through every stage&lt;/li&gt;
&lt;li&gt;Treat the raw bucket as immutable evidence&lt;/li&gt;
&lt;li&gt;Add bucket lifecycle policies for quarantine and intermediate zones&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  4) Orchestration for transforms (Step Functions)
&lt;/h3&gt;

&lt;p&gt;This is where the pipeline becomes operationally manageable.&lt;/p&gt;

&lt;p&gt;I use Step Functions to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;branch on validation status&lt;/li&gt;
&lt;li&gt;invoke quarantine logic on failure&lt;/li&gt;
&lt;li&gt;invoke Glue for transform&lt;/li&gt;
&lt;li&gt;run data quality checks&lt;/li&gt;
&lt;li&gt;branch on DQ result&lt;/li&gt;
&lt;li&gt;emit success/failure events/notifications&lt;/li&gt;
&lt;li&gt;maintain an auditable execution trail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS documents the Step Functions Glue integration for starting Glue jobs, which is exactly what I use here. (&lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/connect-glue.html" rel="noopener noreferrer"&gt;docs.aws.amazon.com&lt;/a&gt;)&lt;/p&gt;
&lt;h4&gt;
  
  
  Example Step Functions state machine (ASL JSON, simplified)
&lt;/h4&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Comment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Serverless ETL/ELT pipeline with validation, transform, DQ, and quarantine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"StartAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ValidationPassed?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"States"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ValidationPassed?"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Choice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Choices"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Variable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.validation.valid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"BooleanEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PersistMetadata"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"QuarantineInvalidInput"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"PersistMetadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pass"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"runId.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.runId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"source.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.source"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"validated.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.validated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"validation.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.validation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"receivedAt.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.receivedAt"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RunGlueTransform"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"RunGlueTransform"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::glue:startJobRun.sync"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"JobName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders-curation-job"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"--run_id.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.runId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"--input_s3_uri.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.validated.s3Uri"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"--output_s3_uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3://my-curated-bucket/curated/orders/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"--dataset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Retry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ErrorEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Glue.ConcurrentRunsExceededException"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"States.TaskFailed"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"IntervalSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"MaxAttempts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"BackoffRate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Catch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ErrorEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"States.ALL"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ResultPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"QuarantineTransformFailure"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RunDataQualityChecks"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"RunDataQualityChecks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::lambda:invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FunctionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dq-evaluator-lambda"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"runId.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.runId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"dataset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"curatedS3Prefix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"s3://my-curated-bucket/curated/orders/"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ResultPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.dqInvoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Catch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ErrorEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"States.ALL"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ResultPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"QuarantineDQFailure"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DQPassed?"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"DQPassed?"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Choice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Choices"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Variable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.dqInvoke.Payload.pass"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"BooleanEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PublishSuccess"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"QuarantineDQFailure"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"PublishSuccess"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Pass"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Result"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SUCCEEDED"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"End"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"QuarantineInvalidInput"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::lambda:invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FunctionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"quarantine-lambda"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"runId.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.runId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"source.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.source"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"reason.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.validation.reason"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"stage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INGEST_VALIDATION"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"End"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"QuarantineTransformFailure"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::lambda:invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FunctionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"quarantine-lambda"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"runId.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.runId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"source.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.source"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GLUE_TRANSFORM_FAILURE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"error.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"stage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TRANSFORM"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"End"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;

    &lt;/span&gt;&lt;span class="nl"&gt;"QuarantineDQFailure"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::lambda:invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FunctionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"quarantine-lambda"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"runId.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.runId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"source.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.source"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DATA_QUALITY_FAILURE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"dqResult.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.dqInvoke.Payload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"stage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DATA_QUALITY"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"End"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Why I prefer Standard over Express here
&lt;/h3&gt;

&lt;p&gt;This pattern tends to include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Glue jobs&lt;/li&gt;
&lt;li&gt;multiple branches&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;audit needs&lt;/li&gt;
&lt;li&gt;potential human investigation/replay&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Step Functions Standard gives me durability and auditability, and AWS documents that Standard and Express have different execution semantics, durations, and integration pattern support (including the &lt;code&gt;.sync&lt;/code&gt; limitation in Express). (&lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/choosing-workflow-type.html" rel="noopener noreferrer"&gt;docs.aws.amazon.com&lt;/a&gt;)&lt;/p&gt;


&lt;h2&gt;
  
  
  Glue transformation implementation (ETL core)
&lt;/h2&gt;

&lt;p&gt;For the transformation layer, I use &lt;strong&gt;AWS Glue&lt;/strong&gt; because it scales well and integrates naturally with S3 and the Data Catalog.&lt;/p&gt;

&lt;p&gt;AWS Glue’s &lt;code&gt;DynamicFrame&lt;/code&gt; is especially useful when incoming schemas are not cleanly stable because records are self-describing and schema inconsistencies can be represented as choice/union types. AWS also documents &lt;code&gt;resolveChoice()&lt;/code&gt; as a recommended approach to handle multi-typed fields. (&lt;a href="https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-crawler-pyspark-extensions-dynamic-frame.html" rel="noopener noreferrer"&gt;docs.aws.amazon.com&lt;/a&gt;)&lt;/p&gt;
&lt;h3&gt;
  
  
  Example Glue job script (PySpark, simplified)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;awsglue.transforms&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;awsglue.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;getResolvedOptions&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.context&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SparkContext&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;awsglue.context&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;GlueContext&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;awsglue.job&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Job&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;awsglue.dynamicframe&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DynamicFrame&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pyspark.sql&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;functions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;

&lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getResolvedOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JOB_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_s3_uri&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output_s3_uri&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dataset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;sc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SparkContext&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;glue_context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;GlueContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;spark&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;glue_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;spark_session&lt;/span&gt;
&lt;span class="n"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;glue_context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JOB_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;input_s3_uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_s3_uri&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;output_s3_uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output_s3_uri&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dataset&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Example assumes CSV; in production I usually parameterize format/options
&lt;/span&gt;&lt;span class="n"&gt;raw_dyf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;glue_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_dynamic_frame&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;connection_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;connection_options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paths&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;input_s3_uri&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recurse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;format_options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;withHeader&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;separator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;quoteChar&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;transformation_ctx&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;raw_dyf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Handle schema drift / ambiguous types
# Example: amount might arrive as string in some files and numeric in others
&lt;/span&gt;&lt;span class="n"&gt;resolved_dyf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw_dyf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolveChoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;specs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cast:double&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cast:string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cast:string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Convert to Spark DataFrame for richer transforms
&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resolved_dyf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDF&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Add operational metadata
&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pipeline_run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dataset_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processed_at_utc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current_timestamp&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Example normalization
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Example partition column
&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;withColumn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ingest_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;F&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;col&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processed_at_utc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

&lt;span class="c1"&gt;# Optional: enforce target schema / select only known columns (ELT-friendly alternative is to keep extras)
&lt;/span&gt;&lt;span class="n"&gt;target_columns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customer_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pipeline_run_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dataset_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;processed_at_utc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ingest_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;df_out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;target_columns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Write curated data
&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;df_out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;append&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parquet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partitionBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ingest_date&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_s3_uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Schema drift handling (the part that usually breaks first)
&lt;/h2&gt;

&lt;p&gt;Schema drift is not one problem. It is several different problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;additive drift&lt;/strong&gt; (new columns appear)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;type drift&lt;/strong&gt; (same column changes type)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;structural drift&lt;/strong&gt; (nested shape changes)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;semantic drift&lt;/strong&gt; (same field name, different meaning)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;contract drift&lt;/strong&gt; (breaking changes without notice)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I recommend treating them differently.&lt;/p&gt;
&lt;h3&gt;
  
  
  My practical schema drift policy
&lt;/h3&gt;
&lt;h4&gt;
  
  
  1) Additive columns (usually safe)
&lt;/h4&gt;

&lt;p&gt;If a producer adds a new column and my downstream consumers can tolerate it, I usually:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;allow the file&lt;/li&gt;
&lt;li&gt;retain the new column in a bronze/validated or semi-curated layer&lt;/li&gt;
&lt;li&gt;update downstream mappings later&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  2) Type drift (common and dangerous)
&lt;/h4&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;amount&lt;/code&gt; was numeric yesterday&lt;/li&gt;
&lt;li&gt;today it arrives as &lt;code&gt;"12.34"&lt;/code&gt; string&lt;/li&gt;
&lt;li&gt;next week it arrives as &lt;code&gt;"N/A"&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where Glue &lt;code&gt;DynamicFrame&lt;/code&gt; + &lt;code&gt;resolveChoice()&lt;/code&gt; helps because I can explicitly coerce or split logic for ambiguous types. AWS documents &lt;code&gt;DynamicFrame&lt;/code&gt; and &lt;code&gt;resolveChoice()&lt;/code&gt; for schema inconsistencies and choice types. (&lt;a href="https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-crawler-pyspark-extensions-dynamic-frame.html" rel="noopener noreferrer"&gt;docs.aws.amazon.com&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;My preference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cast when safe&lt;/li&gt;
&lt;li&gt;preserve raw/original field if coercion may lose information&lt;/li&gt;
&lt;li&gt;quarantine only when the drift breaks contractual expectations for trusted datasets&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;
  
  
  3) Breaking structural changes (often quarantine-worthy)
&lt;/h4&gt;

&lt;p&gt;If a file violates the expected envelope (for example, missing required keys, wrong delimiter, broken nesting), I quarantine it and emit an alert.&lt;/p&gt;
&lt;h4&gt;
  
  
  4) Catalog and schema tracking
&lt;/h4&gt;

&lt;p&gt;I typically track schema evolution using one or both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Glue Data Catalog + crawler/table versioning&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;dataset contract file&lt;/strong&gt; (JSON/YAML) in a config repo or S3&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For higher maturity pipelines, I define a per-dataset schema policy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;allow_additive_columns: true&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;allow_type_coercion: limited&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;strict_required_columns: ["order_id", "event_ts"]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;quarantine_on_breaking_change: true&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Data quality checks (quality gates before trust)
&lt;/h2&gt;

&lt;p&gt;Schema validity is not data quality.&lt;/p&gt;

&lt;p&gt;A file can be structurally valid and still be unusable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;duplicate IDs&lt;/li&gt;
&lt;li&gt;null business keys&lt;/li&gt;
&lt;li&gt;stale data&lt;/li&gt;
&lt;li&gt;impossible values&lt;/li&gt;
&lt;li&gt;code list mismatches&lt;/li&gt;
&lt;li&gt;row counts far outside expected ranges&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I usually place data quality checks &lt;strong&gt;after transformation&lt;/strong&gt; and before I publish the data as trusted/curated.&lt;/p&gt;

&lt;p&gt;AWS Glue Data Quality supports rules via DQDL and can be used in Glue ETL jobs or against Data Catalog datasets. (&lt;a href="https://docs.aws.amazon.com/glue/latest/dg/glue-data-quality.html" rel="noopener noreferrer"&gt;docs.aws.amazon.com&lt;/a&gt;)&lt;/p&gt;
&lt;h3&gt;
  
  
  Example DQDL ruleset (illustrative)
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Rules = [
  RowCount &amp;gt; 0,
  IsComplete "order_id",
  IsUnique "order_id",
  ColumnExists "event_ts",
  ColumnValues "currency" in ["AUD","USD","EUR"],
  ColumnDataType "amount" = "DOUBLE"
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Example DQ evaluator Lambda (simple custom approach)
&lt;/h3&gt;

&lt;p&gt;In some environments, I start with a lightweight custom Lambda check before moving to full Glue DQ. This is especially useful for small curated datasets or when I want fast control over business-specific rules.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pyarrow.parquet&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pq&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;s3fs&lt;/span&gt;

&lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_parse_s3_uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^s3://([^/]+)/?(.*)$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid S3 URI&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;curated_prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curatedS3Prefix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_parse_s3_uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;curated_prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_objects_v2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MaxKeys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;objects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Contents&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.parquet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NO_PARQUET_OUTPUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Example: check first file quickly (for large jobs, use Glue DQ or Athena-based checks)
&lt;/span&gt;    &lt;span class="n"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;S3FileSystem&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;first_uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filesystem&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_pandas&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;required_cols&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;required_cols&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MISSING_COLUMNS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;isnull&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NULL_ORDER_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;astype&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NEGATIVE_AMOUNT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkedFiles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  My recommendation in production
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Start with &lt;strong&gt;a small ruleset&lt;/strong&gt; that reflects actual business criticality&lt;/li&gt;
&lt;li&gt;Separate &lt;strong&gt;hard-fail rules&lt;/strong&gt; (must quarantine) from &lt;strong&gt;warn rules&lt;/strong&gt; (monitor only)&lt;/li&gt;
&lt;li&gt;Persist DQ results to an S3 prefix and emit metrics (pass rate, failed rules, affected rows)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Replay design (safe reprocessing without chaos)
&lt;/h2&gt;

&lt;p&gt;Replay is one of the most under-designed parts of data pipelines.&lt;/p&gt;

&lt;p&gt;If I cannot replay safely, operations become manual and risky very quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I want from replay
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;deterministic input selection&lt;/li&gt;
&lt;li&gt;idempotent processing&lt;/li&gt;
&lt;li&gt;clear audit trail&lt;/li&gt;
&lt;li&gt;no accidental duplicate publishing&lt;/li&gt;
&lt;li&gt;support for single-file and bulk replays&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Replay pattern used here
&lt;/h3&gt;

&lt;p&gt;I use a &lt;strong&gt;replay manifest&lt;/strong&gt; dropped into a replay bucket. That manifest triggers the replay dispatcher Lambda (via EventBridge), which then starts the same Step Functions state machine with replay metadata.&lt;/p&gt;

&lt;p&gt;This gives me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a consistent path for normal and replay runs&lt;/li&gt;
&lt;li&gt;a clear replay artifact I can review/version&lt;/li&gt;
&lt;li&gt;easy automation from scripts or ops tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Example replay manifest
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"replayId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"replay-2026-02-25-orders-fix-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Reprocess files after schema mapping fix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dataset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"files"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"bucket"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-raw-ingest-bucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"landing/orders/2026/02/24/orders_001.csv"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"bucket"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-raw-ingest-bucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"landing/orders/2026/02/24/orders_002.csv"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"force"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dryRun"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example replay dispatcher Lambda (conceptual)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;sfn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stepfunctions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;STATE_MACHINE_ARN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;STATE_MACHINE_ARN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;detail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unquote_plus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;manifest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="n"&gt;replay_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replayId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="n"&gt;run_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;replay_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;04&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bucket&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replay&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replayId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;replay_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;options&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;options&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;validation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="c1"&gt;# In a fuller implementation, I would re-run validation or call the same validator Lambda
&lt;/span&gt;        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_execution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;stateMachineArn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;STATE_MACHINE_ARN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid4&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
            &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;started&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;manifest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;files&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Replay best practices I strongly recommend
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Use a &lt;strong&gt;business idempotency key&lt;/strong&gt; (for example, source file URI + dataset + version)&lt;/li&gt;
&lt;li&gt;Tag replay outputs with replay metadata&lt;/li&gt;
&lt;li&gt;Keep replay behavior explicit (&lt;code&gt;force&lt;/code&gt;, &lt;code&gt;dryRun&lt;/code&gt;, &lt;code&gt;skipIfPublished&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Never mutate raw inputs during replay&lt;/li&gt;
&lt;li&gt;Record replay requests and outcomes in a durable audit log (DynamoDB or S3 append logs)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ETL vs ELT: how I support both in the same architecture
&lt;/h2&gt;

&lt;p&gt;This pattern is intentionally hybrid.&lt;/p&gt;

&lt;h3&gt;
  
  
  ETL mode (transform before publish)
&lt;/h3&gt;

&lt;p&gt;I use this when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;downstream consumers need consistent schema fast&lt;/li&gt;
&lt;li&gt;I want centralized transformation logic&lt;/li&gt;
&lt;li&gt;I need quality gates before data is widely consumed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flow:&lt;br&gt;
&lt;code&gt;validated -&amp;gt; Glue transform -&amp;gt; DQ -&amp;gt; curated&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  ELT mode (load first, transform later)
&lt;/h3&gt;

&lt;p&gt;I use this when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I need rapid ingestion of many datasets&lt;/li&gt;
&lt;li&gt;transforms are evolving quickly&lt;/li&gt;
&lt;li&gt;downstream teams own transformations (Athena/warehouse/dbt/etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Flow:&lt;br&gt;
&lt;code&gt;validated -&amp;gt; catalog/register -&amp;gt; downstream SQL transforms&lt;/code&gt;&lt;br&gt;
with the same validation/quarantine/replay capabilities still in place.&lt;/p&gt;

&lt;p&gt;I can even run both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;land curated “v1” in Glue&lt;/li&gt;
&lt;li&gt;also preserve validated data for future ELT workloads&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Implementation discussion (what matters beyond the happy path)
&lt;/h2&gt;

&lt;h2&gt;
  
  
  1) Idempotency and duplicates
&lt;/h2&gt;

&lt;p&gt;At-least-once delivery can show up in many places (producer retries, EventBridge fan-out, replays, manual operations). I assume duplicates can happen.&lt;/p&gt;

&lt;p&gt;My approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;derive a stable &lt;code&gt;source_object_id&lt;/code&gt; (bucket + key + versionId if enabled)&lt;/li&gt;
&lt;li&gt;persist processing status (optional DynamoDB table)&lt;/li&gt;
&lt;li&gt;make publishing steps idempotent&lt;/li&gt;
&lt;li&gt;partition outputs carefully to avoid duplicate row amplification&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your producers can overwrite keys, enable &lt;strong&gt;S3 versioning&lt;/strong&gt; and include &lt;code&gt;versionId&lt;/code&gt; in your identity model.&lt;/p&gt;




&lt;h2&gt;
  
  
  2) Observability
&lt;/h2&gt;

&lt;p&gt;I treat observability as part of the pipeline design, not an add-on.&lt;/p&gt;

&lt;p&gt;I emit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ValidationPassed&lt;/code&gt;, &lt;code&gt;ValidationFailed&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GlueJobSucceeded&lt;/code&gt;, &lt;code&gt;GlueJobFailed&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DQPassed&lt;/code&gt;, &lt;code&gt;DQFailed&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Quarantined&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ReplayStarted&lt;/code&gt;, &lt;code&gt;ReplayCompleted&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also propagate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;runId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dataset&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;source bucket/key&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;replayId&lt;/code&gt; (if applicable)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes CloudWatch Logs searches and alarms much more useful.&lt;/p&gt;




&lt;h2&gt;
  
  
  3) Security and access control
&lt;/h2&gt;

&lt;p&gt;At minimum, I lock down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;S3 bucket policies to only the required principals&lt;/li&gt;
&lt;li&gt;KMS encryption for all buckets (SSE-KMS)&lt;/li&gt;
&lt;li&gt;Least-privilege IAM roles for Lambda, Step Functions, and Glue&lt;/li&gt;
&lt;li&gt;Explicit separation between raw, validated, curated, and quarantine write permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also recommend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;object ownership settings to avoid ACL surprises&lt;/li&gt;
&lt;li&gt;S3 lifecycle policies for quarantine retention&lt;/li&gt;
&lt;li&gt;VPC configuration for Glue/Lambda only when needed (avoid unnecessary complexity)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4) Cost and performance tuning
&lt;/h2&gt;

&lt;p&gt;This serverless pattern scales well, but costs can drift if I do not tune it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where I watch cost first
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;too many tiny files causing too many executions&lt;/li&gt;
&lt;li&gt;over-frequent Glue job runs for tiny payloads&lt;/li&gt;
&lt;li&gt;verbose DQ checks on every micro-batch&lt;/li&gt;
&lt;li&gt;repeated replay runs without dedupe&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Practical optimizations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;batch small files by dataset/time window before heavy transforms&lt;/li&gt;
&lt;li&gt;use prefix routing to isolate noisy datasets&lt;/li&gt;
&lt;li&gt;right-size Glue workers and concurrency&lt;/li&gt;
&lt;li&gt;use Step Functions Express only for truly short, idempotent, high-volume orchestration (not this &lt;code&gt;.sync&lt;/code&gt; Glue-driven path)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  5) Deployment strategy
&lt;/h2&gt;

&lt;p&gt;I normally deploy this stack with &lt;strong&gt;AWS CDK&lt;/strong&gt;, &lt;strong&gt;SAM&lt;/strong&gt;, or &lt;strong&gt;Terraform&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A clean decomposition is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Foundation stack&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;S3 buckets&lt;/li&gt;
&lt;li&gt;KMS keys&lt;/li&gt;
&lt;li&gt;IAM roles&lt;/li&gt;
&lt;li&gt;EventBridge bus/rules&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Compute stack&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Lambda functions&lt;/li&gt;
&lt;li&gt;Glue jobs&lt;/li&gt;
&lt;li&gt;Step Functions&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Data contracts stack/config&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;dataset configs&lt;/li&gt;
&lt;li&gt;DQ rulesets&lt;/li&gt;
&lt;li&gt;schema policies&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;This reduces blast radius when I update Glue scripts or state machine logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  6) Testing strategy (worth doing early)
&lt;/h2&gt;

&lt;p&gt;I test this pipeline at three levels:&lt;/p&gt;

&lt;h3&gt;
  
  
  Unit tests
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Lambda validation logic&lt;/li&gt;
&lt;li&gt;path parsing&lt;/li&gt;
&lt;li&gt;schema policy decisions&lt;/li&gt;
&lt;li&gt;quarantine payload generation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Integration tests
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;sample files dropped into raw bucket&lt;/li&gt;
&lt;li&gt;expected state machine transitions&lt;/li&gt;
&lt;li&gt;curated output creation&lt;/li&gt;
&lt;li&gt;quarantine routing and reason codes&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Replay tests
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;replay one file&lt;/li&gt;
&lt;li&gt;replay a batch&lt;/li&gt;
&lt;li&gt;replay a previously quarantined file after a code fix&lt;/li&gt;
&lt;li&gt;verify no duplicate publish side effects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I strongly recommend keeping a small set of &lt;strong&gt;golden input files&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;valid file&lt;/li&gt;
&lt;li&gt;missing required column&lt;/li&gt;
&lt;li&gt;type drift file&lt;/li&gt;
&lt;li&gt;malformed file&lt;/li&gt;
&lt;li&gt;duplicate file&lt;/li&gt;
&lt;li&gt;late-arriving file&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Common failure modes and how I handle them
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Failure mode: “Pipeline is green but data is wrong”
&lt;/h3&gt;

&lt;p&gt;This usually means schema validation exists but business DQ does not.&lt;br&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; add DQ gates and track DQ outcomes as first-class metrics.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure mode: “Replays create duplicate records”
&lt;/h3&gt;

&lt;p&gt;This is almost always an idempotency design issue.&lt;br&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; make output writes and publish steps idempotent using stable keys and checkpointing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure mode: “Glue job keeps failing after producer change”
&lt;/h3&gt;

&lt;p&gt;This is often schema drift or delimiter/header changes.&lt;br&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; detect earlier in validation, classify drift type, and quarantine with explicit reason.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure mode: “Debugging is painful”
&lt;/h3&gt;

&lt;p&gt;This happens when correlation IDs are not propagated.&lt;br&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; carry &lt;code&gt;runId&lt;/code&gt; across EventBridge -&amp;gt; Lambda -&amp;gt; Step Functions -&amp;gt; Glue -&amp;gt; DQ -&amp;gt; Notifications.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;This architecture works well because it is not just a data flow. It is an &lt;strong&gt;operational flow&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The key idea is simple: I do not let “data landed in S3” mean “data is trusted.” I explicitly separate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;landing&lt;/li&gt;
&lt;li&gt;validation&lt;/li&gt;
&lt;li&gt;transformation&lt;/li&gt;
&lt;li&gt;quality enforcement&lt;/li&gt;
&lt;li&gt;quarantine&lt;/li&gt;
&lt;li&gt;replay&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation gives me reliability, observability, and a much better developer experience when the inevitable producer-side surprises happen.&lt;/p&gt;

&lt;p&gt;If I were extending this next, I would add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a dataset contract registry (versioned)&lt;/li&gt;
&lt;li&gt;a metadata/status table (DynamoDB)&lt;/li&gt;
&lt;li&gt;automated replay tooling for quarantined objects&lt;/li&gt;
&lt;li&gt;a producer scorecard (quality and schema stability over time)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Amazon S3 EventBridge integration and enablement (S3 User Guide) (&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/enable-event-notifications-eventbridge.html" rel="noopener noreferrer"&gt;docs.aws.amazon.com&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Step Functions workflow type selection (Standard vs Express semantics, durations, integration pattern differences) (&lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/choosing-workflow-type.html" rel="noopener noreferrer"&gt;docs.aws.amazon.com&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Step Functions integration with AWS Glue (&lt;code&gt;startJobRun&lt;/code&gt; / Glue service integration) (&lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/connect-glue.html" rel="noopener noreferrer"&gt;docs.aws.amazon.com&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;AWS Glue Data Quality (DQDL, DeeQu-based managed service) (&lt;a href="https://docs.aws.amazon.com/glue/latest/dg/glue-data-quality.html" rel="noopener noreferrer"&gt;docs.aws.amazon.com&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;AWS Glue &lt;code&gt;DynamicFrame&lt;/code&gt; and schema inconsistency handling (&lt;a href="https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-crawler-pyspark-extensions-dynamic-frame.html" rel="noopener noreferrer"&gt;docs.aws.amazon.com&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;AWS Glue &lt;code&gt;resolveChoice()&lt;/code&gt; guidance for ambiguous types in &lt;code&gt;DynamicFrame&lt;/code&gt; (&lt;a href="https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-crawler-pyspark-transforms-ResolveChoice.html" rel="noopener noreferrer"&gt;docs.aws.amazon.com&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>serverless</category>
      <category>webdev</category>
      <category>programming</category>
      <category>aws</category>
    </item>
    <item>
      <title>Idempotency Architecture for Lambda-Driven Systems on AWS</title>
      <dc:creator>Renaldi</dc:creator>
      <pubDate>Sun, 22 Mar 2026 22:00:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/idempotency-architecture-for-lambda-driven-systems-on-aws-3hp4</link>
      <guid>https://dev.to/aws-builders/idempotency-architecture-for-lambda-driven-systems-on-aws-3hp4</guid>
      <description>&lt;p&gt;Duplicate processing is one of those problems that looks small in a diagram and very expensive in production.&lt;/p&gt;

&lt;p&gt;I have seen teams build clean event-driven and Lambda-based systems, only to run into duplicate charges, duplicated emails, repeated downstream writes, or inconsistent state once retries and redrives start happening. The tricky part is that the system is often behaving &lt;em&gt;as designed&lt;/em&gt;. AWS services are doing what they should do: retrying, buffering, redriving, and favoring delivery durability.&lt;/p&gt;

&lt;p&gt;This is exactly why I consider idempotency architecture one of the most important and most underexplained topics in serverless engineering.&lt;/p&gt;

&lt;p&gt;In this post, I will walk through how I design idempotency for Lambda-driven systems on AWS, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;exactly-once myth&lt;/strong&gt; vs the &lt;strong&gt;at-least-once reality&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;how to choose &lt;strong&gt;idempotency keys&lt;/strong&gt; and &lt;strong&gt;dedupe windows&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;DynamoDB-based idempotency store&lt;/strong&gt; design&lt;/li&gt;
&lt;li&gt;using &lt;strong&gt;AWS Lambda Powertools idempotency utility&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;handling retries from &lt;strong&gt;API Gateway&lt;/strong&gt;, &lt;strong&gt;SQS&lt;/strong&gt;, and &lt;strong&gt;EventBridge&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;an &lt;strong&gt;end-to-end implementation walkthrough&lt;/strong&gt; with code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I will keep this practical and architecture-heavy, so you can adapt it to real workloads instead of only toy examples.&lt;/p&gt;




&lt;h2&gt;
  
  
  The core idea in one sentence
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Idempotency means I can safely process the same logical request more than once and still end up with the same intended outcome.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That does &lt;strong&gt;not&lt;/strong&gt; mean the system only receives the request once. It means my system is resilient when it receives it multiple times.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exactly-once is usually not the right mental model
&lt;/h2&gt;

&lt;p&gt;A lot of production mistakes start with the assumption that a serverless flow will process each request exactly once.&lt;/p&gt;

&lt;p&gt;In practice, in distributed systems, what I usually get is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;at-least-once delivery&lt;/strong&gt; (common with queues/events)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;retries&lt;/strong&gt; at multiple layers (client SDKs, AWS services, Lambda, Step Functions, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;timeouts and ambiguous outcomes&lt;/strong&gt; (did the function finish but the caller timed out?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;redrives / replay&lt;/strong&gt; (DLQ reprocessing, archive replay, manual reruns)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;duplicate submissions&lt;/strong&gt; from clients (double-click, refresh, mobile reconnect)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So instead of trying to force an “exactly-once” guarantee everywhere, I design for:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;at-least-once delivery&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;idempotent handlers&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;safe retries&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;observability around duplicates&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That mental shift makes the architecture much more robust.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where duplicates come from in AWS Lambda-driven systems
&lt;/h2&gt;

&lt;p&gt;Before I show the solution, I like to make the duplicate paths explicit.&lt;/p&gt;

&lt;h3&gt;
  
  
  API Gateway -&amp;gt; Lambda
&lt;/h3&gt;

&lt;p&gt;Duplicates can happen when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the client retries after a timeout&lt;/li&gt;
&lt;li&gt;the network drops after the backend succeeded&lt;/li&gt;
&lt;li&gt;the user taps “Submit” multiple times&lt;/li&gt;
&lt;li&gt;an upstream reverse proxy retries&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  SQS -&amp;gt; Lambda
&lt;/h3&gt;

&lt;p&gt;Duplicates can happen when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lambda fails and the message becomes visible again&lt;/li&gt;
&lt;li&gt;processing exceeds visibility timeout&lt;/li&gt;
&lt;li&gt;partial batch failures cause some records to be retried&lt;/li&gt;
&lt;li&gt;DLQ redrive sends records back later&lt;/li&gt;
&lt;li&gt;standard queues deliver the same message more than once&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  EventBridge -&amp;gt; Lambda
&lt;/h3&gt;

&lt;p&gt;Duplicates can happen when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;target invocation is retried&lt;/li&gt;
&lt;li&gt;a publisher emits semantically duplicate events&lt;/li&gt;
&lt;li&gt;archive/replay is used&lt;/li&gt;
&lt;li&gt;consumers reprocess historical events intentionally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why I architect idempotency at the &lt;strong&gt;business operation&lt;/strong&gt; level, not at just one transport layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  What a good idempotency architecture looks like
&lt;/h2&gt;

&lt;p&gt;At a high level, I want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;stable idempotency key&lt;/strong&gt; for each logical operation&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;dedupe window&lt;/strong&gt; (TTL) appropriate for the business&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;persistence store&lt;/strong&gt; to track processing status and cached results&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;conditional writes&lt;/strong&gt; to prevent concurrent duplicate execution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;response replay&lt;/strong&gt; for safe duplicate requests when appropriate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;clear behavior&lt;/strong&gt; for mismatched payloads using the same key&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Architecture at a glance
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn4lcms01s4mj7ge8rxh8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn4lcms01s4mj7ge8rxh8.png" alt=" " width="800" height="242"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  End-to-end walkthrough (the scenario I will implement)
&lt;/h2&gt;

&lt;p&gt;To make this concrete, I will use a common example:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;“Create payment intent / order processing”&lt;/strong&gt; style operation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Flow
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;A client sends &lt;code&gt;POST /payments&lt;/code&gt; with an &lt;code&gt;Idempotency-Key&lt;/code&gt; header.&lt;/li&gt;
&lt;li&gt;API Gateway invokes Lambda.&lt;/li&gt;
&lt;li&gt;Lambda checks DynamoDB idempotency table.&lt;/li&gt;
&lt;li&gt;If the key is new, Lambda acquires an &lt;strong&gt;IN_PROGRESS lock&lt;/strong&gt; and processes the request.&lt;/li&gt;
&lt;li&gt;Lambda writes the business result (for example, a payment record) and stores the response in the idempotency table with status &lt;strong&gt;COMPLETED&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If the same request is retried, Lambda returns the cached response instead of processing again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then I will extend the same pattern to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SQS worker retries&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;EventBridge consumer retries/replay&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Designing the idempotency key
&lt;/h2&gt;

&lt;p&gt;This is where a lot of teams accidentally introduce bugs.&lt;/p&gt;

&lt;p&gt;A good idempotency key should identify the &lt;strong&gt;logical operation&lt;/strong&gt;, not just the transport envelope.&lt;/p&gt;

&lt;h3&gt;
  
  
  Good key examples
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;payment:{merchant_id}:{client_request_id}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;order-create:{tenant_id}:{cart_checkout_id}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;invoice-email:{invoice_id}:{template_version}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;event:{event_id}&lt;/code&gt; (if the publisher guarantees a stable event ID)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Risky key choices
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;raw timestamp&lt;/li&gt;
&lt;li&gt;Lambda &lt;code&gt;aws_request_id&lt;/code&gt; (changes every invocation)&lt;/li&gt;
&lt;li&gt;SQS &lt;code&gt;receiptHandle&lt;/code&gt; (changes on delivery)&lt;/li&gt;
&lt;li&gt;entire payload serialized without normalization (field order / formatting issues)&lt;/li&gt;
&lt;li&gt;keys that are too broad (cause false dedupe)&lt;/li&gt;
&lt;li&gt;keys that are too narrow (miss duplicates)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  My rule of thumb
&lt;/h3&gt;

&lt;p&gt;I choose a key from &lt;strong&gt;business identity + operation intent&lt;/strong&gt;, and I define it explicitly in the contract.&lt;/p&gt;

&lt;p&gt;For APIs, that often means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;require an &lt;code&gt;Idempotency-Key&lt;/code&gt; header from the client, and&lt;/li&gt;
&lt;li&gt;validate that it maps to a stable request intent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For asynchronous consumers, that often means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use the publisher’s stable &lt;code&gt;eventId&lt;/code&gt;, or&lt;/li&gt;
&lt;li&gt;derive a deterministic business key (for example &lt;code&gt;order_id + action&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Dedupe windows (TTL): how long should I remember a key?
&lt;/h2&gt;

&lt;p&gt;There is no universal value. I set the dedupe window based on business and retry patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  What affects the dedupe window
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;expected client retry duration&lt;/li&gt;
&lt;li&gt;SQS redrive timing and replay operations&lt;/li&gt;
&lt;li&gt;EventBridge replay windows / operational reruns&lt;/li&gt;
&lt;li&gt;whether duplicates after long periods are still harmful&lt;/li&gt;
&lt;li&gt;cost of storing idempotency records&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Practical examples
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API payment creation&lt;/strong&gt;: 24 hours to 7 days&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook ingestion&lt;/strong&gt;: 1 to 7 days (depends on provider retry policy)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batch event processing&lt;/strong&gt;: hours to days&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;high-volume telemetry&lt;/strong&gt;: maybe minutes to hours (if duplicate impact is low)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Important nuance
&lt;/h3&gt;

&lt;p&gt;TTL in DynamoDB is &lt;strong&gt;eventually applied&lt;/strong&gt;, not immediate deletion. I design my logic so:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;expiry_timestamp&lt;/code&gt; is authoritative in code, and&lt;/li&gt;
&lt;li&gt;TTL is the cleanup mechanism.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, I do not depend on the item disappearing exactly at expiry time.&lt;/p&gt;




&lt;h2&gt;
  
  
  DynamoDB-based idempotency store design (recommended pattern)
&lt;/h2&gt;

&lt;p&gt;I prefer DynamoDB for idempotency state in Lambda workloads because it gives me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;low-latency key lookups&lt;/li&gt;
&lt;li&gt;conditional writes&lt;/li&gt;
&lt;li&gt;TTL support&lt;/li&gt;
&lt;li&gt;simple scaling&lt;/li&gt;
&lt;li&gt;good fit for stateless Lambda functions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Table design (single-purpose table)
&lt;/h3&gt;

&lt;p&gt;A dedicated table keeps the pattern easy to reason about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partition key&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;id&lt;/code&gt; (string): the idempotency key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Recommended attributes&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;status&lt;/code&gt; (&lt;code&gt;IN_PROGRESS&lt;/code&gt;, &lt;code&gt;COMPLETED&lt;/code&gt;, optionally &lt;code&gt;EXPIRED&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;expiryTimestamp&lt;/code&gt; (epoch seconds for dedupe window)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;inProgressExpiryTimestamp&lt;/code&gt; (shorter lock expiry to recover from crashed executions)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;payloadHash&lt;/code&gt; (optional but highly recommended)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;responseData&lt;/code&gt; (optional, cached result or safe response envelope)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;createdAt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;updatedAt&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;source&lt;/code&gt; (api / sqs / eventbridge)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;functionName&lt;/code&gt; (optional for ops visibility)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why &lt;code&gt;payloadHash&lt;/code&gt; matters
&lt;/h3&gt;

&lt;p&gt;If a client reuses the same idempotency key with a &lt;strong&gt;different payload&lt;/strong&gt;, I want to detect that and reject it. Otherwise I can accidentally return a cached response for the wrong request.&lt;/p&gt;

&lt;p&gt;This is a subtle but critical best practice.&lt;/p&gt;




&lt;h2&gt;
  
  
  State transitions I use
&lt;/h2&gt;

&lt;p&gt;Here is the lifecycle I generally implement:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No record exists&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Try conditional write -&amp;gt; create &lt;code&gt;IN_PROGRESS&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;IN_PROGRESS&lt;/code&gt; exists&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Another invocation is already processing (or crashed recently)&lt;/li&gt;
&lt;li&gt;Return a retryable outcome or fail fast depending on source&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;COMPLETED&lt;/code&gt; exists and not expired&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Return cached result (or safe ack)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expired&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Treat as a new request (depending on business policy)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That gives me concurrency safety &lt;em&gt;and&lt;/em&gt; retry safety.&lt;/p&gt;




&lt;h2&gt;
  
  
  Infrastructure example (SAM / CloudFormation snippets)
&lt;/h2&gt;

&lt;p&gt;Below is a minimal setup for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lambda function&lt;/li&gt;
&lt;li&gt;DynamoDB idempotency table&lt;/li&gt;
&lt;li&gt;IAM permissions&lt;/li&gt;
&lt;li&gt;env vars for configuration
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;AWSTemplateFormatVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2010-09-09'&lt;/span&gt;
&lt;span class="na"&gt;Transform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless-2016-10-31&lt;/span&gt;
&lt;span class="na"&gt;Description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Idempotent Lambda API example&lt;/span&gt;

&lt;span class="na"&gt;Resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;PaymentsFunction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless::Function&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python3.12&lt;/span&gt;
      &lt;span class="na"&gt;Handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app.lambda_handler&lt;/span&gt;
      &lt;span class="na"&gt;CodeUri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/&lt;/span&gt;
      &lt;span class="na"&gt;MemorySize&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;512&lt;/span&gt;
      &lt;span class="na"&gt;Timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;29&lt;/span&gt;
      &lt;span class="na"&gt;Policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Allow&lt;/span&gt;
              &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:GetItem&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:PutItem&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:UpdateItem&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dynamodb:DeleteItem&lt;/span&gt;
              &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!GetAtt&lt;/span&gt; &lt;span class="s"&gt;IdempotencyTable.Arn&lt;/span&gt;
      &lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;IDEMPOTENCY_TABLE_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;IdempotencyTable&lt;/span&gt;
          &lt;span class="na"&gt;IDEMPOTENCY_EXPIRES_SECONDS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;86400"&lt;/span&gt; &lt;span class="c1"&gt;# 24h&lt;/span&gt;
      &lt;span class="na"&gt;Events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;CreatePaymentApi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Api&lt;/span&gt;
          &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;Path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/payments&lt;/span&gt;
            &lt;span class="na"&gt;Method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;post&lt;/span&gt;

  &lt;span class="na"&gt;IdempotencyTable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::DynamoDB::Table&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;BillingMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PAY_PER_REQUEST&lt;/span&gt;
      &lt;span class="na"&gt;AttributeDefinitions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;id&lt;/span&gt;
          &lt;span class="na"&gt;AttributeType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;S&lt;/span&gt;
      &lt;span class="na"&gt;KeySchema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;id&lt;/span&gt;
          &lt;span class="na"&gt;KeyType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HASH&lt;/span&gt;
      &lt;span class="na"&gt;TimeToLiveSpecification&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;AttributeName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;expiration&lt;/span&gt;
        &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;PointInTimeRecoverySpecification&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;PointInTimeRecoveryEnabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;SSESpecification&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;SSEEnabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="na"&gt;Outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ApiUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/payments"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Notes on the table schema vs Powertools
&lt;/h3&gt;

&lt;p&gt;AWS Lambda Powertools idempotency utility has its own default attribute names and record model. If I use the library, I usually let it manage the item shape and only customize when I truly need to.&lt;/p&gt;

&lt;p&gt;That said, I still think through the conceptual schema above so the team understands what is being stored and why.&lt;/p&gt;




&lt;h2&gt;
  
  
  Implementation option 1 (recommended): AWS Lambda Powertools idempotency utility
&lt;/h2&gt;

&lt;p&gt;If I am using Python Lambda functions, AWS Lambda Powertools is my default choice. It saves me from reimplementing concurrency locks, conditional checks, and record state transitions from scratch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;aws-lambda-powertools[boto3]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  API Gateway example with idempotency (Python)
&lt;/h2&gt;

&lt;p&gt;This example assumes the client sends:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Idempotency-Key&lt;/code&gt; header&lt;/li&gt;
&lt;li&gt;JSON body containing payment details&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;event_key_jmespath&lt;/code&gt; to extract the idempotency key from headers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;payload_validation_jmespath&lt;/code&gt; to validate that key reuse with different payloads is detected&lt;/li&gt;
&lt;li&gt;a DynamoDB persistence layer
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_lambda_powertools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Logger&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_lambda_powertools.utilities.idempotency&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;DynamoDBPersistenceLayer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IdempotencyConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;idempotent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_lambda_powertools.utilities.idempotency.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;IdempotencyValidationError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payments-api&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IDEMPOTENCY_TABLE_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;EXPIRES_SECONDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IDEMPOTENCY_EXPIRES_SECONDS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;86400&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;persistence_layer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DynamoDBPersistenceLayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;IdempotencyConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;# API Gateway/Lambda proxy event header lookup (normalize to lower-case in code if needed)
&lt;/span&gt;    &lt;span class="n"&gt;event_key_jmespath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers.idempotency-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# Payload fields that should remain consistent when reusing the same key
&lt;/span&gt;    &lt;span class="n"&gt;payload_validation_jmespath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;powertools_json(body).[customerId, amount, currency]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;expires_after_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;EXPIRES_SECONDS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;use_local_cache&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_payment_intent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="c1"&gt;# Replace this with your real business logic / external payment call.
&lt;/span&gt;    &lt;span class="c1"&gt;# The critical point is that this function is wrapped with idempotency.
&lt;/span&gt;    &lt;span class="n"&gt;payment_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pay_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;customerId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paymentId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payment_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AUTHORIZED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@idempotent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;persistence_store&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;persistence_layer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customerId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Missing required fields: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_payment_intent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Powertools can persist and return this response for duplicate calls
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@logger.inject_lambda_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;log_event&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="c1"&gt;# Normalize headers so event_key_jmespath is predictable
&lt;/span&gt;    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;process_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;IdempotencyValidationError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Idempotency key was reused with a different request payload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;}),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why this pattern works well
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;If the same request is retried, Powertools returns the stored response.&lt;/li&gt;
&lt;li&gt;If the same key is reused with a different body, I return &lt;code&gt;409 Conflict&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;I avoid duplicate payment authorization for simple retries.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  API client contract (important and often skipped)
&lt;/h2&gt;

&lt;p&gt;Idempotency works much better when the contract is explicit.&lt;/p&gt;

&lt;p&gt;For API producers/clients, I document:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Idempotency-Key&lt;/code&gt; is required for mutating operations (&lt;code&gt;POST&lt;/code&gt;, sometimes &lt;code&gt;PATCH&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;same key + same intent/payload -&amp;gt; safe retry&lt;/li&gt;
&lt;li&gt;same key + different payload -&amp;gt; &lt;code&gt;409 Conflict&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;dedupe window (for example, 24h)&lt;/li&gt;
&lt;li&gt;response replay behavior (cached result may be returned)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That avoids ambiguity across frontend, mobile, and backend teams.&lt;/p&gt;




&lt;h2&gt;
  
  
  Handling retries from SQS (Lambda event source mapping)
&lt;/h2&gt;

&lt;p&gt;SQS is one of the most common places where teams need idempotency but only discover it after duplicates happen.&lt;/p&gt;

&lt;h3&gt;
  
  
  What changes for SQS?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Lambda receives a &lt;strong&gt;batch&lt;/strong&gt; of messages.&lt;/li&gt;
&lt;li&gt;Some messages may succeed while others fail.&lt;/li&gt;
&lt;li&gt;I should use &lt;strong&gt;partial batch response&lt;/strong&gt; so only failed records are retried.&lt;/li&gt;
&lt;li&gt;Each message should still be processed idempotently.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Key design for SQS consumers
&lt;/h3&gt;

&lt;p&gt;I avoid using transient delivery metadata. I prefer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a business key in the message body (for example &lt;code&gt;orderId&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;or an upstream event ID included in the message&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example key:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;order-paid:{orderId}&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;inventory-reservation:{reservationId}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  SQS consumer example (Powertools Batch + per-record idempotency)
&lt;/h2&gt;

&lt;p&gt;Below is a simplified Python example using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Powertools Batch utility for partial batch handling&lt;/li&gt;
&lt;li&gt;Powertools idempotency on the per-record processing function
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_lambda_powertools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Logger&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_lambda_powertools.utilities.batch&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BatchProcessor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EventType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;process_partial_response&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_lambda_powertools.utilities.batch.types&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PartialItemFailureResponse&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_lambda_powertools.utilities.data_classes.sqs_event&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SQSRecord&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_lambda_powertools.utilities.idempotency&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;DynamoDBPersistenceLayer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IdempotencyConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;idempotent_function&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orders-sqs-worker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IDEMPOTENCY_TABLE_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;persistence_layer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DynamoDBPersistenceLayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Here we build the key from the function argument "data"
# (Powertools hashes the configured subset to create an idempotency key)
&lt;/span&gt;&lt;span class="n"&gt;idempotency_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;IdempotencyConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;event_key_jmespath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orderId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload_validation_jmespath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[orderId, action, version]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;expires_after_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# 3 days
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;processor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BatchProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;EventType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SQS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@idempotent_function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_keyword_argument&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;idempotency_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;persistence_store&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;persistence_layer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_order_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="c1"&gt;# Business logic here (must be safe to retry / replay via idempotency)
&lt;/span&gt;    &lt;span class="c1"&gt;# Example: reserve inventory, update status, publish follow-up event, etc.
&lt;/span&gt;    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Processing order event&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;extra&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orderId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orderId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orderId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orderId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SQSRecord&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;process_order_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;PartialItemFailureResponse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;process_partial_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;record_handler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;record_handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Best practices I apply with SQS + idempotency
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use partial batch response&lt;/strong&gt; to avoid retrying already-successful records.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set visibility timeout&lt;/strong&gt; longer than worst-case processing time (or heartbeat/extend strategy).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the idempotency key in the message payload&lt;/strong&gt;, not delivery metadata.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a dedupe window long enough&lt;/strong&gt; to cover retries, DLQ redrive, and operational replay.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Handling retries from EventBridge targets
&lt;/h2&gt;

&lt;p&gt;EventBridge makes event-driven architecture clean, but duplicate-safe consumers are still my responsibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  EventBridge-specific considerations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;EventBridge may retry target delivery.&lt;/li&gt;
&lt;li&gt;Archive/replay can intentionally re-send events.&lt;/li&gt;
&lt;li&gt;Different publishers may emit semantically duplicate events unless the contract is strict.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Key strategy for EventBridge consumers
&lt;/h3&gt;

&lt;p&gt;If the event has a stable &lt;code&gt;id&lt;/code&gt; or domain event ID, I use it. If not, I derive one from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;detail-type&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;source/domain identifier&lt;/li&gt;
&lt;li&gt;business entity ID&lt;/li&gt;
&lt;li&gt;action/version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;eventbridge:{source}:{detailType}:{detail.orderId}:{detail.version}&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  EventBridge consumer example (Python Lambda + Powertools idempotency)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_lambda_powertools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Logger&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_lambda_powertools.utilities.idempotency&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;DynamoDBPersistenceLayer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;IdempotencyConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;idempotent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eventbridge-consumer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IDEMPOTENCY_TABLE_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;persistence_layer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;DynamoDBPersistenceLayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;IdempotencyConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c1"&gt;# Prefer a producer-defined unique event ID if available in detail
&lt;/span&gt;    &lt;span class="n"&gt;event_key_jmespath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail.eventId || id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payload_validation_jmespath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[source, detail-type, detail.orderId, detail.version]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;expires_after_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@idempotent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;persistence_store&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;persistence_layer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;detail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;detail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Handling EventBridge event&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;extra&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eventId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eventId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orderId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orderId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Execute your domain logic here
&lt;/span&gt;    &lt;span class="c1"&gt;# Example: update read model, trigger notification, call downstream API, etc.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;handled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orderId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orderId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;handle_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  When I implement idempotency manually (without Powertools)
&lt;/h2&gt;

&lt;p&gt;Powertools is my default, but sometimes I implement manually when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I need a custom record schema shared across services/languages&lt;/li&gt;
&lt;li&gt;I need custom conflict behavior beyond the library defaults&lt;/li&gt;
&lt;li&gt;I am in a language/runtime where I am standardizing a platform abstraction&lt;/li&gt;
&lt;li&gt;I want explicit control over lock and result update semantics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key principle stays the same: &lt;strong&gt;use DynamoDB conditional writes&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Manual DynamoDB idempotency pattern (Python + boto3)
&lt;/h2&gt;

&lt;p&gt;The pattern below shows the core idea:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Try to write &lt;code&gt;IN_PROGRESS&lt;/code&gt; with &lt;code&gt;attribute_not_exists(id)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If successful, execute business logic&lt;/li&gt;
&lt;li&gt;Update item to &lt;code&gt;COMPLETED&lt;/code&gt; and store result&lt;/li&gt;
&lt;li&gt;If conditional check fails, inspect existing item and decide
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;botocore.exceptions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ClientError&lt;/span&gt;

&lt;span class="n"&gt;dynamodb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dynamodb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IdempotencyTable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sha256_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;acquire_lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lock_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IN_PROGRESS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payloadHash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payload_hash&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expiration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ttl_seconds&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inProgressExpiryTimestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;lock_seconds&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;createdAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;updatedAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ConditionExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attribute_not_exists(id)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acquired&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ClientError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ConditionalCheckFailedException&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acquired&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
        &lt;span class="n"&gt;ConsistentRead&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Item&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mark_completed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response_obj&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;TableName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;
        &lt;span class="n"&gt;UpdateExpression&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET #s = :completed, responseData = :resp, updatedAt = :now&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ExpressionAttributeNames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;ExpressionAttributeValues&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;COMPLETED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:resp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_obj&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:now&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;idem_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;idempotency-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;payload_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sha256_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customerId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;customerId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;acquire_lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idem_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload_hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;acquired&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idem_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Rare race / eventual state issue; safe to retry
&lt;/span&gt;            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payloadHash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;payload_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Idempotency key payload mismatch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})}&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;COMPLETED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;responseData&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IN_PROGRESS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# For API workflows you might return 409 or 425-style retry signal (implementation-specific)
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Request is already in progress&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})}&lt;/span&gt;

    &lt;span class="c1"&gt;# Execute business logic after lock acquired
&lt;/span&gt;    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paymentId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pay_123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AUTHORIZED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;mark_completed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idem_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Manual pattern caveats
&lt;/h3&gt;

&lt;p&gt;If I go manual, I also need to think about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;expired &lt;code&gt;IN_PROGRESS&lt;/code&gt; lock recovery&lt;/li&gt;
&lt;li&gt;exception handling and safe cleanup&lt;/li&gt;
&lt;li&gt;serialization of cached responses&lt;/li&gt;
&lt;li&gt;metrics for duplicate hits vs fresh requests&lt;/li&gt;
&lt;li&gt;consistent behavior across all event sources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is exactly why Powertools is usually the better default.&lt;/p&gt;




&lt;h2&gt;
  
  
  End-to-end implementation discussion (how I wire this in production)
&lt;/h2&gt;

&lt;p&gt;This is the part I care about most in architecture reviews: not just the code, but where idempotency sits in the overall system.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Idempotency belongs close to the handler boundary
&lt;/h3&gt;

&lt;p&gt;I usually apply idempotency at the Lambda entry point (or record handler for batches), before business side effects occur.&lt;/p&gt;

&lt;p&gt;Why:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it prevents duplicate external calls&lt;/li&gt;
&lt;li&gt;it keeps the protection broad&lt;/li&gt;
&lt;li&gt;it makes retries safe by default&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2) I still design downstream writes carefully
&lt;/h3&gt;

&lt;p&gt;Idempotency at the Lambda layer is great, but if the function can partially succeed before crashing, I also check downstream safety:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unique constraints in relational DBs&lt;/li&gt;
&lt;li&gt;conditional writes in DynamoDB&lt;/li&gt;
&lt;li&gt;provider-side idempotency support for payment APIs or webhooks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think in layers, not in a single magic switch.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) I define duplicate behavior per source
&lt;/h3&gt;

&lt;p&gt;The response to a duplicate is not always the same.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API Gateway&lt;/strong&gt;: return cached success response (best UX)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQS&lt;/strong&gt;: ack success for already-processed message, avoid poison-loop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EventBridge&lt;/strong&gt;: safely no-op or return success after dedupe&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4) I separate “idempotency key” and “correlation ID”
&lt;/h3&gt;

&lt;p&gt;They are related but not identical.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Correlation ID&lt;/strong&gt; -&amp;gt; tracing/observability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency key&lt;/strong&gt; -&amp;gt; duplicate suppression for a specific operation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes they can be the same, but I do not assume that.&lt;/p&gt;




&lt;h2&gt;
  
  
  Handling edge cases and failure modes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Edge case 1: Same key, different payload
&lt;/h3&gt;

&lt;p&gt;This should be treated as a contract violation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best practice:&lt;/strong&gt; return &lt;code&gt;409 Conflict&lt;/code&gt; (or equivalent domain error) and log it loudly.&lt;/p&gt;

&lt;p&gt;Why I do this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;protects clients from accidental misuse&lt;/li&gt;
&lt;li&gt;prevents serving incorrect cached results&lt;/li&gt;
&lt;li&gt;surfaces integration bugs early&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Edge case 2: Function times out after making a side effect
&lt;/h3&gt;

&lt;p&gt;This is the classic ambiguous outcome problem.&lt;/p&gt;

&lt;p&gt;Idempotency helps, but only if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the side effect can be detected or safely repeated, and/or&lt;/li&gt;
&lt;li&gt;the result gets persisted before timeout&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Best practices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep timeouts realistic&lt;/li&gt;
&lt;li&gt;use downstream idempotency where available&lt;/li&gt;
&lt;li&gt;break long operations into Step Functions when needed&lt;/li&gt;
&lt;li&gt;persist progress checkpoints for multi-step work&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Edge case 3: &lt;code&gt;IN_PROGRESS&lt;/code&gt; records stuck after crashes
&lt;/h3&gt;

&lt;p&gt;If a function crashes after acquiring the lock, duplicates may keep seeing &lt;code&gt;IN_PROGRESS&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Best practices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use an &lt;strong&gt;in-progress lock expiry&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;make retries back off&lt;/li&gt;
&lt;li&gt;alert on sustained &lt;code&gt;IN_PROGRESS&lt;/code&gt; accumulation&lt;/li&gt;
&lt;li&gt;evaluate whether the operation is safe to re-attempt after lock expiry&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Edge case 4: Replay and backfill
&lt;/h3&gt;

&lt;p&gt;Operational replay is common and healthy. I design for it intentionally.&lt;/p&gt;

&lt;p&gt;Best practices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;choose a dedupe window that matches replay expectations&lt;/li&gt;
&lt;li&gt;if replay should &lt;em&gt;re-run&lt;/em&gt; effects, use a different idempotency namespace/version&lt;/li&gt;
&lt;li&gt;document replay semantics for ops teams&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;normal key: &lt;code&gt;invoice-email:{invoiceId}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;forced replay key namespace: &lt;code&gt;invoice-email:replay:{jobId}:{invoiceId}&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Observability: what I monitor for idempotency
&lt;/h2&gt;

&lt;p&gt;Idempotency is not just a code concern. I want to know how often it is being exercised and whether it is hiding a deeper issue.&lt;/p&gt;

&lt;h3&gt;
  
  
  Metrics I like to emit
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;IdempotencyFreshRequests&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IdempotencyDuplicateHits&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IdempotencyInProgressConflicts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IdempotencyValidationConflicts&lt;/code&gt; (same key, different payload)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IdempotencyStoreErrors&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;handler latency split by fresh vs duplicate&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Logs I always include
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;idempotency key (or redacted/hash if sensitive)&lt;/li&gt;
&lt;li&gt;source (&lt;code&gt;api&lt;/code&gt;, &lt;code&gt;sqs&lt;/code&gt;, &lt;code&gt;eventbridge&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;dedupe outcome (&lt;code&gt;fresh&lt;/code&gt;, &lt;code&gt;duplicate_completed&lt;/code&gt;, &lt;code&gt;duplicate_in_progress&lt;/code&gt;, &lt;code&gt;validation_mismatch&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;business identifier (&lt;code&gt;orderId&lt;/code&gt;, &lt;code&gt;paymentId&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes incident triage much faster.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical best practices checklist (the part I use in reviews)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Key selection
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Key represents a &lt;strong&gt;business operation&lt;/strong&gt;, not transport metadata&lt;/li&gt;
&lt;li&gt;[ ] Key is stable across retries/replays&lt;/li&gt;
&lt;li&gt;[ ] Key cardinality is high enough to avoid false collisions&lt;/li&gt;
&lt;li&gt;[ ] Payload mismatch with same key is detected&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Dedupe window
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] TTL matches retry + redrive + replay realities&lt;/li&gt;
&lt;li&gt;[ ] Expiry is checked in code (not only by DynamoDB TTL cleanup)&lt;/li&gt;
&lt;li&gt;[ ] Window is documented in API/event contract&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Store design
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] DynamoDB conditional write used for first writer wins&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;IN_PROGRESS&lt;/code&gt; and &lt;code&gt;COMPLETED&lt;/code&gt; states are handled explicitly&lt;/li&gt;
&lt;li&gt;[ ] Cached response/ack strategy is defined&lt;/li&gt;
&lt;li&gt;[ ] Encryption, backups/PITR, and least privilege are configured&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Source-specific behavior
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] API duplicates return deterministic response&lt;/li&gt;
&lt;li&gt;[ ] SQS uses partial batch response&lt;/li&gt;
&lt;li&gt;[ ] EventBridge consumer supports replay safely&lt;/li&gt;
&lt;li&gt;[ ] Redrive/replay semantics are documented for ops&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Operations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Metrics and alarms exist for duplicate spikes and store failures&lt;/li&gt;
&lt;li&gt;[ ] Logs include dedupe outcomes and business IDs&lt;/li&gt;
&lt;li&gt;[ ] Runbooks cover stale &lt;code&gt;IN_PROGRESS&lt;/code&gt; records and replay scenarios&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Common mistakes I see (and how I avoid them)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Mistake 1: Using Lambda &lt;code&gt;requestId&lt;/code&gt; as the idempotency key
&lt;/h3&gt;

&lt;p&gt;That only identifies the invocation, not the logical request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; use business operation identity or client-provided idempotency key.&lt;/p&gt;




&lt;h3&gt;
  
  
  Mistake 2: Assuming FIFO queue means I do not need idempotency
&lt;/h3&gt;

&lt;p&gt;FIFO helps with ordering and deduplication windows, but it does not replace end-to-end idempotency for all side effects and replay paths.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; still make the consumer idempotent.&lt;/p&gt;




&lt;h3&gt;
  
  
  Mistake 3: Dedupe only at the API layer
&lt;/h3&gt;

&lt;p&gt;Then an async worker downstream duplicates the side effect anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; apply idempotency where side effects happen, especially in SQS/EventBridge consumers.&lt;/p&gt;




&lt;h3&gt;
  
  
  Mistake 4: No payload validation on key reuse
&lt;/h3&gt;

&lt;p&gt;This can return the wrong cached response and create hidden data integrity issues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; validate a stable subset of the payload with the idempotency key.&lt;/p&gt;




&lt;h3&gt;
  
  
  Mistake 5: Too-short TTL
&lt;/h3&gt;

&lt;p&gt;The key expires before retries/redrives finish, so duplicates sneak through.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; pick TTL based on actual operational timelines, not guesswork.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;If I had to summarize production-grade idempotency architecture in one line, it would be this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design for duplicate delivery as normal behavior, then make your Lambda handlers safe, deterministic, and observable.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AWS gives us excellent building blocks for this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lambda&lt;/li&gt;
&lt;li&gt;SQS / EventBridge / API Gateway&lt;/li&gt;
&lt;li&gt;DynamoDB conditional writes&lt;/li&gt;
&lt;li&gt;AWS Lambda Powertools idempotency utility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I combine them intentionally, retries stop being scary and start being a reliability feature instead of a data integrity risk.&lt;/p&gt;

&lt;p&gt;If you are building Lambda-driven systems that write to money, inventory, notifications, or customer state, idempotency is not optional. It is a core part of the architecture.&lt;/p&gt;

&lt;p&gt;That placement helps readers understand the flow before diving into implementation details.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AWS Lambda Powertools (Python) documentation&lt;/li&gt;
&lt;li&gt;AWS Lambda developer guide&lt;/li&gt;
&lt;li&gt;Amazon SQS developer guide (Lambda event source mappings / retries / partial batch response)&lt;/li&gt;
&lt;li&gt;Amazon EventBridge documentation (retries, targets, replay/archive)&lt;/li&gt;
&lt;li&gt;Amazon DynamoDB documentation (conditional writes, TTL, PITR)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>webdev</category>
      <category>eventdriven</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Building a Serverless Command Bus / Workflow Backbone for Microservices</title>
      <dc:creator>Renaldi</dc:creator>
      <pubDate>Sun, 15 Mar 2026 22:00:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/building-a-serverless-command-bus-workflow-backbone-for-microservices-599b</link>
      <guid>https://dev.to/aws-builders/building-a-serverless-command-bus-workflow-backbone-for-microservices-599b</guid>
      <description>&lt;p&gt;When I design microservice platforms on AWS, one of the recurring problems I see is not “how do I trigger a Lambda,” but rather “how do I keep a growing set of services coordinated, observable, and governed without turning everything into tight coupling?”&lt;/p&gt;

&lt;p&gt;That is where a &lt;strong&gt;serverless command bus plus workflow backbone&lt;/strong&gt; becomes a powerful pattern.&lt;/p&gt;

&lt;p&gt;In this post, I will walk through how I build this in practice using &lt;strong&gt;Amazon EventBridge&lt;/strong&gt; and &lt;strong&gt;AWS Step Functions&lt;/strong&gt;, and I will cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Commands vs events&lt;/li&gt;
&lt;li&gt;EventBridge vs direct invoke&lt;/li&gt;
&lt;li&gt;Step Functions for orchestrated flows&lt;/li&gt;
&lt;li&gt;Auditability and tracing&lt;/li&gt;
&lt;li&gt;Contract evolution&lt;/li&gt;
&lt;li&gt;Governance and domain boundaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I will also include an &lt;strong&gt;end-to-end implementation walkthrough&lt;/strong&gt;, code samples, and architecture guidance so this is not just conceptual.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this pattern is worth building
&lt;/h2&gt;

&lt;p&gt;As systems grow, teams usually end up with some mixture of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;synchronous service-to-service calls&lt;/li&gt;
&lt;li&gt;ad hoc retries&lt;/li&gt;
&lt;li&gt;duplicated orchestration logic&lt;/li&gt;
&lt;li&gt;inconsistent logging/correlation IDs&lt;/li&gt;
&lt;li&gt;unclear ownership of contracts&lt;/li&gt;
&lt;li&gt;event naming drift&lt;/li&gt;
&lt;li&gt;“mystery failures” across multiple services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A command bus/workflow backbone gives you a more intentional architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Commands&lt;/strong&gt; are routed to the right handler or orchestrator&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflows&lt;/strong&gt; coordinate multi-step business actions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Events&lt;/strong&gt; are published as business facts for downstream consumers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tracing/audit&lt;/strong&gt; become standard instead of optional&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contract governance&lt;/strong&gt; is enforced at boundaries rather than after incidents&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is especially effective for domains like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;order fulfillment&lt;/li&gt;
&lt;li&gt;onboarding/KYC&lt;/li&gt;
&lt;li&gt;claims processing&lt;/li&gt;
&lt;li&gt;content moderation pipelines&lt;/li&gt;
&lt;li&gt;payment settlement&lt;/li&gt;
&lt;li&gt;inventory reservation&lt;/li&gt;
&lt;li&gt;fulfillment and notification fan-out&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I mean by “Command Bus” in a serverless context
&lt;/h2&gt;

&lt;p&gt;In classic application architecture, a command bus is often an in-process dispatching mechanism. In distributed systems, I use the term a bit differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;command&lt;/strong&gt; represents an intent to perform a business action&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;command bus&lt;/strong&gt; is the routing layer that accepts commands and sends them to the correct handler/orchestrator&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;workflow backbone&lt;/strong&gt; is the orchestration mechanism that executes multi-step business flows and emits events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On AWS, a practical implementation is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API Gateway + Lambda&lt;/strong&gt; for ingress&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EventBridge&lt;/strong&gt; as the command routing fabric&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Step Functions&lt;/strong&gt; as the workflow engine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EventBridge (domain event bus)&lt;/strong&gt; for business events&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB / S3 / CloudWatch / X-Ray / OTEL&lt;/strong&gt; for auditability and observability&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Commands vs Events (and why this distinction matters)
&lt;/h2&gt;

&lt;p&gt;This is the first thing I standardize with teams, because it affects naming, routing, retries, and ownership.&lt;/p&gt;

&lt;h3&gt;
  
  
  Commands
&lt;/h3&gt;

&lt;p&gt;A command is an instruction or request to do something.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CreateOrder&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ApproveLoanApplication&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ReserveInventory&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;InitiateRefund&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Commands are typically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;intent-driven&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;often addressed to &lt;strong&gt;one owning domain/service&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;can be &lt;strong&gt;accepted/rejected&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;may need &lt;strong&gt;validation and idempotency&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;often require &lt;strong&gt;stronger governance&lt;/strong&gt; at the boundary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A command implies someone is asking for work to be done.&lt;/p&gt;

&lt;h3&gt;
  
  
  Events
&lt;/h3&gt;

&lt;p&gt;An event is a fact that something already happened.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;OrderCreated&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;InventoryReserved&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PaymentAuthorized&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RefundInitiated&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Events are typically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;past-tense facts&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;can have &lt;strong&gt;many subscribers&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;should be treated as &lt;strong&gt;immutable&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;support &lt;strong&gt;decoupled reactions&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;are excellent for &lt;strong&gt;read models, analytics, notifications, integrations&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An event should not be phrased as an instruction.&lt;/p&gt;

&lt;h3&gt;
  
  
  A rule I use with teams
&lt;/h3&gt;

&lt;p&gt;If the payload is saying, “please do this,” it is a &lt;strong&gt;command&lt;/strong&gt;.&lt;br&gt;&lt;br&gt;
If the payload is saying, “this happened,” it is an &lt;strong&gt;event&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That sounds simple, but it prevents a lot of design drift.&lt;/p&gt;


&lt;h2&gt;
  
  
  EventBridge vs direct invoke (when to use each)
&lt;/h2&gt;

&lt;p&gt;I do not recommend forcing everything through a bus. Some interactions should remain direct and synchronous.&lt;/p&gt;
&lt;h3&gt;
  
  
  Use EventBridge when you want
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;loose coupling between producers and consumers&lt;/li&gt;
&lt;li&gt;routing based on message metadata&lt;/li&gt;
&lt;li&gt;asynchronous execution&lt;/li&gt;
&lt;li&gt;fan-out to multiple consumers&lt;/li&gt;
&lt;li&gt;cross-account integration patterns&lt;/li&gt;
&lt;li&gt;central governance on message flow&lt;/li&gt;
&lt;li&gt;easy addition of new downstream consumers later&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Use direct invoke (HTTP/gRPC/Lambda invoke) when you need
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;immediate response to the caller&lt;/li&gt;
&lt;li&gt;low latency request/response semantics&lt;/li&gt;
&lt;li&gt;tight transactional dependencies (within reason)&lt;/li&gt;
&lt;li&gt;user-facing UX that cannot tolerate async polling/callbacks&lt;/li&gt;
&lt;li&gt;simple service composition where orchestration overhead is not justified&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  My practical guidance
&lt;/h3&gt;

&lt;p&gt;I usually use a &lt;strong&gt;hybrid&lt;/strong&gt; model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direct invoke&lt;/strong&gt; for request/response reads and tightly-coupled synchronous operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Command bus + Step Functions&lt;/strong&gt; for business workflows and long-running actions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Event bus&lt;/strong&gt; for domain events and downstream side effects&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives you the best of both worlds without ideological overreach.&lt;/p&gt;


&lt;h2&gt;
  
  
  Reference Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffxumnxndus9urwvskf9c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffxumnxndus9urwvskf9c.png" alt=" " width="800" height="293"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Architecture overview
&lt;/h3&gt;

&lt;p&gt;At a high level, the flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client sends a command (e.g., &lt;code&gt;CreateOrder&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;API layer validates, stamps correlation metadata, applies idempotency&lt;/li&gt;
&lt;li&gt;Command is published to a &lt;strong&gt;Command Bus&lt;/strong&gt; (EventBridge)&lt;/li&gt;
&lt;li&gt;Rule routes command to a &lt;strong&gt;Step Functions&lt;/strong&gt; state machine&lt;/li&gt;
&lt;li&gt;Workflow invokes domain steps (fraud, payment, inventory, order write)&lt;/li&gt;
&lt;li&gt;Workflow emits &lt;strong&gt;domain events&lt;/strong&gt; (e.g., &lt;code&gt;OrderCreated&lt;/code&gt; or &lt;code&gt;OrderRejected&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Event consumers react independently (notifications, analytics, CRM, projections)&lt;/li&gt;
&lt;li&gt;Audit and tracing data is captured throughout&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This separates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;request intent ingestion&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;workflow orchestration&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;event publication&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;consumer reactions&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;governance/observability&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  End-to-End Walkthrough: &lt;code&gt;CreateOrder&lt;/code&gt; command
&lt;/h2&gt;

&lt;p&gt;To make this concrete, I will use an order workflow.&lt;/p&gt;
&lt;h3&gt;
  
  
  Business scenario
&lt;/h3&gt;

&lt;p&gt;A client submits an order. The system needs to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Validate the request and check idempotency&lt;/li&gt;
&lt;li&gt;Run a fraud check&lt;/li&gt;
&lt;li&gt;Authorize payment&lt;/li&gt;
&lt;li&gt;Reserve inventory&lt;/li&gt;
&lt;li&gt;Persist the order&lt;/li&gt;
&lt;li&gt;Publish the resulting business event(s)&lt;/li&gt;
&lt;li&gt;Notify downstream systems&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Step 1: Client submits a command
&lt;/h3&gt;

&lt;p&gt;The client calls an API endpoint such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;POST /orders&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Internally, I treat this as a &lt;code&gt;CreateOrderCommand&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The API layer does not directly orchestrate business logic. Its job is to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authenticate/authorize&lt;/li&gt;
&lt;li&gt;validate payload shape&lt;/li&gt;
&lt;li&gt;assign &lt;code&gt;commandId&lt;/code&gt; and &lt;code&gt;correlationId&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;apply idempotency safeguards&lt;/li&gt;
&lt;li&gt;publish the command to the bus&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Step 2: Command is routed on the command bus
&lt;/h3&gt;

&lt;p&gt;The command lands on an EventBridge bus (logical “command bus”).&lt;/p&gt;

&lt;p&gt;A rule matches:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;source&lt;/li&gt;
&lt;li&gt;detail-type&lt;/li&gt;
&lt;li&gt;possibly domain/version metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That rule starts a Step Functions state machine for &lt;code&gt;CreateOrder&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This gives me routing flexibility without wiring the producer directly to the orchestrator.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: Step Functions orchestrates the business flow
&lt;/h3&gt;

&lt;p&gt;The state machine runs each step with retries and error handling.&lt;/p&gt;

&lt;p&gt;Typical flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;FraudCheck&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AuthorizePayment&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ReserveInventory&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PersistOrder&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PublishOrderCreatedEvent&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any step fails:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;compensate if needed (e.g., release reservation / void payment)&lt;/li&gt;
&lt;li&gt;emit failure event (e.g., &lt;code&gt;OrderRejected&lt;/code&gt;, &lt;code&gt;PaymentFailed&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;write audit trail&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Step 4: Domain events are published
&lt;/h3&gt;

&lt;p&gt;Once the workflow succeeds or fails, the workflow emits a domain event to an EventBridge &lt;strong&gt;event bus&lt;/strong&gt; (logical “domain event bus”).&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;OrderCreated&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OrderRejected&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PaymentFailed&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These events can fan out to multiple consumers without changing the workflow code.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 5: Consumers react independently
&lt;/h3&gt;

&lt;p&gt;Downstream services subscribe and act:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Notifications send email/SMS/push&lt;/li&gt;
&lt;li&gt;Analytics ingests event into a lake/warehouse&lt;/li&gt;
&lt;li&gt;CRM updates lifecycle state&lt;/li&gt;
&lt;li&gt;Projection service updates a query/read model&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these consumers need to be in the critical path of order creation.&lt;/p&gt;


&lt;h2&gt;
  
  
  Implementation discussion (practical, not just theoretical)
&lt;/h2&gt;

&lt;p&gt;Below is a concrete implementation shape I use and recommend.&lt;/p&gt;


&lt;h2&gt;
  
  
  Message contract design (command and event envelopes)
&lt;/h2&gt;

&lt;p&gt;I strongly recommend standardizing a message envelope early. It pays off in observability, replay, governance, and contract evolution.&lt;/p&gt;
&lt;h3&gt;
  
  
  Command envelope example
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"meta"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"messageType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"commandName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CreateOrder"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"schemaVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"commandId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cmd_01HSYJQF3A6M0R6G0J7VY7A1J9"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"correlationId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"corr_2f1d3a07-3d89-4a62-9f6c-9f29f4e96f7e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"causationId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"occurredAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-25T09:20:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tenantId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tenant-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"initiator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user-123"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"routing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders.workflow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"normal"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-100045"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CUST-9001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AUD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sku"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SKU-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"unitPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;49.95&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"sku"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SKU-999"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"qty"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"unitPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;19.95&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"paymentMethodToken"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tok_abc123"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Event envelope example
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"meta"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"messageType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"eventName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OrderCreated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"schemaVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"eventId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"evt_01HSYK2GQKCNZC3Q0Z6R3T9M7K"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"correlationId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"corr_2f1d3a07-3d89-4a62-9f6c-9f29f4e96f7e"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"causationId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cmd_01HSYJQF3A6M0R6G0J7VY7A1J9"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"occurredAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-25T09:20:03Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"producer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders.workflow"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-100045"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-100045"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CREATED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"paymentStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AUTHORIZED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"inventoryStatus"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RESERVED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"totalAmount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;119.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AUD"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Why I standardize envelopes
&lt;/h3&gt;

&lt;p&gt;This makes it much easier to implement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;trace propagation&lt;/li&gt;
&lt;li&gt;replay tooling&lt;/li&gt;
&lt;li&gt;audit logging&lt;/li&gt;
&lt;li&gt;schema validation&lt;/li&gt;
&lt;li&gt;version migration&lt;/li&gt;
&lt;li&gt;tenant-aware governance&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  API ingress (Lambda) for command intake
&lt;/h2&gt;

&lt;p&gt;This example shows a command-ingress Lambda that validates input, applies idempotency, and publishes to EventBridge.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/handlers/create-order-command.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyEventV2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyStructuredResultV2&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-lambda&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;EventBridgeClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PutEventsCommand&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aws-sdk/client-eventbridge&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DynamoDBClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aws-sdk/client-dynamodb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DynamoDBDocumentClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PutCommand&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aws-sdk/lib-dynamodb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;randomUUID&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EventBridgeClient&lt;/span&gt;&lt;span class="p"&gt;({});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DynamoDBDocumentClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DynamoDBClient&lt;/span&gt;&lt;span class="p"&gt;({}));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;COMMAND_BUS_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;COMMAND_BUS_NAME&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;IDEMPOTENCY_TABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;IDEMPOTENCY_TABLE&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;COMMAND_SOURCE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;com.example.orders.api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyEventV2&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;APIGatewayProxyStructuredResultV2&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;correlationId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-correlation-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s2"&gt;`corr_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;idempotency-key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s2"&gt;`idem_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Missing request body&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Minimal shape validation (use JSON Schema in production)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Invalid order payload&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Idempotency write (fail if key already exists)&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PutCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IDEMPOTENCY_TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
          &lt;span class="nx"&gt;correlationId&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;ConditionExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;attribute_not_exists(pk)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ConditionalCheckFailedException&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Duplicate request&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;correlationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;idempotencyKey&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commandId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`cmd_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commandEnvelope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;messageType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;command&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;commandName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CreateOrder&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;schemaVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1.0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;commandId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;correlationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;causationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;occurredAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tenant-001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;initiator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user-123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;routing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;orders&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;orders.workflow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;normal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;eb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PutEventsCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;Entries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;EventBusName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;COMMAND_BUS_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;Source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;COMMAND_SOURCE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;DetailType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CreateOrderCommand&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="na"&gt;Detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;commandEnvelope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-correlation-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;correlationId&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Command accepted&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;commandId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;correlationId&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A note on API response semantics
&lt;/h3&gt;

&lt;p&gt;I usually return &lt;strong&gt;&lt;code&gt;202 Accepted&lt;/code&gt;&lt;/strong&gt; for async workflows. That is clearer than pretending the business operation is already complete.&lt;/p&gt;




&lt;h2&gt;
  
  
  EventBridge routing for the command bus
&lt;/h2&gt;

&lt;p&gt;I use EventBridge rules to map command types to orchestrators or handlers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Routing rule concept
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;source = com.example.orders.api&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;detail-type = CreateOrderCommand&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;target = Step Functions state machine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a clean handoff between ingress and orchestration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step Functions for orchestrated flows
&lt;/h2&gt;

&lt;p&gt;This is the backbone of the pattern for multi-step business actions.&lt;/p&gt;

&lt;p&gt;Step Functions gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retries/backoff&lt;/li&gt;
&lt;li&gt;explicit error branches&lt;/li&gt;
&lt;li&gt;timeouts&lt;/li&gt;
&lt;li&gt;compensation flows&lt;/li&gt;
&lt;li&gt;state tracking&lt;/li&gt;
&lt;li&gt;execution history&lt;/li&gt;
&lt;li&gt;integration with Lambda and AWS services&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Example state machine (Amazon States Language)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Comment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CreateOrder workflow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"StartAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FraudCheck"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"States"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"FraudCheck"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::lambda:invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FunctionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${FraudCheckFnArn}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Payload.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ResultSelector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"fraud.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.Payload"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ResultPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.fraudResult"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Retry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ErrorEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Lambda.ServiceException"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Lambda.SdkClientException"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"States.TaskFailed"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"IntervalSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"BackoffRate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"MaxAttempts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FraudDecision"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"FraudDecision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Choice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Choices"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Variable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.fraudResult.fraud.approved"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"BooleanEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AuthorizePayment"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PublishOrderRejected"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"AuthorizePayment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::lambda:invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FunctionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${AuthorizePaymentFnArn}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Payload.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ResultSelector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"payment.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.Payload"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ResultPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.paymentResult"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Catch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ErrorEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"States.ALL"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PublishPaymentFailed"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ReserveInventory"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ReserveInventory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::lambda:invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FunctionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${ReserveInventoryFnArn}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Payload.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ResultSelector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"inventory.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.Payload"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ResultPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.inventoryResult"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Catch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ErrorEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"States.ALL"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PublishOrderRejected"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PersistOrder"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PersistOrder"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::lambda:invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FunctionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${PersistOrderFnArn}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Payload.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ResultSelector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"order.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.Payload"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ResultPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.orderResult"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PublishOrderCreated"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PublishOrderCreated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::events:putEvents"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Entries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"EventBusName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${DomainEventBusName}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.example.orders.workflow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"DetailType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OrderCreated"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Detail.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"States.JsonToString($.orderResult.order.eventEnvelope)"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"End"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PublishPaymentFailed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::events:putEvents"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Entries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"EventBusName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${DomainEventBusName}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.example.orders.workflow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"DetailType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PaymentFailed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Detail.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"States.JsonToString($.paymentResult.paymentFailureEvent)"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"End"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"PublishOrderRejected"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::events:putEvents"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Entries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"EventBusName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"${DomainEventBusName}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.example.orders.workflow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"DetailType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OrderRejected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Detail.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"States.JsonToString($.fraudResult.fraud.rejectionEvent)"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"End"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why I like this approach
&lt;/h3&gt;

&lt;p&gt;The orchestration is visible and reviewable. You can reason about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what happens on success&lt;/li&gt;
&lt;li&gt;what retries are safe&lt;/li&gt;
&lt;li&gt;how failures are surfaced&lt;/li&gt;
&lt;li&gt;which events are emitted&lt;/li&gt;
&lt;li&gt;which steps need compensation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is much harder when orchestration is hidden across multiple services.&lt;/p&gt;




&lt;h2&gt;
  
  
  Domain task implementation example (Lambda)
&lt;/h2&gt;

&lt;p&gt;Here is a simplified payment authorization step.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/handlers/authorize-payment.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-lambda&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;WorkflowInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;correlationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;commandId&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nl"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;unitPrice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;paymentMethodToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WorkflowInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;correlationId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;correlationId&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalAmount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;qty&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;unitPrice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Simulated payment auth&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;paymentAuthorized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INFO&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Authorizing payment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;correlationId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;awsRequestId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;awsRequestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;totalAmount&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;paymentAuthorized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Payment authorization failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;authorized&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;authorizationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`auth_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalAmount&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  CDK example (wiring the core pieces)
&lt;/h2&gt;

&lt;p&gt;Below is a simplified AWS CDK (TypeScript) example to show how the pieces connect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// lib/command-bus-stack.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-cdk-lib&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Construct&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;constructs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-cdk-lib/aws-events&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-cdk-lib/aws-events-targets&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-cdk-lib/aws-lambda&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-cdk-lib/aws-dynamodb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;sfn&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-cdk-lib/aws-stepfunctions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CommandBusStack&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Stack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Construct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StackProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;commandBus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EventBus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CommandBus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;eventBusName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;command-bus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;domainEventBus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;EventBus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DomainEventBus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;eventBusName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;domain-event-bus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idempotencyTable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;IdempotencyTable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;partitionKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AttributeType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRING&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;billingMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BillingMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PAY_PER_REQUEST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;timeToLiveAttribute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ttl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createOrderCommandFn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CreateOrderCommandFn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODEJS_20_X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;create-order-command.handler&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dist&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;COMMAND_BUS_NAME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;commandBus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventBusName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;IDEMPOTENCY_TABLE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;idempotencyTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tableName&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;commandBus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;grantPutEventsTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;createOrderCommandFn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;idempotencyTable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;grantReadWriteData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;createOrderCommandFn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Placeholder state machine - replace with full definition&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createOrderStateMachine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StateMachine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CreateOrderStateMachine&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;definitionBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sfn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DefinitionBody&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;StartAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Done&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;States&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Succeed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}))&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Rule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;RouteCreateOrderCommand&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;eventBus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;commandBus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;eventPattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;com.example.orders.api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;detailType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CreateOrderCommand&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SfnStateMachine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;createOrderStateMachine&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CommandBusName&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;commandBus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventBusName&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CfnOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;DomainEventBusName&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;domainEventBus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;eventBusName&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  CDK notes from real projects
&lt;/h3&gt;

&lt;p&gt;In production, I also add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DLQs where supported&lt;/li&gt;
&lt;li&gt;alarms on failed executions and throttles&lt;/li&gt;
&lt;li&gt;log retention policies&lt;/li&gt;
&lt;li&gt;encryption settings&lt;/li&gt;
&lt;li&gt;resource policies for cross-account publishing&lt;/li&gt;
&lt;li&gt;least-privilege IAM per state/task&lt;/li&gt;
&lt;li&gt;tagging and cost allocation labels&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Auditability and tracing (this is where the pattern really pays off)
&lt;/h2&gt;

&lt;p&gt;A lot of teams build async systems and then struggle to answer a simple question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“What happened to this request?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I design for that from day one.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I want to trace across the platform
&lt;/h3&gt;

&lt;p&gt;At minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;correlationId&lt;/code&gt; (request-level trace)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;commandId&lt;/code&gt; / &lt;code&gt;eventId&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;causationId&lt;/code&gt; (what produced this)&lt;/li&gt;
&lt;li&gt;workflow execution ID / ARN&lt;/li&gt;
&lt;li&gt;domain entity ID (e.g., &lt;code&gt;orderId&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;tenant ID (if multi-tenant)&lt;/li&gt;
&lt;li&gt;timestamps and status transitions&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Where I record this
&lt;/h3&gt;

&lt;p&gt;I typically combine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;structured logs&lt;/strong&gt; (CloudWatch Logs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;execution history&lt;/strong&gt; (Step Functions)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;traces&lt;/strong&gt; (X-Ray / OpenTelemetry where applicable)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;audit store&lt;/strong&gt; (DynamoDB or S3, depending on retention/query needs)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Audit log strategy (practical approach)
&lt;/h3&gt;

&lt;p&gt;I usually write a normalized audit record for each important transition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;command accepted&lt;/li&gt;
&lt;li&gt;workflow started&lt;/li&gt;
&lt;li&gt;step completed/failed&lt;/li&gt;
&lt;li&gt;event published&lt;/li&gt;
&lt;li&gt;consumer processed/failed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This can be as simple as a DynamoDB table keyed by &lt;code&gt;correlationId&lt;/code&gt; and &lt;code&gt;timestamp&lt;/code&gt;, or an append-only S3 log for long-term retention.&lt;/p&gt;

&lt;h3&gt;
  
  
  Important distinction: audit vs debug logs
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Debug logs&lt;/strong&gt; help developers troubleshoot&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit records&lt;/strong&gt; help operators, compliance, and support teams reconstruct what happened&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not assume one replaces the other.&lt;/p&gt;




&lt;h2&gt;
  
  
  Contract evolution (how to avoid breaking consumers)
&lt;/h2&gt;

&lt;p&gt;This is one of the biggest long-term risks in event-driven systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  My default rule: evolve, do not break
&lt;/h3&gt;

&lt;p&gt;For events, I prefer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;additive changes&lt;/li&gt;
&lt;li&gt;optional fields&lt;/li&gt;
&lt;li&gt;versioned schemas&lt;/li&gt;
&lt;li&gt;compatibility checks in CI&lt;/li&gt;
&lt;li&gt;deprecation windows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For commands, I am stricter because commands are business API boundaries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practical contract evolution patterns
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1) Envelope version + payload schema version
&lt;/h4&gt;

&lt;p&gt;I keep envelope metadata stable and version the business schema independently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"meta"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"schemaVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.1"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2) Add fields, do not rename/remove casually
&lt;/h4&gt;

&lt;p&gt;Safer changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add optional field&lt;/li&gt;
&lt;li&gt;add new event type (if semantics change)&lt;/li&gt;
&lt;li&gt;add new metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Riskier changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;renaming fields&lt;/li&gt;
&lt;li&gt;changing field meaning&lt;/li&gt;
&lt;li&gt;changing enum semantics&lt;/li&gt;
&lt;li&gt;changing required/optional behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  3) Prefer new event types when semantics change materially
&lt;/h4&gt;

&lt;p&gt;If &lt;code&gt;OrderCreated&lt;/code&gt; starts meaning something different, I usually create a new event type rather than mutating semantics.&lt;/p&gt;

&lt;h4&gt;
  
  
  4) Validate contracts in CI
&lt;/h4&gt;

&lt;p&gt;I recommend automated checks for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON schema validation&lt;/li&gt;
&lt;li&gt;compatibility rules (backward/forward depending on consumer strategy)&lt;/li&gt;
&lt;li&gt;sample payload fixtures&lt;/li&gt;
&lt;li&gt;consumer contract tests&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Command vs event evolution nuance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Commands&lt;/strong&gt; are usually controlled at ingress and can be validated more strictly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Events&lt;/strong&gt; may have many consumers and require more conservative change management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is another reason to keep commands and events conceptually separate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Governance and domain boundaries (the part teams often postpone)
&lt;/h2&gt;

&lt;p&gt;A command bus is not just a technical component. It is a governance surface.&lt;/p&gt;

&lt;p&gt;If you skip governance, you will eventually get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inconsistent naming&lt;/li&gt;
&lt;li&gt;ambiguous ownership&lt;/li&gt;
&lt;li&gt;wildcard subscriptions everywhere&lt;/li&gt;
&lt;li&gt;unbounded fan-out&lt;/li&gt;
&lt;li&gt;accidental data exposure&lt;/li&gt;
&lt;li&gt;“shared bus chaos”&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Governance principles I use
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1) Explicit ownership by domain
&lt;/h4&gt;

&lt;p&gt;Each command/event type should have a clear owner.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Orders owns &lt;code&gt;CreateOrder&lt;/code&gt;, &lt;code&gt;OrderCreated&lt;/code&gt;, &lt;code&gt;OrderRejected&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Inventory owns &lt;code&gt;ReserveInventory&lt;/code&gt; (if exposed), &lt;code&gt;InventoryReserved&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Payments owns &lt;code&gt;PaymentAuthorized&lt;/code&gt;, &lt;code&gt;PaymentFailed&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  2) Naming conventions
&lt;/h4&gt;

&lt;p&gt;I standardize at least:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;source&lt;/code&gt; values (e.g., &lt;code&gt;com.example.orders.workflow&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;detail-type&lt;/code&gt; values (e.g., &lt;code&gt;OrderCreated&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;envelope metadata keys&lt;/li&gt;
&lt;li&gt;versioning conventions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This prevents drift and simplifies routing/analytics.&lt;/p&gt;

&lt;h4&gt;
  
  
  3) Separate buses by concern (when warranted)
&lt;/h4&gt;

&lt;p&gt;I often separate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;command bus&lt;/strong&gt; (internal command routing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;domain event bus&lt;/strong&gt; (business events)&lt;/li&gt;
&lt;li&gt;optional &lt;strong&gt;integration bus&lt;/strong&gt; (external partner/integration events)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In larger organizations, I may also separate by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;environment (&lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;test&lt;/code&gt;, &lt;code&gt;prod&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;business domain&lt;/li&gt;
&lt;li&gt;account boundaries&lt;/li&gt;
&lt;li&gt;compliance/data residency needs&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  4) Least privilege on publishers and subscribers
&lt;/h4&gt;

&lt;p&gt;Producers should not be able to publish arbitrary event types to every bus. IAM and EventBridge resource policies should reflect domain boundaries.&lt;/p&gt;

&lt;h4&gt;
  
  
  5) Data classification rules
&lt;/h4&gt;

&lt;p&gt;Do not let sensitive payloads flow “because it is convenient.” Define:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PII handling rules&lt;/li&gt;
&lt;li&gt;masking/redaction requirements&lt;/li&gt;
&lt;li&gt;retention policies&lt;/li&gt;
&lt;li&gt;replay restrictions&lt;/li&gt;
&lt;li&gt;external egress controls&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Failure handling and resilience (what makes this production-grade)
&lt;/h2&gt;

&lt;p&gt;Async architecture is easy to demo and hard to run well unless failure paths are designed intentionally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Areas I design explicitly
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Idempotency
&lt;/h4&gt;

&lt;p&gt;Commands can be retried by clients, gateways, or operators. Idempotency is non-negotiable for business writes.&lt;/p&gt;

&lt;p&gt;Use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;client-provided idempotency key (preferred)&lt;/li&gt;
&lt;li&gt;server-generated fallback only as backup&lt;/li&gt;
&lt;li&gt;TTL on idempotency records to manage table growth&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Retries and backoff
&lt;/h4&gt;

&lt;p&gt;Retries should be defined per step, not copy-pasted blindly.&lt;/p&gt;

&lt;p&gt;Questions I ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the step retry-safe?&lt;/li&gt;
&lt;li&gt;Is the downstream side effect idempotent?&lt;/li&gt;
&lt;li&gt;What is the blast radius if retried?&lt;/li&gt;
&lt;li&gt;Do I need jitter/backoff?&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Dead-letter and replay
&lt;/h4&gt;

&lt;p&gt;Some failures need operator intervention, not endless retries.&lt;/p&gt;

&lt;p&gt;I usually implement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DLQ for failed async processing where supported&lt;/li&gt;
&lt;li&gt;replay tooling with filters (by date, event type, correlation ID)&lt;/li&gt;
&lt;li&gt;safe replay mode (e.g., dry-run validation before reprocessing)&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Compensation
&lt;/h4&gt;

&lt;p&gt;Not every workflow can be truly atomic. For distributed business flows, compensation matters.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;if inventory reservation succeeds but payment later fails, release reservation&lt;/li&gt;
&lt;li&gt;if payment succeeds but order persistence fails, void/mark payment for manual review&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Step Functions makes these paths explicit and testable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing strategy (what I actually test)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Contract tests
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;validate command/event envelopes against schemas&lt;/li&gt;
&lt;li&gt;enforce required metadata fields&lt;/li&gt;
&lt;li&gt;check backward compatibility rules&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2) Workflow tests
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;success path&lt;/li&gt;
&lt;li&gt;step-specific failure paths&lt;/li&gt;
&lt;li&gt;retries/timeouts&lt;/li&gt;
&lt;li&gt;compensation branches&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3) Consumer isolation tests
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;consumers should handle unknown fields&lt;/li&gt;
&lt;li&gt;consumers should tolerate duplicate events&lt;/li&gt;
&lt;li&gt;consumers should reject malformed payloads safely&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4) Observability assertions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;logs include correlation IDs&lt;/li&gt;
&lt;li&gt;emitted events include causation/correlation metadata&lt;/li&gt;
&lt;li&gt;alarms trigger on failed workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I treat observability metadata as part of the contract, not a nice-to-have.&lt;/p&gt;




&lt;h2&gt;
  
  
  When not to use this pattern
&lt;/h2&gt;

&lt;p&gt;I like this pattern a lot, but I would not use it everywhere.&lt;/p&gt;

&lt;p&gt;I avoid it when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the flow is very simple and synchronous&lt;/li&gt;
&lt;li&gt;latency is critical and async orchestration adds unnecessary overhead&lt;/li&gt;
&lt;li&gt;the team is not ready to operate event-driven systems yet&lt;/li&gt;
&lt;li&gt;there is no real need for decoupling/auditability&lt;/li&gt;
&lt;li&gt;a direct service call is clearer and sufficient&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Architecture should reduce complexity, not relocate it.&lt;/p&gt;




&lt;h2&gt;
  
  
  A practical rollout plan (if you are introducing this incrementally)
&lt;/h2&gt;

&lt;p&gt;If I were introducing this into an existing microservice platform, I would phase it in:&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Standardize envelopes and correlation IDs
&lt;/h3&gt;

&lt;p&gt;Keep current calls, but enforce consistent metadata.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2: Introduce EventBridge for non-critical domain events
&lt;/h3&gt;

&lt;p&gt;Start with notifications/analytics fan-out.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3: Move one business workflow to Step Functions
&lt;/h3&gt;

&lt;p&gt;Choose a flow with clear business value and observable pain today.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 4: Add command routing and idempotent ingress
&lt;/h3&gt;

&lt;p&gt;Introduce a command bus for selected workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 5: Formalize governance and contract checks
&lt;/h3&gt;

&lt;p&gt;Naming, ownership, CI validation, replay procedures, IAM boundaries.&lt;/p&gt;

&lt;p&gt;This sequence reduces organizational friction while building platform confidence.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;The biggest advantage of a serverless command bus/workflow backbone is not just that it is “event-driven” or “serverless.” It is that it gives your microservices ecosystem a &lt;strong&gt;clear execution model&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;commands express intent&lt;/li&gt;
&lt;li&gt;workflows coordinate business actions&lt;/li&gt;
&lt;li&gt;events publish facts&lt;/li&gt;
&lt;li&gt;consumers react independently&lt;/li&gt;
&lt;li&gt;audit and tracing are built in&lt;/li&gt;
&lt;li&gt;governance is enforceable at boundaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That combination is what helps a platform scale across both &lt;strong&gt;services&lt;/strong&gt; and &lt;strong&gt;teams&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If I am building for long-term maintainability, this is one of the patterns I reach for early, especially when I know business workflows will grow in complexity and scrutiny.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AWS EventBridge documentation&lt;/li&gt;
&lt;li&gt;AWS Step Functions documentation&lt;/li&gt;
&lt;li&gt;AWS Lambda developer guide&lt;/li&gt;
&lt;li&gt;AWS API Gateway developer guide&lt;/li&gt;
&lt;li&gt;AWS CDK documentation&lt;/li&gt;
&lt;li&gt;AWS X-Ray and observability documentation&lt;/li&gt;
&lt;li&gt;AWS Well-Architected Framework (especially reliability and operational excellence guidance)&lt;/li&gt;
&lt;li&gt;Event-driven architecture patterns and domain-driven design references (commands/events, bounded contexts, integration events)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>microservices</category>
      <category>aws</category>
    </item>
    <item>
      <title>Building with API Gateway, Lambda and DynamoDB Single-Table Design for Multi-Tenant SaaS</title>
      <dc:creator>Renaldi</dc:creator>
      <pubDate>Sun, 08 Mar 2026 22:00:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/building-with-api-gateway-lambda-and-dynamodb-single-table-design-for-multi-tenant-saas-3cm3</link>
      <guid>https://dev.to/aws-builders/building-with-api-gateway-lambda-and-dynamodb-single-table-design-for-multi-tenant-saas-3cm3</guid>
      <description>&lt;p&gt;When I build multi-tenant SaaS on AWS, one of my favorite combinations is &lt;strong&gt;API Gateway + Lambda + DynamoDB&lt;/strong&gt;. It gives me a clean serverless control plane, fast iteration, and a lot of flexibility in how I model data and enforce tenancy.&lt;/p&gt;

&lt;p&gt;In this post, I will walk through an end-to-end implementation pattern for a &lt;strong&gt;multi-tenant SaaS API&lt;/strong&gt; using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon API Gateway&lt;/strong&gt; for API ingress&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Lambda&lt;/strong&gt; for business logic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon DynamoDB&lt;/strong&gt; with a &lt;strong&gt;single-table design&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amazon Cognito + JWT&lt;/strong&gt; for authentication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-tenant throttling and quotas&lt;/strong&gt; using API Gateway usage plans (with a Lambda authorizer pattern)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I will focus on the architectural and data modeling decisions that matter in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tenant isolation patterns&lt;/li&gt;
&lt;li&gt;Partition key design&lt;/li&gt;
&lt;li&gt;Hot partition mitigation&lt;/li&gt;
&lt;li&gt;GSIs for access patterns&lt;/li&gt;
&lt;li&gt;Auth context propagation (Cognito/JWT)&lt;/li&gt;
&lt;li&gt;Per-tenant throttling and quotas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I will also include code and an implementation walkthrough so you can adapt the pattern to your own SaaS.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this stack works so well for multi-tenant SaaS
&lt;/h2&gt;

&lt;p&gt;I like this stack because it lets me combine &lt;strong&gt;application-level tenancy controls&lt;/strong&gt; with &lt;strong&gt;infrastructure-level scalability&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API Gateway&lt;/strong&gt; gives me request routing, auth integration, throttling, and observability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda&lt;/strong&gt; gives me a stateless execution layer where I can consistently apply tenant-aware logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB&lt;/strong&gt; gives me low-latency reads/writes and a data model that can be shaped around access patterns, which is exactly what multi-tenant SaaS needs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest challenge is not deploying these services. It is designing them so that one tenant cannot interfere with another tenant, and so that one noisy tenant does not dominate throughput.&lt;/p&gt;

&lt;p&gt;That is where the design details matter.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I am building in this walkthrough
&lt;/h2&gt;

&lt;p&gt;To make the examples concrete, I will use a simple &lt;strong&gt;B2B issue tracking SaaS&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each &lt;strong&gt;tenant&lt;/strong&gt; is a customer organization&lt;/li&gt;
&lt;li&gt;Each tenant has:

&lt;ul&gt;
&lt;li&gt;users&lt;/li&gt;
&lt;li&gt;projects&lt;/li&gt;
&lt;li&gt;tickets&lt;/li&gt;
&lt;li&gt;comments&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Users authenticate with &lt;strong&gt;Cognito&lt;/strong&gt;
&lt;/li&gt;

&lt;li&gt;API calls are authorized via JWT and mapped to a tenant context&lt;/li&gt;

&lt;li&gt;Data lives in one DynamoDB table (single-table design)&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Example endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;POST /projects/{projectId}/tickets&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /projects/{projectId}/tickets?status=OPEN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /me/tickets&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /tickets/{ticketId}/comments&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Important design principle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I do not trust the client to tell me the tenant ID in the path/body for authorization purposes. I derive tenancy from the &lt;strong&gt;validated JWT&lt;/strong&gt;, then enforce it in my Lambda logic and DynamoDB key design.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxpcmu4r9xgxldubuqxoo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxpcmu4r9xgxldubuqxoo.png" alt=" " width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This architecture uses:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cognito&lt;/strong&gt; to issue JWTs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Gateway (REST API)&lt;/strong&gt; as the front door&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda Authorizer&lt;/strong&gt; to validate JWT and inject normalized auth context&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda handlers&lt;/strong&gt; for business operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB single table&lt;/strong&gt; for all entities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Gateway usage plans&lt;/strong&gt; for per-tenant throttling/quota (via authorizer metering key)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I am intentionally using a &lt;strong&gt;Lambda authorizer&lt;/strong&gt; (instead of only a native Cognito authorizer) because it lets me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;normalize claims (&lt;code&gt;tenantId&lt;/code&gt;, &lt;code&gt;roles&lt;/code&gt;, &lt;code&gt;plan&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;enforce custom auth checks&lt;/li&gt;
&lt;li&gt;return a &lt;strong&gt;&lt;code&gt;usageIdentifierKey&lt;/code&gt;&lt;/strong&gt; to support per-tenant usage plans and quotas in API Gateway REST API&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  End-to-end request flow (from login to DynamoDB write)
&lt;/h2&gt;

&lt;p&gt;Let’s walk through a &lt;code&gt;POST /projects/{projectId}/tickets&lt;/code&gt; call.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;User signs in via Cognito&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cognito issues a JWT (ID token or access token, depending your claim strategy)&lt;/li&gt;
&lt;li&gt;The token contains user identity and tenant-related claims (for example &lt;code&gt;tenant_id&lt;/code&gt;, &lt;code&gt;plan&lt;/code&gt;, &lt;code&gt;roles&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Client calls API Gateway&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sends &lt;code&gt;Authorization: Bearer &amp;lt;JWT&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;API Gateway invokes Lambda Authorizer&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Validates the JWT signature and issuer&lt;/li&gt;
&lt;li&gt;Extracts/normalizes claims&lt;/li&gt;
&lt;li&gt;Returns:

&lt;ul&gt;
&lt;li&gt;IAM policy (&lt;code&gt;Allow&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;context&lt;/code&gt; (tenant ID, user ID, roles, plan)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;usageIdentifierKey&lt;/code&gt; (tenant-specific metering key for usage plan enforcement)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;API Gateway invokes Lambda handler&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The handler reads the authorizer context from &lt;code&gt;event.requestContext.authorizer&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Lambda writes to DynamoDB&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uses tenant-prefixed keys and tenant-aware access patterns&lt;/li&gt;
&lt;li&gt;Applies conditional expressions as needed&lt;/li&gt;
&lt;li&gt;Writes base item + GSI attributes&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CloudWatch metrics/logs capture telemetry&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API Gateway request count/throttles&lt;/li&gt;
&lt;li&gt;Lambda duration/errors&lt;/li&gt;
&lt;li&gt;DynamoDB consumed capacity/throttles&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This pattern keeps tenancy and throttling context consistent across the request path.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tenant isolation patterns (and which one I am using)
&lt;/h2&gt;

&lt;p&gt;Before writing a single key schema, I decide what kind of tenancy model I need.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Silo model (strongest isolation)
&lt;/h3&gt;

&lt;p&gt;Each tenant gets isolated infrastructure (separate table / account / stack).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Strong isolation&lt;/li&gt;
&lt;li&gt;Easier compliance stories for some customers&lt;/li&gt;
&lt;li&gt;No noisy neighbor at data layer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Higher operational overhead&lt;/li&gt;
&lt;li&gt;Harder fleet-wide schema changes&lt;/li&gt;
&lt;li&gt;More expensive for small tenants&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2) Pool model (shared infrastructure, tenant-aware app logic)
&lt;/h3&gt;

&lt;p&gt;All tenants share the same infrastructure (same API, same Lambda, same table), but every request and every item is tenant-scoped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cost-efficient&lt;/li&gt;
&lt;li&gt;Operationally simpler for most SaaS products&lt;/li&gt;
&lt;li&gt;Scales well if keys are designed correctly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You must be disciplined about isolation in code and data model&lt;/li&gt;
&lt;li&gt;Hot partitions/noisy neighbors need mitigation&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3) Bridge model (hybrid)
&lt;/h3&gt;

&lt;p&gt;Some tenants are pooled, some are siloed (for enterprise/compliance tiers).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flexible business model&lt;/li&gt;
&lt;li&gt;Lets you upsell isolation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;More deployment and routing complexity&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What I am using here
&lt;/h3&gt;

&lt;p&gt;For this post, I am using the &lt;strong&gt;pool model&lt;/strong&gt; with strong tenant-aware design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tenant ID is always derived from auth context&lt;/li&gt;
&lt;li&gt;DynamoDB keys are tenant-prefixed&lt;/li&gt;
&lt;li&gt;Access patterns are tenant-scoped&lt;/li&gt;
&lt;li&gt;API Gateway usage plans enforce per-tenant limits&lt;/li&gt;
&lt;li&gt;Lambda code refuses cross-tenant access even if a user manipulates IDs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a very practical pattern for SaaS teams that want speed and scale without overbuilding day one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Single-table design mindset for multi-tenant SaaS
&lt;/h2&gt;

&lt;p&gt;A lot of DynamoDB pain comes from designing tables like relational schemas.&lt;/p&gt;

&lt;p&gt;I do the opposite: I start with &lt;strong&gt;access patterns&lt;/strong&gt;, then design keys around them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Access patterns I need
&lt;/h3&gt;

&lt;p&gt;For this SaaS, I care about these reads/writes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a ticket in a project&lt;/li&gt;
&lt;li&gt;List tickets by project (optionally filtered by status)&lt;/li&gt;
&lt;li&gt;List tickets assigned to the current user&lt;/li&gt;
&lt;li&gt;Get a ticket by ID&lt;/li&gt;
&lt;li&gt;Add/list comments for a ticket&lt;/li&gt;
&lt;li&gt;List projects for a tenant&lt;/li&gt;
&lt;li&gt;Look up user membership / role in a tenant&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These access patterns will drive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;primary key design (&lt;code&gt;PK&lt;/code&gt;, &lt;code&gt;SK&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;GSI design&lt;/li&gt;
&lt;li&gt;where I shard to avoid hot partitions&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Partition key design (tenant-aware and access-pattern-first)
&lt;/h2&gt;

&lt;p&gt;This is the part that usually makes or breaks multi-tenant DynamoDB.&lt;/p&gt;

&lt;h3&gt;
  
  
  A common mistake
&lt;/h3&gt;

&lt;p&gt;A very common first attempt is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PK = TENANT#&amp;lt;tenantId&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SK = &amp;lt;everything else&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That can work for small tenants, but it becomes risky when a tenant is large or very active because &lt;strong&gt;all writes for that tenant&lt;/strong&gt; concentrate on a small number of partitions.&lt;/p&gt;

&lt;h3&gt;
  
  
  My design approach
&lt;/h3&gt;

&lt;p&gt;I still make keys tenant-aware, but I &lt;strong&gt;distribute writes across tenant sub-collections&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I use patterns like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PK = TENANT#&amp;lt;tenantId&amp;gt;&lt;/code&gt; for tenant metadata and low-volume tenant-level items&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PK = TENANT#&amp;lt;tenantId&amp;gt;#PROJECT#&amp;lt;projectId&amp;gt;&lt;/code&gt; for project and ticket collections&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PK = TENANT#&amp;lt;tenantId&amp;gt;#TICKET#&amp;lt;ticketId&amp;gt;&lt;/code&gt; for ticket root + comments (if comment-heavy)&lt;/li&gt;
&lt;li&gt;Sharded GSIs (or write-sharded partitions) for high-volume list patterns&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives me tenant isolation without forcing every high-volume write into one tenant partition.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example item model (single table)
&lt;/h2&gt;

&lt;p&gt;I usually keep a few shared attributes on every item:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PK&lt;/code&gt;, &lt;code&gt;SK&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;entityType&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tenantId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;timestamps (&lt;code&gt;createdAt&lt;/code&gt;, &lt;code&gt;updatedAt&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;optional GSI attributes (&lt;code&gt;GSI1PK&lt;/code&gt;, &lt;code&gt;GSI1SK&lt;/code&gt;, &lt;code&gt;GSI2PK&lt;/code&gt;, &lt;code&gt;GSI2SK&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Example items
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Tenant metadata
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"PK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TENANT#t_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"SK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"META"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entityType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TENANT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tenantId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"t_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Acme Corp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plan"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pro"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-25T00:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Project
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"PK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TENANT#t_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"SK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PROJECT#p_001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entityType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PROJECT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tenantId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"t_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"projectId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"p_001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Platform"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-25T00:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Ticket (project-scoped partition)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"PK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TENANT#t_123#PROJECT#p_001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"SK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TICKET#tk_9001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entityType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TICKET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tenantId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"t_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"projectId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"p_001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ticketId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tk_9001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"API returns 500 on retry"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OPEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priority"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HIGH"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"assigneeUserId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"u_77"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"createdByUserId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"u_12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-25T01:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"updatedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-25T01:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"GSI1PK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TENANT#t_123#PROJECT#p_001#STATUS#OPEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"GSI1SK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UPDATED#2026-02-25T01:00:00Z#TICKET#tk_9001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"GSI2PK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TENANT#t_123#ASSIGNEE#u_77"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"GSI2SK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"UPDATED#2026-02-25T01:00:00Z#TICKET#tk_9001"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Comment (ticket-scoped partition)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"PK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TENANT#t_123#TICKET#tk_9001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"SK"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"COMMENT#2026-02-25T01:05:00Z#c_001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"entityType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"COMMENT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tenantId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"t_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ticketId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tk_9001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"commentId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"c_001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authorUserId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"u_12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Investigating now"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-25T01:05:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why this layout works
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Tenant is embedded in every key path&lt;/li&gt;
&lt;li&gt;Tickets are grouped by project for efficient project views&lt;/li&gt;
&lt;li&gt;Comments are grouped by ticket for efficient comment threads&lt;/li&gt;
&lt;li&gt;User-assignee lookup is handled by GSI&lt;/li&gt;
&lt;li&gt;Status-based listing is handled by GSI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a good default shape for many SaaS workloads.&lt;/p&gt;




&lt;h2&gt;
  
  
  GSIs for access patterns (and how I decide them)
&lt;/h2&gt;

&lt;p&gt;I try to make every GSI exist for a &lt;strong&gt;specific query&lt;/strong&gt;, not “just in case”.&lt;/p&gt;

&lt;h3&gt;
  
  
  GSI1: List tickets by project + status
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; &lt;code&gt;GET /projects/{projectId}/tickets?status=OPEN&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GSI1PK = TENANT#&amp;lt;tenantId&amp;gt;#PROJECT#&amp;lt;projectId&amp;gt;#STATUS#&amp;lt;status&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GSI1SK = UPDATED#&amp;lt;timestamp&amp;gt;#TICKET#&amp;lt;ticketId&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This lets me list tickets by status and sort by most recent updates.&lt;/p&gt;

&lt;h3&gt;
  
  
  GSI2: List tickets assigned to a user
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; &lt;code&gt;GET /me/tickets&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GSI2PK = TENANT#&amp;lt;tenantId&amp;gt;#ASSIGNEE#&amp;lt;userId&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GSI2SK = UPDATED#&amp;lt;timestamp&amp;gt;#TICKET#&amp;lt;ticketId&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives me a tenant-safe “my work” view.&lt;/p&gt;

&lt;h3&gt;
  
  
  Optional GSI3: Lookup by public ticket reference (if needed)
&lt;/h3&gt;

&lt;p&gt;If you expose a user-friendly ticket reference like &lt;code&gt;ACME-194&lt;/code&gt;, I may add a sparse GSI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GSI3PK = TENANT#&amp;lt;tenantId&amp;gt;#REF#ACME-194&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GSI3SK = TICKET#&amp;lt;ticketId&amp;gt;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I do not add this unless I truly need it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hot partition mitigation (the part many examples skip)
&lt;/h2&gt;

&lt;p&gt;A single-table design can be “correct” and still fail under traffic if hot partitions are ignored.&lt;/p&gt;

&lt;p&gt;Here are the patterns I use.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) Avoid concentrating all writes into one tenant root partition
&lt;/h3&gt;

&lt;p&gt;If all writes use &lt;code&gt;PK = TENANT#&amp;lt;tenantId&amp;gt;&lt;/code&gt;, a large tenant can get hot fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mitigation:&lt;/strong&gt; partition by natural sub-aggregates (project, ticket thread, etc.)&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Use write sharding for high-volume list/index patterns
&lt;/h3&gt;

&lt;p&gt;Suppose one tenant has a huge number of ticket updates, and your status GSI gets hot.&lt;/p&gt;

&lt;p&gt;I can shard the GSI partition key:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GSI1PK = TENANT#t_123#PROJECT#p_001#STATUS#OPEN#SHARD#03&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then query multiple shards and merge results in Lambda.&lt;/p&gt;

&lt;p&gt;How do I pick the shard?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hash &lt;code&gt;ticketId&lt;/code&gt; or &lt;code&gt;(ticketId + status)&lt;/code&gt; into &lt;code&gt;N&lt;/code&gt; buckets&lt;/li&gt;
&lt;li&gt;Start small (for example 4 or 8 shards)&lt;/li&gt;
&lt;li&gt;Increase if traffic grows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shardFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shardCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;shardCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;shard&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3) Separate write-heavy entities from read-heavy aggregations
&lt;/h3&gt;

&lt;p&gt;If comments are very high volume, I often avoid mixing them in the same partition as ticket metadata.&lt;/p&gt;

&lt;p&gt;That is why in the example above I used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ticket record under &lt;code&gt;TENANT#...#PROJECT#...&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;comments under &lt;code&gt;TENANT#...#TICKET#...&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4) Use on-demand capacity early, then tune if needed
&lt;/h3&gt;

&lt;p&gt;For newer SaaS products, I often start with &lt;strong&gt;on-demand&lt;/strong&gt; capacity because it reduces operational tuning.&lt;/p&gt;

&lt;p&gt;As traffic patterns stabilize, I decide whether provisioned + autoscaling is worth it.&lt;/p&gt;

&lt;h3&gt;
  
  
  5) Watch metrics by access pattern, not just table-wide
&lt;/h3&gt;

&lt;p&gt;I monitor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Throttled requests&lt;/li&gt;
&lt;li&gt;Consumed read/write capacity&lt;/li&gt;
&lt;li&gt;Latency by endpoint&lt;/li&gt;
&lt;li&gt;GSI-specific pressure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If one endpoint is slow, I inspect whether the underlying key pattern is too concentrated.&lt;/p&gt;




&lt;h2&gt;
  
  
  Auth context propagation (Cognito/JWT -&amp;gt; API Gateway -&amp;gt; Lambda)
&lt;/h2&gt;

&lt;p&gt;This is the glue that keeps tenant isolation consistent.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I want from auth context
&lt;/h3&gt;

&lt;p&gt;By the time my business Lambda runs, I want a normalized auth context like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tenantId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;userId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;roles&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;plan&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;(optional) &lt;code&gt;scopes&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;orgId&lt;/code&gt;, &lt;code&gt;isSupportUser&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I do not want each Lambda handler parsing arbitrary JWT claims differently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cognito claim strategy
&lt;/h3&gt;

&lt;p&gt;You have a few options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use Cognito &lt;strong&gt;ID token&lt;/strong&gt; claims directly (simpler if tenant custom attributes are on the user profile)&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;Pre Token Generation&lt;/strong&gt; trigger to inject normalized tenant claims into the token you use for APIs&lt;/li&gt;
&lt;li&gt;Use groups + custom claims for role/plan mapping&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this post, I will assume the token contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sub&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;custom:tenant_id&lt;/code&gt; (or &lt;code&gt;tenant_id&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;custom:plan&lt;/code&gt; (or &lt;code&gt;plan&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cognito:groups&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Lambda Authorizer example (TypeScript)
&lt;/h2&gt;

&lt;p&gt;This authorizer validates the Cognito JWT, extracts tenant context, and returns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an Allow policy&lt;/li&gt;
&lt;li&gt;authorizer context&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;usageIdentifierKey&lt;/code&gt; (for per-tenant API Gateway usage plan metering)&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: In API Gateway REST APIs, &lt;code&gt;usageIdentifierKey&lt;/code&gt; should be the &lt;strong&gt;API key value&lt;/strong&gt; you want metered. In production, I usually map &lt;code&gt;tenantId -&amp;gt; meteringKey&lt;/code&gt; and return the metering key, not the raw tenant ID.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// authorizer.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;APIGatewayTokenAuthorizerEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;APIGatewayAuthorizerResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-lambda&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;CognitoJwtVerifier&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-jwt-verify&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userPoolId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;USER_POOL_ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clientId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APP_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;verifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;CognitoJwtVerifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;userPoolId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tokenUse&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// or "access" if your claims are injected there&lt;/span&gt;
  &lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;principalId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Allow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Deny&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;principalId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;policyDocument&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;2012-10-17&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;Statement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;execute-api:Invoke&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;Effect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;effect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Replace with a real lookup (DynamoDB / Secrets Manager / config service)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;lookupMeteringKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Example only. In production, return the API Gateway API key *value* assigned to that tenant.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`meter-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayTokenAuthorizerEvent&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;APIGatewayAuthorizerResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorizationToken&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^Bearer&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;verifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;custom:tenant_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tenant_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Missing tenant claim&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
      &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;custom:plan&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;plan&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;free&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;groups&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cognito:groups&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;meteringKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;lookupMeteringKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;buildPolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Allow&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;methodArn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;rolesJson&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;usageIdentifierKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;meteringKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// API Gateway treats "Unauthorized" specially&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why I normalize here instead of in every Lambda
&lt;/h3&gt;

&lt;p&gt;This avoids duplicated auth parsing and reduces inconsistent authorization logic across handlers.&lt;/p&gt;

&lt;p&gt;It also gives me one place to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;map plans&lt;/li&gt;
&lt;li&gt;map roles&lt;/li&gt;
&lt;li&gt;support internal support-user impersonation rules (if needed)&lt;/li&gt;
&lt;li&gt;connect metering/throttling to tenancy&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Business Lambda: reading auth context and enforcing tenancy
&lt;/h2&gt;

&lt;p&gt;Now let’s create a ticket.&lt;/p&gt;

&lt;p&gt;This handler:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reads tenant context from the authorizer&lt;/li&gt;
&lt;li&gt;ignores any tenant ID in the request body&lt;/li&gt;
&lt;li&gt;writes a ticket item to DynamoDB using a tenant-prefixed key&lt;/li&gt;
&lt;li&gt;populates GSIs for list access patterns
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// create-ticket.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyResult&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-lambda&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DynamoDBClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aws-sdk/client-dynamodb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DynamoDBDocumentClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PutCommand&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aws-sdk/lib-dynamodb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;randomUUID&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DynamoDBDocumentClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DynamoDBClient&lt;/span&gt;&lt;span class="p"&gt;({}));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TABLE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AuthorizerCtx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;rolesJson&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAuthContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;AuthorizerCtx&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorizer&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;free&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rolesJson&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rolesJson&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rolesJson&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Missing auth context&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;plan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rolesJson&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;shardFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shardCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charCodeAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;shardCount&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;APIGatewayProxyResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAuthContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projectId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathParameters&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;projectId is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assigneeUserId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assigneeUserId&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assigneeUserId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;priority&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MEDIUM&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ticketId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`tk_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OPEN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Optional GSI sharding for high-volume status lists&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;statusShard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;shardFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ticketId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;PK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`TENANT#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;#PROJECT#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;SK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`TICKET#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ticketId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;entityType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TICKET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

      &lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;ticketId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;assigneeUserId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;createdByUserId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

      &lt;span class="c1"&gt;// List tickets by project + status&lt;/span&gt;
      &lt;span class="na"&gt;GSI1PK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`TENANT#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;#PROJECT#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;#STATUS#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;#SHARD#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;statusShard&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;GSI1SK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`UPDATED#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;#TICKET#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ticketId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

      &lt;span class="c1"&gt;// List tickets by assignee&lt;/span&gt;
      &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;assigneeUserId&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;GSI2PK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`TENANT#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;#ASSIGNEE#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;assigneeUserId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;GSI2SK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`UPDATED#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;#TICKET#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ticketId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PutCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;ConditionExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;attribute_not_exists(PK) AND attribute_not_exists(SK)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;ticketId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;create-ticket failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Internal server error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Isolation note
&lt;/h3&gt;

&lt;p&gt;The handler never accepts &lt;code&gt;tenantId&lt;/code&gt; from the request body for authorization purposes.&lt;/p&gt;

&lt;p&gt;Even if a client sends a different tenant ID, it does not matter because the handler uses &lt;code&gt;tenantId&lt;/code&gt; from the validated authorizer context.&lt;/p&gt;




&lt;h2&gt;
  
  
  Querying with GSIs (list tickets by project + status)
&lt;/h2&gt;

&lt;p&gt;Now let’s implement &lt;code&gt;GET /projects/{projectId}/tickets?status=OPEN&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Because I sharded &lt;code&gt;GSI1PK&lt;/code&gt;, I may need to query multiple shards and merge the results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// list-project-tickets.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyResult&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-lambda&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DynamoDBClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aws-sdk/client-dynamodb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;DynamoDBDocumentClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;QueryCommand&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aws-sdk/lib-dynamodb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DynamoDBDocumentClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DynamoDBClient&lt;/span&gt;&lt;span class="p"&gt;({}));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TABLE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getTenantId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requestContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorizer&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Missing tenantId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyEvent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;APIGatewayProxyResult&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tenantId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getTenantId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projectId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathParameters&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;queryStringParameters&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OPEN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;projectId is required&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shardCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;shardCount&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;QueryCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;IndexName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GSI1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;KeyConditionExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GSI1PK = :pk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;ExpressionAttributeValues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;:pk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`TENANT#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tenantId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;#PROJECT#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;#STATUS#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;#SHARD#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;shard&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="na"&gt;Limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// tune based on your UX pagination model&lt;/span&gt;
          &lt;span class="na"&gt;ScanIndexForward&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// newest first&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Items&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

    &lt;span class="c1"&gt;// Merge and sort in-memory because we queried multiple shards&lt;/span&gt;
    &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;localeCompare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;list-project-tickets failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Internal server error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Tradeoff discussion
&lt;/h3&gt;

&lt;p&gt;Sharding improves write scalability, but read logic is more complex because I need fan-out queries.&lt;/p&gt;

&lt;p&gt;That is a normal DynamoDB tradeoff. I only add this complexity for access patterns that actually need it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Per-tenant throttling and quotas (API Gateway + authorizer metering)
&lt;/h2&gt;

&lt;p&gt;This is one of the most useful SaaS controls and one of the least discussed implementation details.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I want
&lt;/h3&gt;

&lt;p&gt;I want to enforce limits like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Free tier tenant: lower RPS and daily quota&lt;/li&gt;
&lt;li&gt;Pro tier tenant: higher RPS and quota&lt;/li&gt;
&lt;li&gt;Enterprise tenant: custom limits&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Why API Gateway usage plans help
&lt;/h3&gt;

&lt;p&gt;API Gateway REST APIs support:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;throttling&lt;/li&gt;
&lt;li&gt;quotas&lt;/li&gt;
&lt;li&gt;API keys&lt;/li&gt;
&lt;li&gt;usage plans&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With a Lambda authorizer, I can return a &lt;strong&gt;&lt;code&gt;usageIdentifierKey&lt;/code&gt;&lt;/strong&gt; so API Gateway meters/throttles by a tenant-specific key, even when the client authenticates with JWT.&lt;/p&gt;

&lt;h3&gt;
  
  
  Important implementation note
&lt;/h3&gt;

&lt;p&gt;For REST APIs, set &lt;code&gt;apiKeySourceType&lt;/code&gt; to &lt;code&gt;AUTHORIZER&lt;/code&gt; so the usage key comes from the authorizer rather than an &lt;code&gt;x-api-key&lt;/code&gt; header.&lt;/p&gt;

&lt;h3&gt;
  
  
  CDK sketch (TypeScript)
&lt;/h3&gt;

&lt;p&gt;Below is a simplified CDK sketch to show the pattern. You will need to adapt it to your stack structure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-cdk-lib&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Construct&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;constructs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-cdk-lib/aws-apigateway&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-cdk-lib/aws-lambda&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-cdk-lib/aws-dynamodb&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MultiTenantApiStack&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Stack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Construct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;StackProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AppTable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;partitionKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AttributeType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRING&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;sortKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AttributeType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRING&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;billingMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;BillingMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PAY_PER_REQUEST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;pointInTimeRecovery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;removalPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RemovalPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RETAIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addGlobalSecondaryIndex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;indexName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GSI1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;partitionKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GSI1PK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AttributeType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRING&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;sortKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GSI1SK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AttributeType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRING&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;projectionType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ProjectionType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ALL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addGlobalSecondaryIndex&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;indexName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GSI2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;partitionKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GSI2PK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AttributeType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRING&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;sortKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GSI2SK&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AttributeType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STRING&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;projectionType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dynamodb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ProjectionType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ALL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authorizerFn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AuthorizerFn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODEJS_20_X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;authorizer.handler&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dist/authorizer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;USER_POOL_ID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-user-pool-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;APP_CLIENT_ID&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;your-app-client-id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createTicketFn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CreateTicketFn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODEJS_20_X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;create-ticket.handler&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dist/create-ticket&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;TABLE_NAME&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tableName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;grantReadWriteData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;createTicketFn&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RestApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SaaSApi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;restApiName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;multi-tenant-saas-api&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;apiKeySourceType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ApiKeySourceType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AUTHORIZER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// key point for per-tenant metering&lt;/span&gt;
      &lt;span class="na"&gt;deployOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;stageName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prod&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;metricsEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;loggingLevel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MethodLoggingLevel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;dataTraceEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;throttlingBurstLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;throttlingRateLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tokenAuthorizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TokenAuthorizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JwtAuthorizer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;authorizerFn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;resultsCacheTtl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;projects&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;projectId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{projectId}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tickets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tickets&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;tickets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addMethod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LambdaIntegration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;createTicketFn&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;authorizer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tokenAuthorizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;authorizationType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AuthorizationType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CUSTOM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Example usage plan (create multiple plans for free/pro/enterprise)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;freePlan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addUsagePlan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;FreePlan&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;free-tier-plan&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;rateLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;burstLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;quota&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;period&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Period&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DAY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;freePlan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addApiStage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deploymentStage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// API keys that represent tenant metering identities.&lt;/span&gt;
    &lt;span class="c1"&gt;// In production, create one per tenant and associate with the correct plan.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tenantApiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addApiKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;TenantApiKey_t_123&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;apiKeyName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tenant-t_123-metering-key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;// value can be auto-generated or explicitly set&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;freePlan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addApiKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tenantApiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How I usually operationalize this
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Create usage plans by tier (&lt;code&gt;free&lt;/code&gt;, &lt;code&gt;pro&lt;/code&gt;, &lt;code&gt;enterprise&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Create an API key per tenant (metering identity)&lt;/li&gt;
&lt;li&gt;Store the key value (or a mapping) securely&lt;/li&gt;
&lt;li&gt;Have the authorizer return the correct &lt;code&gt;usageIdentifierKey&lt;/code&gt; for the tenant&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives me a clean SaaS throttle/quota model without making clients manage API keys directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Authorization checks beyond JWT validation (critical for tenant safety)
&lt;/h2&gt;

&lt;p&gt;JWT validation is not enough. I also enforce authorization in Lambda based on tenant and role.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User can create tickets only in projects belonging to their tenant&lt;/li&gt;
&lt;li&gt;User can comment only on tickets in their tenant&lt;/li&gt;
&lt;li&gt;Support/admin actions require specific roles/scopes&lt;/li&gt;
&lt;li&gt;Cross-tenant resource IDs are rejected even if guessed&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Example guard pattern
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;requireRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rolesJson&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;roles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rolesJson&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rolesJson&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;roles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;allowed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Forbidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I typically build a small internal library for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;auth context parsing&lt;/li&gt;
&lt;li&gt;role checks&lt;/li&gt;
&lt;li&gt;tenant-scoped key helpers&lt;/li&gt;
&lt;li&gt;common error responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That reduces drift across Lambda functions.&lt;/p&gt;




&lt;h2&gt;
  
  
  End-to-end implementation walkthrough
&lt;/h2&gt;

&lt;p&gt;Here is a practical sequence I would follow to build this from scratch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Define your access patterns first
&lt;/h3&gt;

&lt;p&gt;Before touching CloudFormation/CDK, write down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reads&lt;/li&gt;
&lt;li&gt;writes&lt;/li&gt;
&lt;li&gt;sorting/filtering requirements&lt;/li&gt;
&lt;li&gt;expected high-volume paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This will drive your &lt;code&gt;PK/SK&lt;/code&gt; and GSI decisions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Set up Cognito and define tenant claims
&lt;/h3&gt;

&lt;p&gt;Create a user pool and app client. Decide how tenant context appears in JWTs.&lt;/p&gt;

&lt;p&gt;I recommend normalizing these claims:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tenant_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;plan&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;roles&lt;/code&gt; or &lt;code&gt;groups&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your current user profile attributes are inconsistent, fix that first. It will save a lot of pain later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Build the Lambda authorizer
&lt;/h3&gt;

&lt;p&gt;Implement JWT validation and claim normalization.&lt;/p&gt;

&lt;p&gt;Outputs should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;tenantId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;userId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rolesJson&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;plan&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;usageIdentifierKey&lt;/code&gt; (tenant metering key)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 4: Create DynamoDB single table + GSIs
&lt;/h3&gt;

&lt;p&gt;Start with only the GSIs you need for your first release.&lt;/p&gt;

&lt;p&gt;A good initial setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;base table (&lt;code&gt;PK&lt;/code&gt;, &lt;code&gt;SK&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GSI1&lt;/code&gt; for project+status ticket listing&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GSI2&lt;/code&gt; for assignee ticket listing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 5: Implement tenant-safe handlers
&lt;/h3&gt;

&lt;p&gt;In every handler:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;parse auth context from API Gateway authorizer context&lt;/li&gt;
&lt;li&gt;derive keys from auth context&lt;/li&gt;
&lt;li&gt;never trust client-supplied tenant IDs for authorization&lt;/li&gt;
&lt;li&gt;use conditional expressions when appropriate&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 6: Add per-tenant usage plans
&lt;/h3&gt;

&lt;p&gt;Create:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;usage plans by tier&lt;/li&gt;
&lt;li&gt;API keys per tenant&lt;/li&gt;
&lt;li&gt;tenant -&amp;gt; metering key mapping&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then return the metering key in the Lambda authorizer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Test with real tokens and tenant boundary tests
&lt;/h3&gt;

&lt;p&gt;I always run tests for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;valid tenant access&lt;/li&gt;
&lt;li&gt;invalid token&lt;/li&gt;
&lt;li&gt;wrong tenant resource ID&lt;/li&gt;
&lt;li&gt;quota exceeded / throttle behavior&lt;/li&gt;
&lt;li&gt;large-tenant list/query traffic&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Example request/response flow (local testing mindset)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Create ticket request
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://{apiId}.execute-api.{region}.amazonaws.com/prod/projects/p_001/tickets"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;JWT&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "title": "API returns 500 on retry",
    "priority": "HIGH",
    "assigneeUserId": "u_77"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Expected response
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ticketId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tk_6b4d8e3e-...."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OPEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-25T01:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  List tickets by status
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://{apiId}.execute-api.{region}.amazonaws.com/prod/projects/p_001/tickets?status=OPEN"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;JWT&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Common mistakes I see in this architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Trusting &lt;code&gt;tenantId&lt;/code&gt; from the client
&lt;/h3&gt;

&lt;p&gt;This is the fastest way to create cross-tenant data leaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; derive tenant context from validated JWT and enforce server-side.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Designing the table like a relational schema
&lt;/h3&gt;

&lt;p&gt;DynamoDB single-table design is access-pattern driven, not entity-table driven.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; start with queries and write paths first.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) Overusing one tenant root partition key
&lt;/h3&gt;

&lt;p&gt;This can create hot partitions for large tenants.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; use tenant-aware sub-collections and shard high-volume patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  4) Adding too many GSIs early
&lt;/h3&gt;

&lt;p&gt;Every GSI adds write amplification and operational cost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; add GSIs only for clear access patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  5) Treating JWT validation as full authorization
&lt;/h3&gt;

&lt;p&gt;A valid token does not mean a valid action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; layer role checks and tenant-resource checks in Lambda.&lt;/p&gt;

&lt;h3&gt;
  
  
  6) Ignoring quota/throttling strategy until later
&lt;/h3&gt;

&lt;p&gt;By the time you need it, retrofitting it can be messy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; establish a per-tenant metering pattern early (usage plans or app-level rate limiting).&lt;/p&gt;




&lt;h2&gt;
  
  
  When I would extend this design
&lt;/h2&gt;

&lt;p&gt;As the SaaS grows, I often add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB Streams&lt;/strong&gt; for async workflows (notifications, audit, search indexing)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQS/EventBridge&lt;/strong&gt; for background processing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fine-grained authorization model&lt;/strong&gt; (RBAC/ABAC) beyond simple groups&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tenant-level feature flags&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit log table&lt;/strong&gt; (append-only) for compliance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support-user safe impersonation&lt;/strong&gt; with strict audit trails&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silo/bridge routing&lt;/strong&gt; for enterprise tenants needing dedicated infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The nice part is that the core pattern (API Gateway + Lambda + DynamoDB + normalized auth context) still holds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;This stack is powerful because it lets me solve &lt;strong&gt;architecture and data modeling together&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The API layer, auth model, and DynamoDB key design are not separate decisions in a multi-tenant SaaS. They are one system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auth defines the tenant context&lt;/li&gt;
&lt;li&gt;The API propagates and enforces it&lt;/li&gt;
&lt;li&gt;The data model encodes it&lt;/li&gt;
&lt;li&gt;Throttling/quotas protect the platform from noisy neighbors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you get those four pieces aligned early, you can move fast without constantly revisiting foundational decisions.&lt;/p&gt;

&lt;p&gt;If I were building a new SaaS MVP today on AWS, this would still be one of my first choices.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Amazon API Gateway REST API documentation&lt;/li&gt;
&lt;li&gt;AWS Lambda developer guide&lt;/li&gt;
&lt;li&gt;Amazon DynamoDB developer guide&lt;/li&gt;
&lt;li&gt;DynamoDB single-table design patterns (AWS documentation and re:Invent talks)&lt;/li&gt;
&lt;li&gt;Amazon Cognito user pools and JWT token documentation&lt;/li&gt;
&lt;li&gt;API Gateway usage plans and API keys documentation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aws-jwt-verify&lt;/code&gt; library documentation&lt;/li&gt;
&lt;li&gt;DynamoDB best practices for partition key design and adaptive capacity&lt;/li&gt;
&lt;li&gt;AWS Well-Architected Framework (Serverless Lens)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>typescript</category>
      <category>lambda</category>
    </item>
    <item>
      <title>Step Functions Distributed Map Best Practices for Large-Scale Batch Workloads</title>
      <dc:creator>Renaldi</dc:creator>
      <pubDate>Sun, 01 Mar 2026 22:00:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/step-functions-distributed-map-best-practices-for-large-scale-batch-workloads-55n2</link>
      <guid>https://dev.to/aws-builders/step-functions-distributed-map-best-practices-for-large-scale-batch-workloads-55n2</guid>
      <description>&lt;p&gt;When I need to process a very large dataset in parallel on AWS without standing up a whole batch platform, AWS Step Functions &lt;strong&gt;Distributed Map&lt;/strong&gt; is one of the first tools I reach for.&lt;/p&gt;

&lt;p&gt;It gives me a clean orchestration layer for large-scale batch jobs while still letting me control concurrency, retries, failure thresholds, and output handling in a way that is production-friendly.&lt;/p&gt;

&lt;p&gt;In this post, I will walk through how I approach &lt;strong&gt;Distributed Map for large-scale batch workloads&lt;/strong&gt;, with a focus on practical best practices I would actually use in a real system.&lt;/p&gt;




&lt;h2&gt;
  
  
  When I choose Distributed Map (and when I do not)
&lt;/h2&gt;

&lt;p&gt;I typically consider &lt;strong&gt;Distributed Map&lt;/strong&gt; when one or more of these are true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;My dataset is too large to comfortably pass around as state input&lt;/li&gt;
&lt;li&gt;I would exceed normal inline &lt;code&gt;Map&lt;/code&gt; limits or create a huge execution history&lt;/li&gt;
&lt;li&gt;I need high concurrency across many items&lt;/li&gt;
&lt;li&gt;My input already lives in S3 (CSV, JSON, object lists, etc.)&lt;/li&gt;
&lt;li&gt;I want a serverless orchestration layer and clear visibility into the run&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I usually &lt;strong&gt;do not&lt;/strong&gt; start with Distributed Map if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The batch is small and an inline &lt;code&gt;Map&lt;/code&gt; or a single Lambda is enough&lt;/li&gt;
&lt;li&gt;I need heavy distributed compute semantics (for example, Spark-style transformations)&lt;/li&gt;
&lt;li&gt;My processing requires very long-running per-item tasks that are better suited to another service design&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key is to treat Distributed Map as an orchestration primitive for &lt;strong&gt;massively parallel item or batch processing&lt;/strong&gt;, not as a replacement for every data platform.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture at a glance
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Figure 1.&lt;/strong&gt; Reference architecture for large-scale batch processing with Step Functions Distributed Map (S3 input, batched child workflows, ResultWriter, and observability).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi9couhdi2uhu05pg02lw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi9couhdi2uhu05pg02lw.png" alt=" " width="800" height="488"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Core best practices I use in production
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Start with a lower &lt;code&gt;MaxConcurrency&lt;/code&gt; and scale up deliberately
&lt;/h3&gt;

&lt;p&gt;This is the biggest mistake I see: people let the workflow fan out too aggressively before validating downstream capacity.&lt;/p&gt;

&lt;p&gt;Distributed Map can run a very high number of child executions in parallel (AWS documents up to 10,000 parallel child workflow executions in distributed mode), which is great, but your &lt;strong&gt;downstream systems&lt;/strong&gt; (Lambda concurrency, databases, APIs, third-party services) are usually the real bottleneck.&lt;/p&gt;

&lt;p&gt;What I do instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start with a conservative &lt;code&gt;MaxConcurrency&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Load test with representative input&lt;/li&gt;
&lt;li&gt;Watch downstream latency, throttling, and error rates&lt;/li&gt;
&lt;li&gt;Increase concurrency gradually&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I treat &lt;code&gt;MaxConcurrency&lt;/code&gt; as a &lt;strong&gt;safety valve&lt;/strong&gt;, not just a performance knob.&lt;/p&gt;




&lt;h3&gt;
  
  
  2) Use &lt;code&gt;ItemBatcher&lt;/code&gt; for throughput and cost efficiency
&lt;/h3&gt;

&lt;p&gt;If each item is tiny and the per-item work is short, processing one item per child execution can create unnecessary overhead.&lt;/p&gt;

&lt;p&gt;In those cases, I use &lt;code&gt;ItemBatcher&lt;/code&gt; to group items into batches and process each batch in one child execution.&lt;/p&gt;

&lt;p&gt;Why this helps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fewer child executions&lt;/li&gt;
&lt;li&gt;Fewer state transitions&lt;/li&gt;
&lt;li&gt;Better Lambda efficiency (especially for small records)&lt;/li&gt;
&lt;li&gt;Easier downstream write batching&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tradeoff is that my worker logic must handle &lt;code&gt;Items[]&lt;/code&gt; input and return a sensible batch-level result.&lt;/p&gt;




&lt;h3&gt;
  
  
  3) Keep child outputs compact and use S3 for detailed results
&lt;/h3&gt;

&lt;p&gt;A common anti-pattern is returning large payloads from every child execution. That creates pressure on Step Functions payload limits and makes debugging harder.&lt;/p&gt;

&lt;p&gt;My default pattern is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Child workflow returns a &lt;strong&gt;compact summary&lt;/strong&gt; (counts, status, pointers)&lt;/li&gt;
&lt;li&gt;Worker writes detailed outputs/errors to S3 (if needed)&lt;/li&gt;
&lt;li&gt;Distributed Map uses &lt;code&gt;ResultWriter&lt;/code&gt; to export consolidated execution results&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps the orchestration layer lightweight while preserving traceability.&lt;/p&gt;




&lt;h3&gt;
  
  
  4) Define explicit failure tolerance (&lt;code&gt;ToleratedFailure*&lt;/code&gt;) for noisy datasets
&lt;/h3&gt;

&lt;p&gt;Large batch workloads often contain some bad records. If I expect occasional malformed rows or downstream rejections, failing the entire Map Run on the first bad batch is usually too strict.&lt;/p&gt;

&lt;p&gt;I set one of these based on business tolerance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ToleratedFailurePercentage&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ToleratedFailureCount&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This lets me absorb known-noisy inputs while still failing fast when quality degrades beyond an acceptable threshold.&lt;/p&gt;

&lt;p&gt;Important design point: these thresholds apply to &lt;strong&gt;failed/timed-out child workflow executions&lt;/strong&gt;, so I make sure my child workflow raises failures intentionally when a batch truly cannot be processed.&lt;/p&gt;




&lt;h3&gt;
  
  
  5) Put retries and error handling inside the child workflow
&lt;/h3&gt;

&lt;p&gt;I prefer retries at the point of failure (inside the &lt;code&gt;ItemProcessor&lt;/code&gt; states), not only at the parent level.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Retry transient Lambda/service errors&lt;/li&gt;
&lt;li&gt;Catch and classify non-retryable failures&lt;/li&gt;
&lt;li&gt;Return a clear batch summary when possible&lt;/li&gt;
&lt;li&gt;Fail the child execution when the batch should count toward failure tolerance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This gives me better control over what counts as a retryable blip versus a real batch failure.&lt;/p&gt;




&lt;h3&gt;
  
  
  6) Choose child execution type intentionally (&lt;code&gt;EXPRESS&lt;/code&gt; vs &lt;code&gt;STANDARD&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Distributed Map runs inside a &lt;strong&gt;Standard&lt;/strong&gt; parent state machine, but each child workflow can be &lt;code&gt;EXPRESS&lt;/code&gt; or &lt;code&gt;STANDARD&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;My practical rule of thumb:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;EXPRESS&lt;/strong&gt; child workflows for high-throughput, shorter-running item/batch tasks&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;STANDARD&lt;/strong&gt; child workflows when I need longer-running orchestration or richer execution semantics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I decide this early because it affects cost, runtime behavior, and observability patterns.&lt;/p&gt;




&lt;h3&gt;
  
  
  7) Design the worker to be idempotent
&lt;/h3&gt;

&lt;p&gt;At scale, retries and reprocessing happen. I assume they &lt;em&gt;will&lt;/em&gt; happen.&lt;/p&gt;

&lt;p&gt;That means my batch worker should be idempotent, for example by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Using deterministic item IDs&lt;/li&gt;
&lt;li&gt;Recording processed item keys/checkpoints&lt;/li&gt;
&lt;li&gt;Using conditional writes (where supported)&lt;/li&gt;
&lt;li&gt;Writing outputs with deterministic object keys (or versioned prefixes)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes retries safe and reduces cleanup work.&lt;/p&gt;




&lt;h3&gt;
  
  
  8) Treat quotas and throttling as part of the design, not an afterthought
&lt;/h3&gt;

&lt;p&gt;With large-scale fan-out, service quotas and API throttling become part of normal engineering.&lt;/p&gt;

&lt;p&gt;I usually validate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step Functions quotas relevant to Distributed Map and Map Runs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;StartExecution&lt;/code&gt; / API throttling expectations&lt;/li&gt;
&lt;li&gt;Lambda concurrency and reserved concurrency&lt;/li&gt;
&lt;li&gt;Downstream service write capacity or request limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This saves a lot of pain later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example state machine (Distributed Map with batching and ResultWriter)
&lt;/h2&gt;

&lt;p&gt;Below is a &lt;strong&gt;JSONPath-based&lt;/strong&gt; example that shows a production-style shape I like to start from.&lt;/p&gt;

&lt;p&gt;What this example demonstrates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;S3-backed input via &lt;code&gt;ItemReader&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ItemBatcher&lt;/code&gt; to process multiple records per child execution&lt;/li&gt;
&lt;li&gt;Controlled parallelism via &lt;code&gt;MaxConcurrency&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Failure tolerance via &lt;code&gt;ToleratedFailurePercentage&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ResultWriter&lt;/code&gt; to S3&lt;/li&gt;
&lt;li&gt;Retries and a clear failure path inside the child workflow
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Comment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Large-scale batch processing with Distributed Map"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"StartAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"RunDistributedBatch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"States"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"RunDistributedBatch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Map"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BatchIngestV1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ItemReader"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ReaderConfig"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"InputType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CSV"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"CSVHeaderLocation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FIRST_ROW"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::s3:getObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Bucket.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.input.bucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Key.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.input.key"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ItemSelector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"jobId.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.jobId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"record.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$$.Map.Item.Value"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ItemBatcher"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"MaxItemsPerBatch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"MaxConcurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ToleratedFailurePercentage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ItemProcessor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ProcessorConfig"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DISTRIBUTED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ExecutionType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EXPRESS"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"StartAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ProcessBatch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"States"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"ProcessBatch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::lambda:invoke"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"OutputPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.Payload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"FunctionName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:lambda:ap-southeast-2:123456789012:function:batch-worker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="nl"&gt;"Payload.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Retry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"ErrorEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                  &lt;/span&gt;&lt;span class="s2"&gt;"Lambda.ServiceException"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                  &lt;/span&gt;&lt;span class="s2"&gt;"Lambda.AWSLambdaException"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                  &lt;/span&gt;&lt;span class="s2"&gt;"Lambda.SdkClientException"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                  &lt;/span&gt;&lt;span class="s2"&gt;"States.TaskFailed"&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"IntervalSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"BackoffRate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"MaxAttempts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Catch"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"ErrorEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"States.ALL"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"ResultPath"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"Next"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MarkBatchFailed"&lt;/span&gt;&lt;span class="w"&gt;
              &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"End"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"MarkBatchFailed"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Fail"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Cause"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Batch worker failed after retries"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"ResultWriter"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"WriterConfig"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Transformation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"COMPACT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"OutputType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JSONL"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:states:::s3:putObject"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Parameters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Bucket.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.resultWriter.bucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"Prefix.$"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$.resultWriter.prefix"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"End"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Notes on this definition (why I like this shape)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;I use &lt;code&gt;ItemReader&lt;/code&gt; so the parent execution input stays small&lt;/li&gt;
&lt;li&gt;I use &lt;code&gt;ItemBatcher&lt;/code&gt; to reduce overhead for tiny records&lt;/li&gt;
&lt;li&gt;I set &lt;code&gt;MaxConcurrency&lt;/code&gt; explicitly so I do not accidentally overwhelm downstream systems&lt;/li&gt;
&lt;li&gt;I use a &lt;strong&gt;small but non-zero&lt;/strong&gt; failure tolerance only if the workload can tolerate some bad batches&lt;/li&gt;
&lt;li&gt;I use &lt;code&gt;ResultWriter&lt;/code&gt; so the Map Run does not need to return a giant in-memory result array&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Example batch worker (Python Lambda)
&lt;/h2&gt;

&lt;p&gt;This is a simplified worker pattern I use for batched processing. The main ideas are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accept &lt;code&gt;Items[]&lt;/code&gt; from &lt;code&gt;ItemBatcher&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Process each item safely&lt;/li&gt;
&lt;li&gt;Return a compact summary&lt;/li&gt;
&lt;li&gt;Fail the batch only when appropriate (so failure tolerance behaves meaningfully)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_stable_item_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="c1"&gt;# When ItemBatcher is used, Step Functions passes batched input as {"Items": [...]}
&lt;/span&gt;    &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="n"&gt;successes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;failed_items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item_wrapper&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# In this example, ItemSelector wrapped the original row into {"jobId": ..., "record": ...}
&lt;/span&gt;        &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item_wrapper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;record&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
        &lt;span class="n"&gt;item_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_stable_item_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Example validation (replace with real business logic)
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Empty record&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# TODO: process the record (write to DynamoDB/S3/etc.)
&lt;/span&gt;            &lt;span class="c1"&gt;# Make downstream writes idempotent (conditional write / deterministic key)
&lt;/span&gt;            &lt;span class="n"&gt;successes&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;failed_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;itemId&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;failure_ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;

    &lt;span class="c1"&gt;# Business decision:
&lt;/span&gt;    &lt;span class="c1"&gt;# fail the child execution only when the batch quality is unacceptable.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;failure_ratio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;successes&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Batch failed: successes=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;successes&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, failures=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, total=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PARTIAL_SUCCESS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SUCCESS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;totalItems&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;successes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;successes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failures&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;failedItemSample&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;failed_items&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Worker implementation tips I strongly recommend
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Keep the response small&lt;/li&gt;
&lt;li&gt;Write detailed failure artifacts to S3 if you need full diagnostics&lt;/li&gt;
&lt;li&gt;Use deterministic keys / conditional writes for idempotency&lt;/li&gt;
&lt;li&gt;Separate retryable vs non-retryable exceptions in your code (if possible)&lt;/li&gt;
&lt;li&gt;Emit metrics (success count, failure count, latency) from the worker&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  One retry gotcha I always call out (important)
&lt;/h2&gt;

&lt;p&gt;I am very careful with adding a &lt;code&gt;Retry&lt;/code&gt; block on the &lt;strong&gt;Distributed Map state itself&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Why? Because AWS documents that if you define retriers on the Distributed Map state, the retry policy applies to &lt;strong&gt;all child workflow executions started by that Map state&lt;/strong&gt;, not just the one that failed. In practice, that can create a lot of duplicate work and surprise costs if you are not expecting it.&lt;/p&gt;

&lt;p&gt;My default approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Put retries inside the child workflow first (fine-grained retries)&lt;/li&gt;
&lt;li&gt;Use Map-level &lt;code&gt;Retry&lt;/code&gt; only when I intentionally want to rerun the whole Map state / Map Run behavior&lt;/li&gt;
&lt;li&gt;Make sure idempotency is in place before enabling broader retries&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Observability and operations checklist (what I watch)
&lt;/h2&gt;

&lt;p&gt;When I productionize this pattern, I monitor the workflow at three levels:&lt;/p&gt;

&lt;h3&gt;
  
  
  A) Parent state machine
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Execution failures/timeouts&lt;/li&gt;
&lt;li&gt;End-to-end duration&lt;/li&gt;
&lt;li&gt;Throttling indicators (if any)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  B) Map Run / child workflow level
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Child execution success/failure trends&lt;/li&gt;
&lt;li&gt;Failure rate relative to my tolerated threshold&lt;/li&gt;
&lt;li&gt;Backlog behavior when concurrency is constrained&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  C) Worker and downstream systems
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Lambda concurrency and duration&lt;/li&gt;
&lt;li&gt;Error rate and retries&lt;/li&gt;
&lt;li&gt;Database/API throttling&lt;/li&gt;
&lt;li&gt;Write latency and saturation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also make sure the team has a runbook for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What to do when failure tolerance is exceeded&lt;/li&gt;
&lt;li&gt;How to inspect Map Run details and failing child executions&lt;/li&gt;
&lt;li&gt;When and how to redrive after fixing data or IAM issues&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Common mistakes I try to avoid
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Leaving &lt;code&gt;MaxConcurrency&lt;/code&gt; effectively unlimited
&lt;/h3&gt;

&lt;p&gt;This can look great in testing and then melt a downstream dependency in production.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Returning huge outputs from child workflows
&lt;/h3&gt;

&lt;p&gt;This adds payload pressure and makes the state machine harder to operate.&lt;/p&gt;

&lt;h3&gt;
  
  
  3) Using batch sizes without thinking about item size
&lt;/h3&gt;

&lt;p&gt;Small records and large records behave very differently. Batch by both &lt;strong&gt;count&lt;/strong&gt; and (if needed) &lt;strong&gt;input bytes&lt;/strong&gt; based on your real data.&lt;/p&gt;

&lt;h3&gt;
  
  
  4) Treating all failures the same
&lt;/h3&gt;

&lt;p&gt;Malformed data, throttling, and transient AWS service errors should not all be handled identically.&lt;/p&gt;

&lt;h3&gt;
  
  
  5) Skipping idempotency
&lt;/h3&gt;

&lt;p&gt;At scale, retries are normal. Idempotency is what keeps retries safe.&lt;/p&gt;




&lt;h2&gt;
  
  
  A practical rollout strategy I use
&lt;/h2&gt;

&lt;p&gt;If I am introducing Distributed Map into an existing system, I usually do it in phases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Prototype with a small sample dataset&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enable explicit &lt;code&gt;MaxConcurrency&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add &lt;code&gt;ItemBatcher&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add &lt;code&gt;ResultWriter&lt;/code&gt; and compact outputs&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Load test with realistic record sizes&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Tune retries, batch size, and failure thresholds&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Document redrive/runbook steps&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This sequence helps me avoid premature complexity while still landing on a production-grade design.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Distributed Map is one of those features that becomes incredibly powerful once I stop thinking of it as “just a bigger map loop” and start treating it as a &lt;strong&gt;batch orchestration framework&lt;/strong&gt; with explicit controls.&lt;/p&gt;

&lt;p&gt;The best results usually come from combining:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;S3-backed input&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deliberate concurrency limits&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Batched child processing&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compact outputs + S3 result export&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clear failure semantics&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Good observability and runbooks&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That combination gives me something that scales well and is still operationally sane.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/state-map-distributed.html" rel="noopener noreferrer"&gt;AWS Step Functions: Distributed Map (Developer Guide)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/state-map.html" rel="noopener noreferrer"&gt;AWS Step Functions: Map state (inline vs distributed)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/input-output-itemreader.html" rel="noopener noreferrer"&gt;AWS Step Functions: ItemReader (Map)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/input-output-itembatcher.html" rel="noopener noreferrer"&gt;AWS Step Functions: ItemBatcher (Map)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/input-output-resultwriter.html" rel="noopener noreferrer"&gt;AWS Step Functions: ResultWriter (Map)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/concepts-examine-map-run.html" rel="noopener noreferrer"&gt;AWS Step Functions: Viewing a Distributed Map Run execution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/procedure-cw-metrics.html" rel="noopener noreferrer"&gt;AWS Step Functions: Monitoring metrics with CloudWatch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/redrive-map-run.html" rel="noopener noreferrer"&gt;AWS Step Functions: Redriving Map Runs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/service-quotas.html" rel="noopener noreferrer"&gt;AWS Step Functions: Service quotas&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://catalog.workshops.aws/large-scale-data-processing-with-step-functions/en-US" rel="noopener noreferrer"&gt;AWS Step Functions Workshop: Large-scale data processing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>architecture</category>
      <category>aws</category>
      <category>distributedsystems</category>
      <category>serverless</category>
    </item>
    <item>
      <title>How to Monitor Event Delivery in Amazon EventBridge</title>
      <dc:creator>Renaldi</dc:creator>
      <pubDate>Wed, 25 Feb 2026 05:06:19 +0000</pubDate>
      <link>https://dev.to/aws-builders/how-to-monitor-event-delivery-in-amazon-eventbridge-4bno</link>
      <guid>https://dev.to/aws-builders/how-to-monitor-event-delivery-in-amazon-eventbridge-4bno</guid>
      <description>&lt;p&gt;When I first started using Amazon EventBridge more heavily, I realized something important pretty quickly: &lt;strong&gt;it is very easy to build event-driven flows, but much harder to know when delivery is degrading before things break&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This post is specifically about &lt;strong&gt;best practices for monitoring event delivery in EventBridge&lt;/strong&gt; (not just target-side application monitoring). AWS actually has strong guidance here, and the key is to monitor &lt;strong&gt;a combination of retry, success, failure, DLQ, and latency metrics&lt;/strong&gt; together instead of looking at a single number in isolation. &lt;/p&gt;

&lt;p&gt;I’ll walk through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what to monitor&lt;/li&gt;
&lt;li&gt;what I alert on (and why)&lt;/li&gt;
&lt;li&gt;an architecture pattern that works well&lt;/li&gt;
&lt;li&gt;practical alarm design so you do not get spammed all day&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why this matters
&lt;/h2&gt;

&lt;p&gt;AWS recommends monitoring EventBridge delivery behavior because underperforming or undersized targets can cause &lt;strong&gt;excessive retries, delivery delays, and permanent delivery failures&lt;/strong&gt;. They also explicitly recommend combining multiple metrics and setting alarms/dashboards for early detection.&lt;/p&gt;

&lt;p&gt;Also, by default, EventBridge retries failed target delivery for &lt;strong&gt;up to 24 hours and up to 185 times&lt;/strong&gt; (with exponential backoff and jitter). If retries are exhausted, the event is dropped unless you configured a DLQ.&lt;/p&gt;

&lt;p&gt;That default behavior is great for resilience, but it also means a problem can quietly turn into a backlog / latency issue if you are not watching the right signals.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture I use for monitoring EventBridge delivery
&lt;/h2&gt;

&lt;p&gt;Here is the monitoring architecture pattern I like for production workloads.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe822qmqcc5wdthhjfuzv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe822qmqcc5wdthhjfuzv.png" alt=" " width="800" height="236"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Best-practice notes in this architecture
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;I prefer &lt;strong&gt;a DLQ per rule target&lt;/strong&gt; (or at least per critical target path), because AWS recommends configuring a dead-letter queue for each rule target to avoid losing undelivered events.&lt;/li&gt;
&lt;li&gt;I keep &lt;strong&gt;CloudWatch alarms on EventBridge metrics&lt;/strong&gt; and &lt;strong&gt;separate alarms on the target service&lt;/strong&gt; (for example Lambda errors, API 5xx, Step Functions failures). EventBridge tells me about delivery behavior; target metrics tell me what is happening after delivery.&lt;/li&gt;
&lt;li&gt;I treat the DLQ as a &lt;strong&gt;diagnostics feed&lt;/strong&gt;, not just a parking lot.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The core EventBridge metrics I monitor (and why)
&lt;/h2&gt;

&lt;p&gt;AWS’s EventBridge best-practices page specifically calls out a small set of metrics for delivery monitoring, and I use those as the foundation.&lt;/p&gt;

&lt;h3&gt;
  
  
  1) &lt;code&gt;InvocationAttempts&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is the &lt;strong&gt;total number of times EventBridge attempted to invoke a target&lt;/strong&gt;, including retries.&lt;/p&gt;

&lt;p&gt;Why I care:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It gives me the denominator for delivery health.&lt;/li&gt;
&lt;li&gt;It helps me see if retries are inflating traffic to a struggling target.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  2) &lt;code&gt;SuccessfulInvocationAttempts&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is the number of invocation attempts where EventBridge successfully delivered the event to the target.&lt;/p&gt;

&lt;p&gt;Why I care:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is the numerator for a success-rate metric.&lt;/li&gt;
&lt;li&gt;On its own, it can look healthy even when retries are growing, so I always compare it with &lt;code&gt;InvocationAttempts&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  3) &lt;code&gt;RetryInvocationAttempts&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is the number of invocation attempts that were retries. AWS explicitly says an increase here may be an early indication of an undersized target.&lt;/p&gt;

&lt;p&gt;Why I care:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This is usually my &lt;strong&gt;earliest warning signal&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;I can catch target stress before it becomes permanent failure.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  4) &lt;code&gt;FailedInvocations&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is the number of invocations that &lt;strong&gt;failed permanently&lt;/strong&gt; (not retried, or not recovered after retry), with some exclusions noted in the metric definition.&lt;/p&gt;

&lt;p&gt;Why I care:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This is a direct “something is broken” signal.&lt;/li&gt;
&lt;li&gt;If this rises and there is no DLQ, I am at risk of event loss after retries are exhausted.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  5) &lt;code&gt;InvocationsSentToDlq&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is the number of invocations moved to a dead-letter queue.&lt;/p&gt;

&lt;p&gt;Why I care:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This tells me failed deliveries are now becoming &lt;strong&gt;operational work&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Even if the system is “surviving,” this still means customers or downstream processes may be impacted.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  6) &lt;code&gt;InvocationsFailedToBeSentToDlq&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is the nightmare metric: EventBridge tried to move failed invocations to the DLQ but could not (for example due to permissions, unavailable resource, or size issues). AWS documents this metric and notes it is emitted only when non-zero.&lt;/p&gt;

&lt;p&gt;Why I care:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This is a &lt;strong&gt;critical alarm&lt;/strong&gt; for me.&lt;/li&gt;
&lt;li&gt;It means my failure safety net is not working.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  7) &lt;code&gt;IngestionToInvocationSuccessLatency&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;AWS recommends this latency metric for detecting event delivery delays. It measures end-to-end latency from ingestion to successful delivery and can surface delays from retries, timeouts, or slow target responses. It also includes the target’s time to respond successfully.&lt;/p&gt;

&lt;p&gt;Why I care:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This catches degraded delivery even when there are no obvious hard failures.&lt;/li&gt;
&lt;li&gt;It is great for detecting “everything is technically working, but much slower than usual.”&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The most useful derived metric: &lt;code&gt;SuccessfulInvocationRate&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;AWS explicitly recommends combining metrics and even gives a metric math example:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SuccessfulInvocationRate = SuccessfulInvocationAttempts / InvocationAttempts&lt;/code&gt;&lt;br&gt;
I strongly agree with this. In practice, I always graph and alert on a success rate, not just raw counts.&lt;/p&gt;

&lt;h3&gt;
  
  
  CloudWatch metric math expression (example)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SuccessfulInvocationRate = SuccessfulInvocationAttempts / InvocationAttempts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  My practical implementation tip
&lt;/h3&gt;

&lt;p&gt;I only evaluate this alarm when there is meaningful traffic. Otherwise, low-volume rules can produce noisy alerts (for example, 1 failed attempt on a rarely used rule).&lt;/p&gt;

&lt;p&gt;A common pattern is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;volume guard&lt;/strong&gt; (enough attempts in the window)&lt;/li&gt;
&lt;li&gt;plus a &lt;strong&gt;success-rate threshold&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I alert on (my starting best-practice alarm set)
&lt;/h2&gt;

&lt;p&gt;Below is a practical alert set I use as a starting point. The exact thresholds should be tuned to your workload and business SLOs.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I treat these as &lt;strong&gt;starting thresholds&lt;/strong&gt;, not universal truths.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Alert 1 (Critical): &lt;code&gt;InvocationsFailedToBeSentToDlq &amp;gt; 0&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Severity:&lt;/strong&gt; P1 / Critical&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Why:&lt;/strong&gt; The DLQ safety net itself is failing. AWS notes this can happen due to permission issues, unavailable resources, or size limits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I do immediately&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check SQS queue existence and region&lt;/li&gt;
&lt;li&gt;Check SQS resource policy / permissions&lt;/li&gt;
&lt;li&gt;Check encryption / access configuration if applicable&lt;/li&gt;
&lt;li&gt;Check message size / payload patterns&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Alert 2 (High): &lt;code&gt;FailedInvocations &amp;gt; 0&lt;/code&gt; on critical rules
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Severity:&lt;/strong&gt; P1 or P2 (depends on rule criticality)&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Why:&lt;/strong&gt; Permanent delivery failure is already happening. AWS documents &lt;code&gt;FailedInvocations&lt;/code&gt; as permanently failed invocations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best practice&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scope alarms &lt;strong&gt;per rule&lt;/strong&gt; for critical paths&lt;/li&gt;
&lt;li&gt;Route alerts to the owning team, not one giant shared channel&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Alert 3 (High): &lt;code&gt;InvocationsSentToDlq &amp;gt; 0&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Severity:&lt;/strong&gt; P2 (or P1 for critical business flows)&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Why:&lt;/strong&gt; Failures are being captured safely, but delivery is still failing and backlog is building. AWS best-practice guidance specifically calls out watching &lt;code&gt;FailedInvocations&lt;/code&gt; and &lt;code&gt;InvocationsSentToDlq&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I do&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Triage DLQ message attributes (&lt;code&gt;ERROR_CODE&lt;/code&gt;, &lt;code&gt;ERROR_MESSAGE&lt;/code&gt;, &lt;code&gt;RETRY_ATTEMPTS&lt;/code&gt;, etc.) to identify the root cause faster. EventBridge includes these attributes in DLQ messages.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Alert 4 (Warning): &lt;code&gt;RetryInvocationAttempts&lt;/code&gt; rising above baseline
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Severity:&lt;/strong&gt; Warning / P3&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Why:&lt;/strong&gt; AWS says an increase may be an early indication of an undersized target.&lt;/p&gt;

&lt;p&gt;This is one of my favorite early-warning alerts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How I tune it&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For steady traffic: alert on absolute count per period&lt;/li&gt;
&lt;li&gt;For bursty traffic: alert on retry rate (retry attempts / invocation attempts)&lt;/li&gt;
&lt;li&gt;Use a longer evaluation window to avoid false positives during short spikes&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Alert 5 (Warning/High): &lt;code&gt;SuccessfulInvocationRate&lt;/code&gt; below threshold
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Severity:&lt;/strong&gt; Warning or P2&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Why:&lt;/strong&gt; This gives a single health signal that accounts for retries and success together (exactly what AWS recommends via metric math).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example starting thresholds&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Warning: &lt;code&gt;&amp;lt; 99.9%&lt;/code&gt; for 10 minutes (high-volume, sensitive systems)&lt;/li&gt;
&lt;li&gt;High: &lt;code&gt;&amp;lt; 99%&lt;/code&gt; for 5–10 minutes&lt;/li&gt;
&lt;li&gt;Critical: &lt;code&gt;&amp;lt; 95%&lt;/code&gt; for 5 minutes on critical paths&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Again, tune these to your business tolerance.&lt;/p&gt;




&lt;h3&gt;
  
  
  Alert 6 (Warning): &lt;code&gt;IngestionToInvocationSuccessLatency&lt;/code&gt; p95/p99 above baseline
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Severity:&lt;/strong&gt; Warning or P2&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Why:&lt;/strong&gt; AWS recommends using this metric to detect delays, and it surfaces retry effects and slow target responses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My recommendation&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Alert on &lt;strong&gt;p95&lt;/strong&gt; or &lt;strong&gt;p99&lt;/strong&gt; rather than average&lt;/li&gt;
&lt;li&gt;Use a baseline per rule (or per target type), because latency expectations differ a lot&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Alert 7 (Warning): &lt;code&gt;ThrottledRules &amp;gt; 0&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Severity:&lt;/strong&gt; Warning / P2 if persistent&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Why:&lt;/strong&gt; AWS documents that throttled rule executions may delay invocations.&lt;/p&gt;

&lt;p&gt;This is especially useful when traffic grows and quota pressure starts to show up.&lt;/p&gt;




&lt;h2&gt;
  
  
  My dashboard layout (simple but effective)
&lt;/h2&gt;

&lt;p&gt;I try not to overcomplicate the dashboard. I keep one dashboard section per critical rule / target path, and I always put these together:&lt;/p&gt;

&lt;h3&gt;
  
  
  Panel group A: Delivery health
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;InvocationAttempts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SuccessfulInvocationAttempts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RetryInvocationAttempts&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FailedInvocations&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;InvocationsSentToDlq&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Panel group B: Delivery quality
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SuccessfulInvocationRate&lt;/code&gt; (metric math)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;IngestionToInvocationSuccessLatency&lt;/code&gt; (p50, p95, p99)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Panel group C: Protection / risk
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;InvocationsFailedToBeSentToDlq&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ThrottledRules&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matches AWS’s “combine multiple metrics for a holistic overview” guidance, and in practice it makes troubleshooting much faster.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical runbook: how I interpret the pattern combinations
&lt;/h2&gt;

&lt;p&gt;This is the part that saves the most time during incidents.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pattern 1: Retries rising, success rate still high
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Likely meaning:&lt;/strong&gt; target is stressed but still mostly coping&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Metrics pattern:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;RetryInvocationAttempts&lt;/code&gt; up&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SuccessfulInvocationRate&lt;/code&gt; mostly stable&lt;/li&gt;
&lt;li&gt;latency rising (&lt;code&gt;IngestionToInvocationSuccessLatency&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What I check&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;target concurrency / scaling&lt;/li&gt;
&lt;li&gt;downstream dependency latency&lt;/li&gt;
&lt;li&gt;throttling on target service&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Pattern 2: Retries rising, success rate dropping, DLQ starts filling
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Likely meaning:&lt;/strong&gt; target is underprovisioned or misconfigured&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Metrics pattern:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;RetryInvocationAttempts&lt;/code&gt; up&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SuccessfulInvocationRate&lt;/code&gt; down&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;InvocationsSentToDlq&lt;/code&gt; &amp;gt; 0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What I check&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;target capacity / rate limits&lt;/li&gt;
&lt;li&gt;permission changes&lt;/li&gt;
&lt;li&gt;endpoint availability / DNS / networking&lt;/li&gt;
&lt;li&gt;recent deployments&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Pattern 3: Failed invocations and DLQ-send failures
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Likely meaning:&lt;/strong&gt; primary path failing and safety net misconfigured&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Metrics pattern:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;FailedInvocations&lt;/code&gt; &amp;gt; 0&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;InvocationsFailedToBeSentToDlq&lt;/code&gt; &amp;gt; 0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What I do&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;page immediately&lt;/li&gt;
&lt;li&gt;validate DLQ queue, region, permissions, and policy&lt;/li&gt;
&lt;li&gt;verify EventBridge can &lt;code&gt;SendMessage&lt;/code&gt; to the SQS DLQ&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS’s DLQ guidance includes permission requirements and examples, and this is the first place I look when DLQ delivery fails.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common mistakes I see (and try to avoid)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Only alerting on &lt;code&gt;FailedInvocations&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This catches issues late. By the time permanent failures show up, you may already have retries, delays, and customer impact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better:&lt;/strong&gt; also alert on retries and latency.&lt;/p&gt;




&lt;h3&gt;
  
  
  2) No DLQ configured
&lt;/h3&gt;

&lt;p&gt;AWS explicitly recommends configuring a DLQ for each rule target to avoid losing undelivered events.&lt;/p&gt;




&lt;h3&gt;
  
  
  3) No alarm on &lt;code&gt;InvocationsFailedToBeSentToDlq&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This is the “we thought we were safe, but we are not” blind spot.&lt;/p&gt;




&lt;h3&gt;
  
  
  4) One global alarm for all rules
&lt;/h3&gt;

&lt;p&gt;This creates noisy alerts and slow triage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better:&lt;/strong&gt; alarm &lt;strong&gt;per critical rule / target path&lt;/strong&gt;, and map ownership.&lt;/p&gt;




&lt;h3&gt;
  
  
  5) Using static latency thresholds with no baseline thinking
&lt;/h3&gt;

&lt;p&gt;Different targets behave differently. A Lambda target and an API destination will not share the same normal latency profile.&lt;/p&gt;




&lt;h2&gt;
  
  
  A practical starting checklist (copy this into your ops notes)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Configure &lt;strong&gt;DLQ for each critical EventBridge rule target&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;[ ] Alarm on &lt;code&gt;InvocationsFailedToBeSentToDlq &amp;gt; 0&lt;/code&gt; (critical)&lt;/li&gt;
&lt;li&gt;[ ] Alarm on &lt;code&gt;FailedInvocations &amp;gt; 0&lt;/code&gt; for critical rules&lt;/li&gt;
&lt;li&gt;[ ] Alarm on &lt;code&gt;InvocationsSentToDlq &amp;gt; 0&lt;/code&gt; for critical rules&lt;/li&gt;
&lt;li&gt;[ ] Track &lt;code&gt;RetryInvocationAttempts&lt;/code&gt; trend (early warning)&lt;/li&gt;
&lt;li&gt;[ ] Create &lt;code&gt;SuccessfulInvocationRate&lt;/code&gt; metric math and alert on sustained dips&lt;/li&gt;
&lt;li&gt;[ ] Track &lt;code&gt;IngestionToInvocationSuccessLatency&lt;/code&gt; p95/p99 and alert on sustained increase&lt;/li&gt;
&lt;li&gt;[ ] Add &lt;code&gt;ThrottledRules&lt;/code&gt; alert if traffic growth is expected&lt;/li&gt;
&lt;li&gt;[ ] Build per-rule dashboards for critical flows
&lt;/li&gt;
&lt;li&gt;[ ] Document a DLQ triage + replay runbook&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;If I had to summarize the best practice in one sentence, it would be this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not monitor EventBridge delivery with a single failure metric. Monitor retries, success rate, DLQ activity, and delivery latency together.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That combination gives me an early warning signal, a confirmed-failure signal, and a customer-impact signal, which is exactly what I want in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AWS EventBridge monitoring best practices for event delivery (recommended metrics, success-rate metric math, retries/DLQ guidance). (&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-monitoring-events-best-practices.html" rel="noopener noreferrer"&gt;AWS Documentation&lt;/a&gt;)
&lt;/li&gt;
&lt;li&gt;AWS EventBridge monitoring metrics reference (&lt;code&gt;AWS/Events&lt;/code&gt; metrics and definitions). (&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-monitoring.html" rel="noopener noreferrer"&gt;AWS Documentation&lt;/a&gt;)
&lt;/li&gt;
&lt;li&gt;AWS EventBridge retry behavior (default retry duration and count). (&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rule-retry-policy.html" rel="noopener noreferrer"&gt;AWS Documentation&lt;/a&gt;)
&lt;/li&gt;
&lt;li&gt;AWS EventBridge DLQ guidance (DLQ behavior, limitations, permissions, message attributes). (&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-rule-dlq.html" rel="noopener noreferrer"&gt;AWS Documentation&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>eventbridge</category>
      <category>eventdriven</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Efficiently Handling High Concurrency with AWS Lambda SnapStart: A Step-by-Step Guide</title>
      <dc:creator>Renaldi</dc:creator>
      <pubDate>Mon, 18 Nov 2024 01:06:10 +0000</pubDate>
      <link>https://dev.to/aws-builders/efficiently-handling-high-concurrency-with-aws-lambda-snapstart-a-step-by-step-guide-2m81</link>
      <guid>https://dev.to/aws-builders/efficiently-handling-high-concurrency-with-aws-lambda-snapstart-a-step-by-step-guide-2m81</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Modern online services frequently face unexpected surges in user activity. It's essential that your system can process multiple simultaneous requests efficiently to keep users satisfied and engaged. To address performance challenges in serverless environments, AWS offers Lambda SnapStart. This enhancement reduces function initialization time, helping maintain responsiveness when demand increases. We'll explore a real-world example demonstrating when this capability becomes valuable, and provide detailed instructions for setting it up in your own environment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw019gmnny89jl4vu2imb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw019gmnny89jl4vu2imb.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Scenario Overview
&lt;/h2&gt;

&lt;p&gt;Consider operating a web-based event admission system that sells access to live performances and gatherings. When highly anticipated shows become available for purchase, your platform experiences a sudden influx of concurrent visitors. To ensure smooth transaction processing during these peak periods, your system infrastructure must expand rapidly while maintaining quick response times for each customer interaction. By implementing Amazon's Lambda SnapStart functionality, you can minimize initialization delays in your cloud functions, enabling better performance during these intense usage periods.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is AWS Lambda SnapStart?
&lt;/h2&gt;

&lt;p&gt;AWS' Lambda SnapStart enhances function response times by performing pre-initialization and creating a cached memory state that can be reused for subsequent executions. This approach captures a ready-to-use version of your code, allowing new instances to launch more quickly. By eliminating the standard initialization delay typically experienced during first-time function calls, this capability particularly benefits applications that need to handle many simultaneous user requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Use Lambda SnapStart in This Scenario?
&lt;/h2&gt;

&lt;p&gt;For an event ticketing service, speed is absolutely critical. When customers attempt to secure their spots, even slight delays can frustrate buyers and potentially cost you business. Implementing Amazon's SnapStart technology for serverless functions helps ensure rapid processing times, maintaining system responsiveness even during peak demand. This approach enables consistent, swift service delivery regardless of how many people are simultaneously trying to purchase tickets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step-by-Step Implementation Guide
&lt;/h2&gt;

&lt;p&gt;Follow these steps to implement AWS Lambda with SnapStart for your ticketing platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Create a New Lambda Function&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;On the AWS Lambda page, click on the "Create function" button.&lt;/li&gt;
&lt;li&gt;Under "Create function", choose "Author from scratch".&lt;/li&gt;
&lt;li&gt;Fill in the following details:&lt;/li&gt;
&lt;li&gt;Function name: TicketingProcessor&lt;/li&gt;
&lt;li&gt;Runtime: Select "Java 17"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Note: Lambda SnapStart currently supports Java runtimes. We'll use Java 17 for this example.&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Under Permissions, expand the "Change default execution role" section.&lt;/li&gt;
&lt;li&gt;Select "Create a new role with basic Lambda permissions".&lt;/li&gt;
&lt;li&gt;Click on "Create function" at the bottom of the page.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Write the Lambda Function Code&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;After the function is created, you'll be taken to the function's configuration page.&lt;/li&gt;
&lt;li&gt;Scroll down to the "Code source" section.&lt;/li&gt;
&lt;li&gt;Under "Code source", click on the file named LambdaFunction.java to open the code editor.&lt;/li&gt;
&lt;li&gt;Replace the existing code with the following Java code:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import java.util.HashMap;
import java.util.Map;

public class TicketingProcessor implements RequestHandler&amp;lt;Map&amp;lt;String, String&amp;gt;, Map&amp;lt;String, String&amp;gt;&amp;gt; {

    // Simulate heavy initialization logic
    static {
        try {
            // Simulate time-consuming startup tasks
            Thread.sleep(5000); // 5-second delay to simulate cold start
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public Map&amp;lt;String, String&amp;gt; handleRequest(Map&amp;lt;String, String&amp;gt; event, Context context) {
        Map&amp;lt;String, String&amp;gt; response = new HashMap&amp;lt;&amp;gt;();
        response.put("message", "Ticket processed successfully!");
        return response;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code simulates a Lambda function with heavy initialization (the static block that sleeps for 5 seconds). SnapStart will help us bypass this delay in subsequent invocations.&lt;/p&gt;

&lt;p&gt;Click on "Deploy" at the top right corner to save and deploy the code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Configure SnapStart for the Lambda Function&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the left-hand menu, under "Versioning", click on "Versions".&lt;/li&gt;
&lt;li&gt;Click on "Publish new version" at the top right.&lt;/li&gt;
&lt;li&gt;In the "Publish new version" dialog, for Version description, enter Initial version with SnapStart.&lt;/li&gt;
&lt;li&gt;Under "SnapStart", select "Enable SnapStart".&lt;/li&gt;
&lt;li&gt;Click on "Publish".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;Note: If you don't see the SnapStart option, ensure that you are using a supported runtime (Java 11 or Java 17). Enabling SnapStart during the publishing of a new version tells AWS to take a snapshot after initialization, which will be used for faster startups.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Test the Lambda Function&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Navigate back to your function by clicking on "Code" in the left-hand menu.&lt;/li&gt;
&lt;li&gt;Click on "Test" at the top right corner.&lt;/li&gt;
&lt;li&gt;In the "Configure test event" dialog:&lt;/li&gt;
&lt;li&gt;Select "Create new test event".&lt;/li&gt;
&lt;li&gt;Event template: Choose "Hello World".&lt;/li&gt;
&lt;li&gt;Event name: Enter TestEvent.&lt;/li&gt;
&lt;li&gt;Leave the default JSON as is:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "key1": "value1",
  "key2": "value2",
  "key3": "value3"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click on "Create". Click on "Test" again to invoke the function. Check the "Execution result" section below. You should see a response &lt;br&gt;
similar to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "message": "Ticket processed successfully!"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the "Duration" in the "Summary" section. It should show a reduced execution time due to SnapStart on subsequent invocations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Simulate High Concurrency&lt;/strong&gt;&lt;br&gt;
To test the function under high concurrency, we'll invoke it multiple times in quick succession.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1:&lt;/strong&gt; Use AWS Lambda Console's "Test" Feature Repeatedly&lt;br&gt;
You can invoke the function multiple times manually to observe the performance improvement.&lt;br&gt;
&lt;strong&gt;Option 2:&lt;/strong&gt; Use AWS CLI to Invoke the Function Concurrently&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Install AWS CLI: If you haven't installed AWS CLI, follow the installation guide &lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Configure AWS CLI: Run aws configure in your terminal and enter your AWS credentials.&lt;/li&gt;
&lt;li&gt;In the AWS Lambda console, on your function's page, note the "ARN" at the top. It looks like arn:aws:lambda:region:account-id:function:TicketingProcessor.&lt;/li&gt;
&lt;li&gt;Create a Script to Invoke the Function Concurrently. Create a file named invoke_lambda.sh with the following content:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash

FUNCTION_NAME="TicketingProcessor"
REGION="your-region" # e.g., us-east-1

for i in {1..100}
do
  aws lambda invoke --function-name $FUNCTION_NAME --region $REGION --payload '{}' response_$i.json &amp;amp;
done

wait
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Replace your-region with your AWS region, such as us-west-2.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 6: Provide the Relevant Permissions and Test&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make the Script Executable by running chmod +x invoke_lambda.sh in your terminal.&lt;/li&gt;
&lt;li&gt;Run the Script by executing ./invoke_lambda.sh to invoke the Lambda function 100 times concurrently.&lt;/li&gt;
&lt;li&gt;Check the Results.&lt;/li&gt;
&lt;li&gt;The responses will be saved in files named response_1.json, response_2.json, ..., response_100.json.&lt;/li&gt;
&lt;li&gt;You can also check the "Monitoring" tab in the AWS Lambda console to see the invocation metrics.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Step 7: Review Performance Metrics&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the AWS Lambda console, navigate to your function's page.&lt;/li&gt;
&lt;li&gt;Click on the "Monitoring" tab.&lt;/li&gt;
&lt;li&gt;Observe the metrics:&lt;/li&gt;
&lt;li&gt;Invocations: Number of times your function was invoked.&lt;/li&gt;
&lt;li&gt;Duration: Time taken for each invocation.&lt;/li&gt;
&lt;li&gt;Concurrency: Number of concurrent executions.&lt;/li&gt;
&lt;li&gt;Errors: Any errors that occurred during execution.&lt;/li&gt;
&lt;li&gt;You should notice that the "Duration" metric shows reduced cold start times due to SnapStart, especially after the initial invocation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Final Notes:&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ensure that your AWS Identity and Access Management (IAM) role has the necessary permissions to execute Lambda functions and access AWS services.&lt;/li&gt;
&lt;li&gt;Be aware that invoking Lambda functions may incur costs. Refer to the &lt;a href="https://aws.amazon.com/lambda/pricing/" rel="noopener noreferrer"&gt;AWS Lambda Pricing&lt;/a&gt; page for more details.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
These implementation steps have shown you how to leverage Amazon's SnapStart capability to enhance your serverless application's responsiveness during peak loads. With this optimization in place, your event ticketing system can now better manage unexpected surges of visitor activity, maintaining quick response times and keeping your customers satisfied throughout their purchasing journey.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Additional Resources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/snapstart.html" rel="noopener noreferrer"&gt;AWS Lambda SnapStart Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/perf-optimize.html" rel="noopener noreferrer"&gt;Optimizing AWS Lambda Performance&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>lambda</category>
      <category>java</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Implementation of Melodistiq: Generating Lyrics and Melodies with AI</title>
      <dc:creator>Renaldi</dc:creator>
      <pubDate>Thu, 25 Apr 2024 01:00:41 +0000</pubDate>
      <link>https://dev.to/aws-builders/unlocking-musical-creativity-with-ai-generating-lyrics-and-melodies-55p0</link>
      <guid>https://dev.to/aws-builders/unlocking-musical-creativity-with-ai-generating-lyrics-and-melodies-55p0</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Hello, fellow cloud enthusiasts and builders! &lt;/p&gt;

&lt;p&gt;In today's blog post, we will dive into an intriguing project that combines artificial intelligence with music creation. This post will detail the implementation of a Python code designed to generate music lyrics and melodies using AI technologies. This follows the successful presentation of the "Unlocking Musical Creativity with AI: Generating Lyrics and Melodies" talk.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd5zxquy5h3vn4p3x2qks.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd5zxquy5h3vn4p3x2qks.png" alt=" " width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Scenario
&lt;/h2&gt;

&lt;p&gt;The goal of this project is to automate the creation of music lyrics and melodies. This can be particularly useful for musicians seeking inspiration or developers exploring the intersection of AI and creative arts. The process involves generating lyrics based on existing song data, creating a melody that fits the generated lyrics, and presenting the final composition in both audio and written formats.&lt;/p&gt;

&lt;h2&gt;
  
  
  Libraries and Tools Used
&lt;/h2&gt;

&lt;p&gt;To accomplish this task, we utilize several Python libraries, each serving a distinct purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pandas (pandas): A powerful data manipulation library used here to handle and preprocess lyrical data.&lt;/li&gt;
&lt;li&gt;Natural Language Toolkit (NLTK): This library is used for text processing to analyze, manipulate, and generate text data.&lt;/li&gt;
&lt;li&gt;OpenAI's GPT-3.5: Leveraged to enhance the quality of the generated lyrics.&lt;/li&gt;
&lt;li&gt;Mingus: An advanced music theory and notation package used to handle music data and generate melodies.&lt;/li&gt;
&lt;li&gt;FastText: A library developed by Facebook for efficient learning of word representations and sentence classification.&lt;/li&gt;
&lt;li&gt;FPDF: A library to generate PDF files, useful for presenting the lyrics in a readable format.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's walk through implementing our code now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Setting Up and Downloading Models&lt;/strong&gt;&lt;br&gt;
First, we load necessary models and corpora:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import fasttext
import fasttext.util
import nltk
nltk.download('cmudict')
nltk.download('punkt')
from nltk.corpus import cmudict
d = cmudict.dict()

fasttext.util.download_model('en', if_exists='ignore')
ft = fasttext.load_model('cc.en.300.bin')

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, fastText and NLTK's CMU Pronouncing Dictionary are initialized. fastText is used later to find words similar to those not found in the CMU dictionary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data Collection&lt;/strong&gt;&lt;br&gt;
Data is loaded and preprocessed from a CSV file containing song lyrics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import pandas as pd
df = pd.read_csv('EdSheeran.csv')
df = df.dropna()
lyrics = df['Lyric'].str.replace('\n', ' ').str.replace('\r', ' ').tolist()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This section reads a CSV file, cleans the data by removing missing values, and formats the lyrics into a list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data Preprocessing&lt;/strong&gt;&lt;br&gt;
The lyrics are tokenized and cleaned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import re
from nltk.tokenize import word_tokenize
words = [word_tokenize(re.sub(r'\W+', ' ', lyric).lower()) for lyric in lyrics]
words = [word for sublist in words for word in sublist]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, special characters are removed, and the text is converted to lowercase to standardize the data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;N-gram Model and Lyrics Generation&lt;/strong&gt;&lt;br&gt;
An N-gram model is used to generate new lyrics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;N-gram Model and Lyrics Generation

An N-gram model is used to generate new lyrics:

from nltk.util import ngrams
from nltk.probability import FreqDist
import random

n_values = [2, 5, 7]
generated_lyrics = ""

for n in n_values:
    ngrams_list = list(ngrams(words, n, pad_left=True, pad_right=True))
    freq_dist = FreqDist(ngrams_list)

    def generate_lyrics(starting_ngram, freq_dist, num_words):
        generated_words = list(starting_ngram)
        for _ in range(num_words):
            next_word_candidates = [ngram[-1] for ngram in freq_dist.keys() if ngram[:n-1] == tuple(generated_words[-(n-1):])]
            if next_word_candidates:
                next_word = random.choice(next_word_candidates)
                generated_words.append(next_word)
            else:
                break
        return ' '.join(generated_words).replace(' ,', ',').replace(' .', '.').replace(' ;', ';')

    starting_ngram = random.choice(list(freq_dist.keys()))
    generated_lyrics += generate_lyrics(starting_ngram, freq_dist, 200)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This segment builds N-grams from the cleaned words and creates a frequency distribution to model the probabilities of word sequences. New lyrics are generated based on these probabilities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enhancing Lyrics with GPT-3.5&lt;/strong&gt;&lt;br&gt;
We then look to enhance our lyrics with the help of GPT-3.5.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import openai
import os
openai.api_key = os.getenv('openai_api_key')
conversations = {}
session_id=0;
conversations[session_id] = []

conversations[session_id].append({"role": "system", "content": "You are a helpful assistant who will transform the lyrics below into a song."})
conversations[session_id].append({"role": "user", "content": generated_lyrics})

response = openai.ChatCompletion.create(
  model="gpt-3.5-turbo",
  messages=conversations[session_id],
  max_tokens=200
)

gpt_lyrics = response.choices[0]["message"]["content"].strip()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, the OpenAI GPT-3.5 API is used to refine and enhance the generated lyrics, adding a layer of complexity and polish that might be lacking from the simple N-gram model. The environment variable openai_api_key is used to authenticate the API request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generating and Exporting the Melody&lt;/strong&gt;&lt;br&gt;
The code then generates a melody using the Mingus library based on the stress patterns of the lyrics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from mingus.containers import Note, Bar, Track
from mingus.midi import midi_file_out
import mingus.extra.lilypond as lilypond

def generate_melody(lyrics):
    # Load the CMU Pronouncing Dictionary
    stress_pattern = []

    tokens = nltk.word_tokenize(lyrics)

    # Fix contractions
    fixed_tokens = [contractions.fix(token) for token in tokens]

    # Function to get the stress pattern of a word
    def get_stress(word):
        word = word.lower()
        phones = pronouncing.phones_for_word(word)
        if phones:
            stress_pattern = [int(s) for s in pronouncing.stresses(phones[0])]
            return [stress_pattern]
        else:
            # handle contractions
            if "'" in word:
                parts = word.split("'")
                stress_pattern = []
                for part in parts:
                    stress_pattern += get_stress(part)
                return stress_pattern
            # handle hyphenated words
            elif '-' in word:
                parts = word.split('-')
                stress_pattern = []
                for part in parts:
                    stress_pattern += get_stress(part)
                return stress_pattern
            else:
                print(f'Word not found in dictionary: {word}')
                # Find a similar word in the dictionary and use its stress pattern
                similar_word = find_similar_word(word)
                if similar_word:
                    return get_stress(similar_word)
                else:
                    # Use default pattern if no similar word is found
                    return [[0, 1, 2]]

    # Get the stress pattern of the lyrics
    for word in fixed_tokens:
        # remove punctuation
        word = re.sub(r'[^\w\s]', '', word)
        stress_pattern += get_stress(word)

    # Flatten the stress_pattern list
    stress_pattern = [item for sublist in stress_pattern for item in sublist]

    print(lyrics)
    print(tokens)
    print(["Here are the stress patterns:"] + stress_pattern)

    # Generate a melody based on the stress pattern
    track = Track()
    b = Bar()
    b.set_meter((4, 4))
    beats_in_current_bar = 0
    for stress in stress_pattern:
        if stress == 0:
            note = Note('C', 4)
        elif stress == 1:
            note = Note('E', 4)
        elif stress == 2:
            note = Note('G', 4)
        b + note
        beats_in_current_bar += 1
        if beats_in_current_bar == 4:
            track.add_bar(b)
            b = Bar()
            b.set_meter((4, 4))
            beats_in_current_bar = 0
    track.add_bar(b)


    return track
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we define a function generate_melody that takes a string of lyrics as input and generates a melody based on the phonetic stress pattern of the words. The function uses the nltk library to tokenize the lyrics, handles contractions, and determines the stress pattern of each word using the CMU Pronouncing Dictionary. We account for special cases like contractions, hyphenated words, and words not found in the dictionary, attempting to find similar words or applying a default stress pattern when necessary.&lt;/p&gt;

&lt;p&gt;After extracting and flattening the stress pattern of the entire lyrics, the function uses this pattern to create a melody where different stress levels are mapped to specific musical notes (C, E, G) in a 4/4 time signature, adding these notes to a musical track using the mingus library. Each stress level in the pattern corresponds to a different note, and the function organizes these notes into bars, with each bar containing up to four beats. The track, comprising a series of bars filled with notes based on the lyrical stress pattern, is returned at the end of the function. This allows for the conversion of lyrical content into a basic musical representation, integrating elements of natural language processing and music composition.&lt;/p&gt;

&lt;p&gt;We can then look into creating the MIDI file and sheet music:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;formatted_lyrics = format_lyrics(gpt_lyrics)
formatted_lyrics_with_newlines = add_newlines(formatted_lyrics)
print("Here are the lyrics:" + formatted_lyrics_with_newlines)

melody = generate_melody(formatted_lyrics_with_newlines)

lilypond_string = lilypond.from_Track(melody)
with open('melody.ly', 'w') as f:
    f.write(lilypond_string)

print(melody)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As can be seen, we output the lyrics and MIDI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Outputting Lyrics as PDF&lt;/strong&gt;&lt;br&gt;
Finally, we generate a PDF document containing the formatted lyrics, useful for singers or for archival purposes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from fpdf import FPDF

pdf = FPDF()
pdf.add_page()
pdf.set_font('Arial', 'B', 16)
title = formatted_lyrics_with_newlines.split('\n')[0]
pdf.cell(0, 10, title, 0, 1, 'C')
pdf.set_font('Arial', '', 12)
pdf.multi_cell(0, 10, formatted_lyrics_with_newlines)
pdf.output('lyrics.pdf')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we have our fully functioning Python code for making music! Now, we can decide where to deploy this. Leveraging the capability of AWS, I am going to deploy it on an EC2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Setting Up the EC2 Instance&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to the EC2 dashboard in AWS Management Console.&lt;/li&gt;
&lt;li&gt;Click on "Launch Instance".&lt;/li&gt;
&lt;li&gt;Choose an Amazon Machine Image (AMI), such as Amazon Linux 2 AMI or Ubuntu Server.&lt;/li&gt;
&lt;li&gt;Select an instance type (e.g., t2.micro for testing purposes).&lt;/li&gt;
&lt;li&gt;Configure instance details as required.&lt;/li&gt;
&lt;li&gt;Add storage if the default isn’t enough.&lt;/li&gt;
&lt;li&gt;Configure Security Group to allow SSH (port 22) and any other necessary ports (e.g., port 80 for web server).&lt;/li&gt;
&lt;li&gt;Review and launch the instance by selecting or creating a new key pair.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now that we have the EC2 instance ready, we can access it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accessing the EC2 Instance&lt;/strong&gt;&lt;br&gt;
Connect to your instance using SSH. For Windows, you can use PuTTY or any SSH client:&lt;br&gt;
&lt;code&gt;ssh -i /path/to/your-key.pem ec2-user@your-instance-ip&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Environment Setup&lt;/strong&gt;&lt;br&gt;
Install Python and other necessary tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo yum update -y
sudo yum install python3 git -y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now move the code over to EC2 with SCP or SFTP and install the relevant Python libraries. You can also clone the repo provided at the end of this post.&lt;br&gt;
&lt;code&gt;pip3 install pandas nltk openai pronouncing re fpdf fasttext&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;You also will need to install lilypond for usage with Yum.&lt;br&gt;
&lt;code&gt;sudo yum install lilypond -y&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Set environment variables (for example, for the OpenAI API key):&lt;br&gt;
&lt;code&gt;export openai_api_key='your-api-key'&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;With the environment now set up, we can run and automate the script.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running and Automating the Script&lt;/strong&gt;&lt;br&gt;
Run the script with the command below.&lt;br&gt;
&lt;code&gt;python3 main.py&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;To ensure the script runs automatically at system boot or keeps running after disconnections, we will use nohup to run the script in the background.&lt;br&gt;
&lt;code&gt;nohup python3 your_script.py &amp;amp;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Set up a systemd service if you want it to start at boot and restart on failures.&lt;/p&gt;

&lt;p&gt;We can then monitor the script execution through checking the logs.&lt;br&gt;
&lt;code&gt;tail -f nohup.out&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Optionally, configure CloudWatch to monitor the EC2 performance or to set up more advanced logging and alerting.&lt;/p&gt;

&lt;p&gt;And with that, we've set up our deployed solution! It takes a bit of learning to wrap your head around the NLP and music theory bits, but it really is quite a straightforward approach to getting started on making music with AI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;br&gt;
This project showcases the power of AI in creative processes like songwriting. By integrating various technologies and libraries, we've created a system that not only generates lyrics but also composes a melody, presenting it in both audio and visual forms. This illustrates the potential for AI to assist in artistic expressions, providing tools that can inspire and enhance the creative capabilities of its users. Whether you're a developer, a musician, or an enthusiast in the realms of AI or music, this project offers fascinating insights and possibilities.&lt;/p&gt;

&lt;p&gt;The repo to the project can be accessed at: &lt;a href="https://github.com/renaldig/melodistiq-music-generator" rel="noopener noreferrer"&gt;https://github.com/renaldig/melodistiq-music-generator&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>python</category>
      <category>music</category>
    </item>
  </channel>
</rss>
