<?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: Rohit Tirmanwar</title>
    <description>The latest articles on DEV Community by Rohit Tirmanwar (@rohit_tirmanwar_dce05d8ee).</description>
    <link>https://dev.to/rohit_tirmanwar_dce05d8ee</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4195074%2F96f0ea9a-b04f-4f24-8fba-1f14d2cd3602.png</url>
      <title>DEV Community: Rohit Tirmanwar</title>
      <link>https://dev.to/rohit_tirmanwar_dce05d8ee</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rohit_tirmanwar_dce05d8ee"/>
    <language>en</language>
    <item>
      <title>Designing an OCR document pipeline on serverless AWS</title>
      <dc:creator>Rohit Tirmanwar</dc:creator>
      <pubDate>Tue, 20 Oct 2026 00:16:42 +0000</pubDate>
      <link>https://dev.to/rohit_tirmanwar_dce05d8ee/designing-an-ocr-document-pipeline-on-serverless-aws-4c5i</link>
      <guid>https://dev.to/rohit_tirmanwar_dce05d8ee/designing-an-ocr-document-pipeline-on-serverless-aws-4c5i</guid>
      <description>&lt;p&gt;Document OCR is easy to demo and hard to run. The demo takes a PDF, calls an OCR service, and prints the fields. The production version has to handle a ten-page scan uploaded from a phone, a form filled in by hand, a field read with 61 percent confidence, and a user who needs to know whether their document was accepted. I have built this on serverless AWS for a platform used by a large number of students, and this is the shape that held up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline, end to end
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Upload straight to S3.&lt;/strong&gt; The browser gets a pre-signed URL from an API and uploads the file directly. Nothing passes through a Lambda, so a 25 MB scan does not hit a payload limit, and the API only records that an upload was requested.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;S3 event to EventBridge to Step Functions.&lt;/strong&gt; The object-created event starts a Step Functions execution per document. The state machine is the only place that knows the order of steps, which is what makes the pipeline readable six months later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate before OCR.&lt;/strong&gt; A short Lambda checks the file type, size and page count, and rejects what the OCR service would reject anyway. This step saves money and gives the user a fast, specific error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asynchronous OCR.&lt;/strong&gt; Amazon Textract's synchronous calls have page and size limits, so anything that can be multi-page goes through the asynchronous API: &lt;code&gt;StartDocumentAnalysis&lt;/code&gt; with an SNS notification channel, and the state machine waits on a task token until the completion message arrives. &lt;code&gt;GetDocumentAnalysis&lt;/code&gt; is paginated, so the collector loops on &lt;code&gt;NextToken&lt;/code&gt; until it has the whole result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extract and normalise.&lt;/strong&gt; Raw OCR output is blocks and geometry. A Lambda maps it to the fields the product cares about, normalises dates and amounts, and records a confidence score per field, not per document.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decide: accept, review, or reject.&lt;/strong&gt; Per-field thresholds, set from measured data, route each document. Everything above the threshold is accepted automatically. Anything below goes to a human review queue with the page image and the proposed value side by side. Anything unreadable goes back to the user with a reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store and notify.&lt;/strong&gt; Results land in DynamoDB keyed by document ID, the state machine emits a completion event, and the application notifies the user.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Confidence thresholds are measured, not guessed
&lt;/h2&gt;

&lt;p&gt;The single most important number in the system is the per-field threshold, and the mistake is to pick one number for every field. A date of birth read wrong is a different kind of error from a middle initial read wrong.&lt;/p&gt;

&lt;p&gt;We built a golden set: a few hundred documents labelled by hand, run through the pipeline, with the OCR value and confidence recorded for each field. From that you can plot, for each field, accuracy against confidence, and pick the threshold where accuracy reaches the level the business accepts. Some fields needed; some could never be trusted automatically and always went to review.&lt;/p&gt;

&lt;p&gt;The golden set then becomes a regression test. Every change to the extractor, every change to a prompt or a model version, runs against it before deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The human review loop is part of the architecture
&lt;/h2&gt;

&lt;p&gt;A pipeline that cannot route to a person is a pipeline that will silently accept wrong data. The review queue is not an exception path; it is a first-class step with its own metrics: queue depth, time to review, and the share of reviewed values that the reviewer changed. That last number is the feedback that tunes the thresholds. If reviewers accept 99 percent of what they see for a field, the threshold for that field is too conservative.&lt;/p&gt;

&lt;p&gt;Reviewer corrections are written back to the same DynamoDB record with who changed what and when. Nothing is overwritten.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sensitive data decides half the design
&lt;/h2&gt;

&lt;p&gt;Documents of this kind carry personal data, so the pipeline is built around not leaking it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The upload bucket and every downstream bucket are encrypted with KMS and block public access.&lt;/li&gt;
&lt;li&gt;S3 lifecycle rules delete the raw uploads after a fixed window. Keep the extracted fields, not the scans, unless there is a legal reason to keep the scans.&lt;/li&gt;
&lt;li&gt;Lambdas log document IDs and confidence scores, never field values. A single log line with a value in it is a data leak that lives for the retention period.&lt;/li&gt;
&lt;li&gt;Reviewers see pages through short-lived pre-signed URLs, and the review application never stores the image.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Failure modes that were not in the diagram
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate uploads.&lt;/strong&gt; Users re-upload when the first upload is slow. The document ID is derived from the user and a content hash, so the second upload short-circuits instead of running OCR twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OCR service throttling.&lt;/strong&gt; Textract has account-level concurrency limits. The state machine uses a retry with backoff on the start call, and a concurrency limit on the step keeps a bulk upload from starving everyone else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stuck executions.&lt;/strong&gt; A completion notification that never arrives leaves the execution waiting on its task token. Every wait state has a heartbeat timeout, and a timed-out execution goes to the review queue with a "needs re-run" flag instead of hanging forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial results.&lt;/strong&gt; A twelve-page document where page nine failed is still useful. Page-level status is stored, and the decision step treats a missing required page as "review," not "reject."&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would keep and what I would change
&lt;/h2&gt;

&lt;p&gt;I would keep Step Functions as the backbone. The visual execution history alone has saved hours of debugging, and the task-token pattern makes the asynchronous OCR call look synchronous in the state machine. I would keep the golden set; it is the only reason we could change the extractor with confidence.&lt;/p&gt;

&lt;p&gt;I would move the confidence thresholds into configuration from the start rather than code, because they change more often than anything else. And I would build the review metrics on day one, because the pipeline is only as good as the feedback loop that tunes it.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>ocr</category>
      <category>architecture</category>
    </item>
    <item>
      <title>AWS CDK patterns that survive a team</title>
      <dc:creator>Rohit Tirmanwar</dc:creator>
      <pubDate>Tue, 20 Oct 2026 00:12:11 +0000</pubDate>
      <link>https://dev.to/rohit_tirmanwar_dce05d8ee/aws-cdk-patterns-that-survive-a-team-3n1a</link>
      <guid>https://dev.to/rohit_tirmanwar_dce05d8ee/aws-cdk-patterns-that-survive-a-team-3n1a</guid>
      <description>&lt;p&gt;AWS CDK in TypeScript is the best way I have found to define serverless infrastructure, and the easiest way I have found for eight engineers to create eight different conventions. The CDK gives you a real programming language, so every engineer brings their own idea of how to structure it. After a few years of leading teams that own CDK codebases, these are the patterns I now set on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  One construct per repeated shape
&lt;/h2&gt;

&lt;p&gt;The first thing to notice in any CDK codebase is repetition: a queue, its dead-letter queue, the Lambda that consumes it, the alarm on the DLQ, the log retention, the IAM grants. Written inline, that is forty lines, and the fifth copy will be missing the alarm.&lt;/p&gt;

&lt;p&gt;Turn each repeated shape into a construct with a small interface:&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;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;QueueWorkerProps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;handler&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;entry&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;visibilityTimeout&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="nl"&gt;alarmTopic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ITopic&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;class&lt;/span&gt; &lt;span class="nc"&gt;QueueWorker&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Construct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NodejsFunction&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;QueueWorkerProps&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dlq&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;Queue&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;Dlq&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;retentionPeriod&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;days&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;queue&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;Queue&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;Queue&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;visibilityTimeout&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;visibilityTimeout&lt;/span&gt; &lt;span class="o"&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;seconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;deadLetterQueue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dlq&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;maxReceiveCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;},&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="nx"&gt;fn&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;NodejsFunction&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;Fn&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;entry&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;entry&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;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;logRetention&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RetentionDays&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ONE_MONTH&lt;/span&gt;&lt;span class="p"&gt;,&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="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventSource&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;SqsEventSource&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="nx"&gt;queue&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;Alarm&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;DlqAlarm&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;metric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dlq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;metricApproximateNumberOfMessagesVisible&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;threshold&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="na"&gt;evaluationPeriods&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="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;addAlarmAction&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;SnsAction&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;alarmTopic&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;Now the fifth copy cannot miss the alarm. The construct is where the team's operational standards live, and a code review of a new worker is a review of ten lines instead of forty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let the CDK name things
&lt;/h2&gt;

&lt;p&gt;Hard-coded resource names feel tidy and cause two real problems. They collide when you deploy a second copy of the stack, and they block CloudFormation from replacing a resource when a change requires it, which turns a small change into a failed deploy.&lt;/p&gt;

&lt;p&gt;Omit &lt;code&gt;functionName&lt;/code&gt;, &lt;code&gt;tableName&lt;/code&gt;, &lt;code&gt;queueName&lt;/code&gt; unless something outside the stack needs the name. When something does, export it through an SSM parameter or a stack output rather than by agreeing on a string in two places.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stages, not copied stacks
&lt;/h2&gt;

&lt;p&gt;Development, staging and production are the same stacks with different parameters, not three directories. A CDK &lt;code&gt;Stage&lt;/code&gt; holds the set of stacks, and the app instantiates it once per environment:&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;const&lt;/span&gt; &lt;span class="nx"&gt;app&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;App&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;PlatformStage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Dev&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;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;configs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dev&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;PlatformStage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;configs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prod&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything that varies by environment lives in one typed config object. If a value is not in that object, it is the same everywhere, and that is a feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;cdk diff&lt;/code&gt; in every pull request
&lt;/h2&gt;

&lt;p&gt;The most useful line in our pipeline is the one that runs &lt;code&gt;cdk diff&lt;/code&gt; against the target environment and posts the output on the pull request. Reviewers see the CloudFormation changes, not just the TypeScript. A one-character change that replaces a DynamoDB table shows up as a replacement before it is merged, not after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assertions tests for the things that must never regress
&lt;/h2&gt;

&lt;p&gt;Unit tests on infrastructure sound excessive until the first time someone removes a removal policy from a production table. The CDK assertions module makes those tests cheap:&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;const&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromStack&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="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AWS::DynamoDB::Table&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;DeletionPolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Retain&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;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resourceCountIs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AWS::SQS::Queue&lt;/span&gt;&lt;span class="dl"&gt;"&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="c1"&gt;// worker queue plus its DLQ&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We do not test everything. We test retention policies, encryption settings, DLQ presence, and that no Lambda has an unlimited log retention. Those tests take seconds and have caught real mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep cross-stack references few and deliberate
&lt;/h2&gt;

&lt;p&gt;CDK makes it one line to reference a resource from another stack, and CloudFormation turns that into an export that locks the two stacks together. Deleting or changing the exporting side then fails until the importing side is updated, and in the wrong order.&lt;/p&gt;

&lt;p&gt;The rule that works: resources that change together live in the same stack. Cross-stack references are for a small number of stable, shared things, such as a VPC or an alarm topic. For everything else, pass values through SSM parameters, which decouple the deployment order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pipelines deploy, people do not
&lt;/h2&gt;

&lt;p&gt;The last pattern is organisational. Nobody runs &lt;code&gt;cdk deploy&lt;/code&gt; to production from a laptop. A pipeline does, from a merged commit, with an OIDC role rather than long-lived keys. Development environments can be deployed from laptops, with per-engineer stage names so two people do not fight over one stack.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters for a team
&lt;/h2&gt;

&lt;p&gt;Each pattern on its own is small. Together they are what lets a team of six to eight engineers, some of them new to AWS, change production infrastructure every week without a dedicated platform team. The constructs encode the standards, the diff shows the blast radius, the tests hold the floor, and the pipeline removes the human from the deploy. That is the whole point of putting infrastructure in a real language: you get to treat it like the rest of the code.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>typescript</category>
      <category>cdk</category>
      <category>devops</category>
    </item>
    <item>
      <title>What keeps a serverless system healthy after launch</title>
      <dc:creator>Rohit Tirmanwar</dc:creator>
      <pubDate>Tue, 20 Oct 2026 00:11:06 +0000</pubDate>
      <link>https://dev.to/rohit_tirmanwar_dce05d8ee/what-keeps-a-serverless-system-healthy-after-launch-4nb6</link>
      <guid>https://dev.to/rohit_tirmanwar_dce05d8ee/what-keeps-a-serverless-system-healthy-after-launch-4nb6</guid>
      <description>&lt;p&gt;Most serverless articles end at the architecture diagram. API Gateway in front, Lambda in the middle, DynamoDB at the back, a queue or two, and the system is "done." I have spent the last five years running systems like that for products used by millions of students, and the diagram was never the hard part. The hard part is everything that happens after launch, when retries, cold starts, throttles and bills show up at the same time.&lt;/p&gt;

&lt;p&gt;These are the six decisions I now make before a serverless system ships, because each one was painful to make afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Every asynchronous handler is idempotent
&lt;/h2&gt;

&lt;p&gt;Lambda invoked from SQS, EventBridge, SNS or a DynamoDB stream is at-least-once. The same event will arrive twice. Not often, but at the worst time, and usually when a downstream service is slow and your function timed out after doing half its work.&lt;/p&gt;

&lt;p&gt;The fix is boring and it has to be everywhere: an idempotency key per event, stored before the side effect happens. For DynamoDB that is a conditional write on the key. For anything else it is a small table of processed keys with a TTL. AWS Lambda Powertools has an idempotency utility that does exactly this, and it is worth adopting on day one rather than retrofitting it into forty functions later.&lt;/p&gt;

&lt;p&gt;If a handler cannot be made idempotent, it needs to be the only consumer of a FIFO queue with a deduplication ID. That is a design constraint, so decide it early.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A dead-letter queue on every asynchronous path, with an alarm
&lt;/h2&gt;

&lt;p&gt;An asynchronous Lambda without a dead-letter queue drops failed events on the floor after its retries are exhausted. You will not know. The event is simply gone.&lt;/p&gt;

&lt;p&gt;Every SQS queue gets a redrive policy to a DLQ. Every asynchronous Lambda invocation gets an on-failure destination. Every DLQ gets a CloudWatch alarm on &lt;code&gt;ApproximateNumberOfMessagesVisible&lt;/code&gt; greater than zero, routed to the on-call channel. The alarm matters as much as the queue. A DLQ nobody watches is a slower way of losing data.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Concurrency limits protect the things that cannot scale
&lt;/h2&gt;

&lt;p&gt;Lambda scales faster than almost everything it talks to. A traffic spike that Lambda absorbs happily becomes a connection storm against a relational database, a throttled third-party API, or a downstream service owned by another team.&lt;/p&gt;

&lt;p&gt;Reserved concurrency on the function that talks to the fragile thing is the simplest brake. It turns a downstream outage into a queue backlog, which is recoverable, instead of a cascade, which is not. For the small number of functions on the synchronous request path where cold starts are visible to users, provisioned concurrency is worth its cost. For everything else, it is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Observability is a correlation ID and four alarms
&lt;/h2&gt;

&lt;p&gt;Logs from a hundred functions are useless without a way to follow one request through them. Every function logs structured JSON with a correlation ID that comes in on the request or event and goes out on everything it emits. Powertools does this too. Without it, debugging a production issue is grep across a hundred log groups.&lt;/p&gt;

&lt;p&gt;Then four alarms per function, no more to start with: error rate, p95 duration against the timeout, throttles, and for stream or queue consumers, iterator age or oldest message age. Those four catch almost everything. Dashboards are for understanding; alarms are for waking up, and they should be rare enough that every one is acted on.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The bill is decided by three settings
&lt;/h2&gt;

&lt;p&gt;Serverless is cheap by default and expensive by accident. The three settings that decide most of the bill:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Log retention.&lt;/strong&gt; CloudWatch log groups default to never expire. Set a retention period on every group, in infrastructure code, from the start. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory size.&lt;/strong&gt; Lambda bills memory multiplied by duration, and more memory often means a shorter duration and a lower cost. Run AWS Lambda Power Tuning once per function and pick the cheapest setting that meets the latency target, instead of guessing 1024 MB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB capacity mode.&lt;/strong&gt; On-demand for spiky or unknown traffic, provisioned with auto scaling for steady traffic. Switching later is allowed, so start with on-demand and look at the numbers after a month.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  6. A runbook exists before the first alarm fires
&lt;/h2&gt;

&lt;p&gt;Every alarm links to a runbook that says what the alarm means, what to check first, and what the safe actions are. It does not need to be long. "DLQ depth above zero: open the queue, read one message, check the function's error logs by correlation ID, fix or redrive" is enough.&lt;/p&gt;

&lt;p&gt;The runbook is also where the team's knowledge stops living in one person's head. On a team of six to eight engineers with a mix of junior and senior people, the runbook is what lets a junior engineer handle a Tuesday night alarm without paging me.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell my past self
&lt;/h2&gt;

&lt;p&gt;None of this is clever. All of it is the difference between a serverless system that is quiet in production and one that generates a steady stream of mysteries. If you are about to ship one, do these six things in the infrastructure code before the first deploy, because adding them later means touching every function and relearning every lesson at 2 a.m.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>architecture</category>
      <category>lambda</category>
    </item>
  </channel>
</rss>
