<?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: AWS Heroes</title>
    <description>The latest articles on DEV Community by AWS Heroes (aws-heroes).</description>
    <link>https://dev.to/aws-heroes</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%2Forganization%2Fprofile_image%2F2491%2Ff0c1a659-c959-42cd-bb12-cd25909dd9db.png</url>
      <title>DEV Community: AWS Heroes</title>
      <link>https://dev.to/aws-heroes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aws-heroes"/>
    <language>en</language>
    <item>
      <title>Orchestrating workflows with Amazon S3 Files and AWS Lambda durable functions using Java SDK - Part 4 Wait for condition</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 31 Aug 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1f1n</link>
      <guid>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1f1n</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1k6e"&gt;part 2&lt;/a&gt; of the series, we explored how to use the AWS Lambda Durable Execution SDK for Java to create and execute durable steps synchronously and asynchronously. &lt;br&gt;
Later, in &lt;a href="https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-oo4"&gt;part 3&lt;/a&gt; of the series, we explored how to use the AWS Lambda Durable Execution SDK for Java to implement wait and callback. &lt;/p&gt;

&lt;p&gt;In this part, we'll explore the &lt;em&gt;wait for condition&lt;/em&gt; operation.&lt;/p&gt;
&lt;h2&gt;
  
  
  Sample application with wait for condition
&lt;/h2&gt;

&lt;p&gt;Let's explore the "wait for condition" operation. We get access to it through &lt;em&gt;DurableContext&lt;/em&gt;.  It repeatedly calls a check function until it signals done. Between polls, the Lambda suspends without consuming compute. State is checkpointed after each check, so progress survives interruptions.&lt;/p&gt;

&lt;p&gt;I first copied the application that we created in &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/tree/main/aws-s3-files-lambda-durable-functions-with-wait-for-callback-java-25" rel="noopener noreferrer"&gt;part 3&lt;/a&gt; into the &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/tree/main/aws-s3-files-lambda-durable-functions-with-wait-for-condition-java-25" rel="noopener noreferrer"&gt;aws-s3-files-lambda-durable-functions-with-wait-for-condition-java-25&lt;/a&gt; repository. The only change we had to make was to reimplement the &lt;em&gt;waitForUpcomingTalksApproval&lt;/em&gt; method of the &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-with-wait-for-condition-java-25/src/main/java/dev/vkazulkin/handler/AbstractAuthorContentExtractor.java" rel="noopener noreferrer"&gt;AbstractAuthorContentExtractor&lt;/a&gt; class.  Here we'll now use the "wait for condition" instead of the "wait for callback" operation.&lt;/p&gt;

&lt;p&gt;This is how the code now looks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nc"&gt;UpcomingTalkApprovalStatus&lt;/span&gt; &lt;span class="nf"&gt;waitForUpcomingTalksApproval&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DurableContext&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Author&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UpcomingTalks&lt;/span&gt; &lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

  &lt;span class="kt"&gt;var&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;WaitForConditionConfig&lt;/span&gt;&lt;span class="o"&gt;.&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;UpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;                
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitStrategy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;WaitStrategies&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;defaultStrategy&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialState&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;UpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
         &lt;span class="nc"&gt;UpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;PENDING&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitForCondition&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
     &lt;span class="s"&gt;"wait-for-approval"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
     &lt;span class="nc"&gt;UpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
         &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentStatus&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stepCtx&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
         &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getUpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
         &lt;span class="k"&gt;return&lt;/span&gt;  
         &lt;span class="nc"&gt;UpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;APPROVED&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;                     
         &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nc"&gt;WaitForConditionResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stopPolling&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
         &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;WaitForConditionResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;continuePolling&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;UpcomingTalkApprovalStatus&lt;/span&gt; &lt;span class="nf"&gt;getUpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Author&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UpcomingTalks&lt;/span&gt; &lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
     &lt;span class="c1"&gt;// Generate random integers in range 0 to 3&lt;/span&gt;
     &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;RANDOM&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;nextInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; 
       &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
          &lt;span class="nc"&gt;UpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;NOT_APPROVED&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt;
       &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
          &lt;span class="nc"&gt;UpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;APPROVED&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's go step by step through this code. The &lt;em&gt;ctx.waitForCondition&lt;/em&gt; invocation receives the current state and a StepContext, and returns a &lt;em&gt;WaitForConditionResult&lt;/em&gt;. WaitForConditionResult contains the result type, in our case &lt;em&gt;UpcomingTalkApprovalStatus&lt;/em&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;WaitForConditionResult.stopPolling(value)&lt;/em&gt;  - condition met (in our case, status "APPROVED"), return value as the final result.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;WaitForConditionResult.continuePolling(value)&lt;/em&gt; - keep polling (bacause the status is "NOT_APPROVED"), pass value to the next check.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the sake of simplicity, we haven't implemented the proper endpoint to poll the status. We provided a simple implementation in the &lt;em&gt;getUpcomingTalkApprovalStatus&lt;/em&gt; method, which generates a random integer between 0 and 3. If 3 is generated, the "APPROVED" status will be returned, indicating that the condition is met and the polling should stop. If 0, 1, or 2 are generated, the operation will keep polling.&lt;/p&gt;

&lt;p&gt;Next, let's look at how to configure &lt;em&gt;WaitForConditionConfig&lt;/em&gt;. First, we pass the &lt;em&gt;initialState&lt;/em&gt; with the status "PENDING," and this status is passed to the first check invocation. After that, we also configure the &lt;em&gt;waitStrategy&lt;/em&gt;. The wait strategy controls the delay between polls. By default, &lt;em&gt;waitForCondition&lt;/em&gt; uses an exponential backoff wait strategy (60 max attempts, 5s initial delay, 300s max delay, 1.5x backoff rate, FULL jitter). We can invoke the &lt;em&gt;WaitStrategies.exponentialBackoff&lt;/em&gt; method to configure exponential backoff with different settings than the default ones. Alternatively, we can use the &lt;em&gt;WaitStrategies.fixedDelay&lt;/em&gt; method. With this wait strategy, we need to configure: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;fixedDelay&lt;/em&gt; - a constant delay between polls and &lt;/li&gt;
&lt;li&gt;
&lt;em&gt;maxAttempts&lt;/em&gt; - a maximum number of attempts before throwing &lt;em&gt;WaitForConditionFailedException&lt;/em&gt;. This exception is thrown when the maximum attempts are exceeded (thrown by the wait strategy).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We can also implement &lt;a href="https://github.com/aws/aws-durable-execution-sdk-java/blob/main/docs/core/wait-for-condition.md#custom-wait-strategies" rel="noopener noreferrer"&gt;Custom Wait Strategies&lt;/a&gt;, which we won't cover in this article.&lt;/p&gt;

&lt;p&gt;That's it. There are no changes in the Infrastructure as Code part that we need to make. Now we can build and package our application with &lt;em&gt;mvn clean package&lt;/em&gt; and deploy it with &lt;em&gt;sam deploy&lt;/em&gt;.  The deployment process can take up to 10 minutes because of the creation and mounting of S3 Files.&lt;/p&gt;

&lt;p&gt;To test our Lambda durable function, we can navigate to the Lambda service, search for the &lt;em&gt;AuthorContentExtractor&lt;/em&gt; or &lt;em&gt;AsyncAuthorContentExtractor&lt;/em&gt; function, and go to the "Test" tab.&lt;/p&gt;

&lt;p&gt;We need to pass the following sample JSON Event to it, which represents the author:&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;"firstName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Vadym"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lastName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Kazulkin"&lt;/span&gt;&lt;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;Then we can test it. After that, we go to the "Durable execution" tab and can see all the execution details:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuiupzpbq96rme4wd1bo8.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuiupzpbq96rme4wd1bo8.png" alt=" " width="799" height="217"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here we see that the operation with the name &lt;em&gt;wait-for-approval&lt;/em&gt; and the subtype &lt;em&gt;WaitForCondition&lt;/em&gt; succeeded after 3 attempts. The number of retries is 2. This means that it took 3 attempts to generate the integer 3. We defined the generation of this value as the condition to stop polling and return the result.&lt;/p&gt;

&lt;p&gt;Let's also look at event history:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8agkrjthysnm8iwskq3h.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8agkrjthysnm8iwskq3h.png" alt=" " width="800" height="571"&gt;&lt;/a&gt;&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl5wlzye6qcaximoo9e6k.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl5wlzye6qcaximoo9e6k.png" alt=" " width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here we see all events (durable operations) in the right order required for the execution of our Lambda durable function.&lt;/p&gt;

&lt;p&gt;There is also the &lt;em&gt;waitForConditionAsync&lt;/em&gt;  operation for non-blocking polling that you can explore on your own. It starts polling but returns a &lt;em&gt;DurableFuture&lt;/em&gt; immediately, allowing other operations to run concurrently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this part of the series, we explored how to use the AWS Lambda Durable Execution SDK for Java to implement the &lt;em&gt;wait for condition&lt;/em&gt; operation.  &lt;/p&gt;

&lt;p&gt;In the next part, we'll explore map and child context operations. Still, our Lambda durable function itself contains too much business logic. Ideally, it should simply be the orchestrator and contain as little business logic as possible. That's why, in the subsequent parts, we'll move the logic for each step into a separate Lambda function. With that, we'll explore how to invoke another Lambda function within the durable step. We'll also show how to invoke multiple Lambda functions in parallel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you like my content, please follow me on &lt;a href="https://github.com/Vadym79" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and give my repositories a star!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also check out my &lt;a href="https://vkazulkin.com" rel="noopener noreferrer"&gt;website&lt;/a&gt; for more technical content and upcoming public speaking activities.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>lambdadurablefunctions</category>
      <category>s3files</category>
    </item>
    <item>
      <title>DynamoDB Streams is not an outbox</title>
      <dc:creator>Allen Helton</dc:creator>
      <pubDate>Tue, 25 Aug 2026 16:39:57 +0000</pubDate>
      <link>https://dev.to/aws-heroes/dynamodb-streams-is-not-an-outbox-256d</link>
      <guid>https://dev.to/aws-heroes/dynamodb-streams-is-not-an-outbox-256d</guid>
      <description>&lt;p&gt;Hopefully you didn't notice, but a couple of weeks ago I sent &lt;a href="https://readysetcloud.io/newsletter/sign-up" rel="noopener noreferrer"&gt;an issue of my newsletter&lt;/a&gt; that never actually got emailed. My dashboard looked fine and it was marked as published, but the email never hit my inbox (or any of yours 😬).&lt;/p&gt;

&lt;p&gt;I didn't notice until two days later when I went to look at the weekly statistics and found nothing. Just a &lt;code&gt;Sent&lt;/code&gt; status and a bunch of 0's on the screen.&lt;/p&gt;

&lt;p&gt;A quick browse through the code didn't turn up anything obvious. Emails are sent via an async handler triggered by an EventBridge event while the status of the newsletter is marked separately in DynamoDB, a classic "&lt;em&gt;db save and domain event publish&lt;/em&gt;" pattern. We've all done it, it looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&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;UpdateItemCommand&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;eventBridge&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="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;Despite its simple appearance, this is a nasty anti-pattern. Formally known as the &lt;em&gt;dual write problem&lt;/em&gt;, if those two actions aren't contained in a single transaction, you're asking for trouble. Why? I'm glad you asked.&lt;/p&gt;

&lt;p&gt;Imagine the write succeeds, but the publish doesn't. Data is updated but the follow-on actions that react to the domain event never run (like your email never being sent 🙃). Things fall into a bad state and you (maybe) end up stumbling upon a problem days later.&lt;/p&gt;

&lt;p&gt;Ok, so flip them. Put the event first and the data save second. But if the publish succeeds and the db save fails, then you risk acting on stale data, which depending on your domain, is much worse.&lt;/p&gt;

&lt;p&gt;Before you ask, no, serverless doesn't make it better. In many cases, serverless makes us too laissez-faire to solve it the "write" way (get it?).&lt;/p&gt;

&lt;h2&gt;
  
  
  What you're probably doing
&lt;/h2&gt;

&lt;p&gt;Let's say you already knew not to do the data save and the event publish at the same time. I bet I know what you're doing instead. It's a pattern AWS has recommended for years and is &lt;a href="https://serverlessland.com/patterns?services=dynamodb-stream" rel="noopener noreferrer"&gt;all over Serverless Land&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You do your save to DynamoDB, let the change data flow into DynamoDB Streams, process it in Lambda, then fire your domain events from the stream processor.&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4cw1nqzhdxgf27t7eosx.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4cw1nqzhdxgf27t7eosx.png" alt="Flowchart going from Lambda to DDB to stream handler" width="800" height="204"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When it comes down to it, this is legitimately better, and it's the same thought process behind &lt;a href="https://readysetcloud.io/blog/allen.helton/built-better-serverless-apis-by-going-storage-first" rel="noopener noreferrer"&gt;going storage-first&lt;/a&gt;. You persist it first, then process it asynchronously. That makes the database write the event trigger, so now your domain events are tightly coupled to the corresponding data change. Plus, DynamoDB Streams retries automatically (and &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/services-ddb-batchfailurereporting.html#streams-batchfailurereporting-bisect" rel="noopener noreferrer"&gt;even bisects batches&lt;/a&gt; when configured) if there's a processing failure.&lt;/p&gt;

&lt;p&gt;Sounds like our dual write problem is solved, right? Wrong. Kind of. It's complicated. 😅&lt;/p&gt;

&lt;p&gt;DynamoDB Streams records are change data capture, not domain events. The Lambda function you write to handle them receives a &lt;code&gt;NewImage&lt;/code&gt; and &lt;code&gt;OldImage&lt;/code&gt; record, and it's up to you to infer what happened upstream. Smells like leaky business logic to me. Before you know it, you have this thousand-line file that looks at 40 different data shapes to determine which event to publish and which transformation to perform. As someone who's found themselves in that hole before, believe me when I say that is a nightmare situation.&lt;/p&gt;

&lt;p&gt;Let's take it a step further. A business operation could (and probably does) affect multiple DynamoDB records. When I publish my newsletter, I create a DynamoDB record with a short TTL for each email address that was successfully sent to. If I am processing entity changes in a stream handler, I'd be up to my ears in duplicate &lt;code&gt;newsletter.sent&lt;/code&gt; events because I'm working on individual records and not an aggregate operation. That ultimately leads to workarounds with atomic counters and &lt;code&gt;lastAction&lt;/code&gt; flags that make it overly complex with little value add.&lt;/p&gt;

&lt;p&gt;And most importantly, you don't know what did or didn't publish. Streams &lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.html" rel="noopener noreferrer"&gt;retain data for 24 hours&lt;/a&gt;, and if your publisher was broken for a day because of an outage or a rogue IAM change, you're out of luck. Even if you opted for something like Kinesis, which has 365 days of data retention, that's not the point. These mechanisms are change data, not the intended effect on the system.&lt;/p&gt;

&lt;p&gt;DynamoDB Streams are a wonderful piece of engineering that gives you retries, but not the durability of intent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you should do instead
&lt;/h2&gt;

&lt;p&gt;Time for a bit of a mental shift. We need to prioritize domain events. If an event is important enough to publish, it's important enough to persist in a database.&lt;/p&gt;

&lt;p&gt;This is where the &lt;em&gt;outbox pattern&lt;/em&gt; comes into play. Instead of deriving your domain events from the data change in a downstream handler, you save the event in an outbox table in a transaction along with the data mutation. The transaction guarantees both the domain event and the data change will save together (or neither).&lt;/p&gt;

&lt;p&gt;With DynamoDB, that could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eventId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ulid&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;TransactWriteItemsCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;TransactItems&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;Update&lt;/span&gt;&lt;span class="p"&gt;:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;newsletter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;marshall&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;issueId&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;issue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="na"&gt;UpdateExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SET #status = :sent&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="na"&gt;Put&lt;/span&gt;&lt;span class="p"&gt;:&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;outbox&lt;/span&gt;&lt;span class="dl"&gt;'&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="nf"&gt;marshall&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;issueId&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;`EVENT#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;eventId&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;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PENDING&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;pendingBucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PENDING&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="c1"&gt;// GSI1 PK, removed on publish&lt;/span&gt;
          &lt;span class="na"&gt;pendingAt&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="c1"&gt;// GSI1 SK&lt;/span&gt;
          &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;newsletter.service&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;newsletter.sent&lt;/span&gt;&lt;span class="dl"&gt;'&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;issueId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;eventId&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Funnily enough, the architecture doesn't change with this pattern. You're still doing Lambda (or whatever compute) to DynamoDB to a stream to another Lambda function. But this time the downstream Lambda function is processing the event you saved, not the mutated data. It doesn't have to infer anything.&lt;/p&gt;

&lt;p&gt;AWS does a fantastic job in their &lt;a href="https://docs.aws.amazon.com/prescriptive-guidance/latest/cloud-design-patterns/transactional-outbox.html" rel="noopener noreferrer"&gt;transactional outbox design pattern docs&lt;/a&gt; showing you how to setup and implement this pattern yourself. So I won't go into more of the code here. But I will address some questions I had the first time I built this.&lt;/p&gt;

&lt;h3&gt;
  
  
  What do I do with an event after it's published?
&lt;/h3&gt;

&lt;p&gt;After you publish an event in your stream handler, it's up to you to either delete it from the outbox or mark it as published. Personally, I prefer to mark it as published and set a 30 day TTL. This means in the stream handler, we're hitting the dual write problem again because we are both publishing an event and updating the outbox with a published status.&lt;/p&gt;

&lt;p&gt;This one is ok though 😉 and it brings up an important point we haven't covered yet. &lt;a href="https://readysetcloud.io/blog/allen.helton/api-essentials-idempotency/" rel="noopener noreferrer"&gt;Idempotency is crucial&lt;/a&gt; for success with this pattern. The outbox pattern trades duplicate delivery in exchange for never losing durable content. If your publisher fails to send the event or update the database with a published status, that's fine! Do it again. The important part is that the intent wasn't lost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I replace the stream handler function with EventBridge Pipes?
&lt;/h3&gt;

&lt;p&gt;Fewer pieces, easier system to maintain, right? In this instance, the Lambda function is the better solution over Pipes. If you were only publishing an event to EventBridge, you could easily filter the stream to the relevant outbox records and forward it straight to a bus. But because you also need to write back to DynamoDB to update the status of the event, the function wins. A pipe can only perform a single operation (outside of enrichments and transformations), but a function can perform many.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does this guarantee ordering?
&lt;/h3&gt;

&lt;p&gt;Nope. The outbox pattern is designed for correctness only. Eventually consistent state across a distributed system. This does not tackle other hard distributed system problems like ordering or exactly-once delivery. Quite the opposite in fact. While Streams do process change data in the order they happen, it's per shard (not globally), and you can't guarantee ordering when it comes to retries. And as soon as you drop an event on EventBridge, you lose any sort of ordering.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does this guarantee events aren't skipped?
&lt;/h3&gt;

&lt;p&gt;If you implemented this exactly as described, it wouldn't. It gives you the normal retry path you're already used to, but still has that 24-hour clock because of the stream data retention. The important piece that's missing is a scheduled reconciler. It's usually another Lambda function that queries a sparse GSI for anything still pending from more than five minutes ago, which is long enough that the stream handler has had its shot, and republishes it. So your full flow would look something like this:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdrgbjmc04oh3ok4k2v0t.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdrgbjmc04oh3ok4k2v0t.png" alt="Chart showing the flow of the transactional outbox pattern" width="800" height="802"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Remember this
&lt;/h2&gt;

&lt;p&gt;There is no clever workaround to the dual write problem. You can't order the calls in a way that mitigates the risk, you can't retry around it, and you can't/shouldn't move it into a stream handler and hope.&lt;/p&gt;

&lt;p&gt;But you can stop having two writes. Well, two writes, but one transaction. 😅&lt;/p&gt;

&lt;p&gt;Remember, what we're solving with the outbox pattern is a correctness problem. If someone asks "&lt;em&gt;how can you guarantee the system is eventually consistent, even under backpressure?&lt;/em&gt;" this is what you reach for. It took me years to arrive here naturally and I've been accidentally building holes in my systems longer than I'd care to admit.&lt;/p&gt;

&lt;p&gt;This pattern does make your architecture more complicated, but the trade-offs are often worth it, especially at scale. In exchange for 2x the WCUs, you get a solid way to store and act upon intent in your system. Anyway, take a look at how you're publishing events and see if there are any unexplained missing events in your system. If there are, you know what to do. 😉 That's what I did and you should be reliably getting your newsletters from now on.&lt;/p&gt;

&lt;p&gt;Happy coding!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>distributedsystems</category>
      <category>eventdriven</category>
    </item>
    <item>
      <title>Orchestrating workflows with Amazon S3 Files and AWS Lambda durable functions using Java SDK - Part 3 Waits and callbacks</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 24 Aug 2026 14:30:00 +0000</pubDate>
      <link>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-oo4</link>
      <guid>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-oo4</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1k6e"&gt;part 2&lt;/a&gt; of the series, we explored how to use the AWS Lambda Durable Execution SDK for Java to create and execute durable steps synchronously and asynchronously. In this part, we'll extend our application by adding and implementing &lt;em&gt;wait&lt;/em&gt; and &lt;em&gt;callback&lt;/em&gt; operations to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sample application with wait for callback
&lt;/h2&gt;

&lt;p&gt;Let's explore waits and callbacks. We get access to them through &lt;em&gt;DurableContext&lt;/em&gt;. Waits are planned pauses where your function stops running and stops charging until it's time to continue. Use them for time periods, external callbacks, or polling for a condition. Callbacks suspend execution until an external system sends a result. Use this for human approvals, webhooks, or any event-driven workflow. In our sample application, we'll combine both and use &lt;em&gt;waitForCallback&lt;/em&gt;. It simplifies callback handling by combining callback creation and submission in one operation.&lt;/p&gt;

&lt;p&gt;I first copied the application that we created in &lt;a href="https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1k6e"&gt;part 2&lt;/a&gt; into the &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/tree/main/aws-s3-files-lambda-durable-functions-with-wait-for-callback-java-25" rel="noopener noreferrer"&gt;aws-s3-files-lambda-durable-functions-with-wait-for-callback-java-25&lt;/a&gt; repository. We'll work on it to extend our application. Please go through the content of part 2 to understand the sample application and the basic concepts of Lambda durable functions. We'll completely reuse the Infrastructure as Code part as well.&lt;/p&gt;

&lt;p&gt;Let's add another, a bit artificial use case to our sample application. After searching for the author's YouTube videos and upcoming talks, the author should confirm those talks. For this, let's extend our &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-with-wait-for-callback-java-25/src/main/java/dev/vkazulkin/handler/AsyncAuthorContentExtractor.java" rel="noopener noreferrer"&gt;AsyncAuthorContentExtractor&lt;/a&gt; Lambda function and add the following invocation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;upcomingTalkApprovalStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitForUpcomingTalksApproval&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, let's look at the implementation of the &lt;em&gt;waitForUpcomingTalksApproval&lt;/em&gt; method in the &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-with-wait-for-callback-java-25/src/main/java/dev/vkazulkin/handler/AbstractAuthorContentExtractor.java" rel="noopener noreferrer"&gt;AbstractAuthorContentExtractor&lt;/a&gt; class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nc"&gt;UpcomingTalkApprovalStatus&lt;/span&gt; &lt;span class="nf"&gt;waitForUpcomingTalksApproval&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DurableContext&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Author&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UpcomingTalks&lt;/span&gt; &lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;waitCallbackConfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;WaitForCallbackConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
           &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;callbackConfig&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CallbackConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
           &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofHours&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
           &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
       &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;waitForCallback&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"wait-for-approval"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
             &lt;span class="nc"&gt;UpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
             &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;callbackId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stepCtx&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; 
              &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendApprovalRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;callbackId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
             &lt;span class="n"&gt;waitCallbackConfig&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;sendApprovalRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;callbackId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Author&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UpcomingTalks&lt;/span&gt; &lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
       &lt;span class="no"&gt;LOGGER&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"get approval for the talk of  "&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" for the talks "&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;upcomingTalks&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; 
       &lt;span class="s"&gt;" with the callback id "&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;callbackId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's explain step by step what is happening here. We use &lt;em&gt;DurableContext&lt;/em&gt; to create the &lt;em&gt;waitForCallback&lt;/em&gt; operation. We give it a name and the return type. This return type corresponds to the type of the object that will be passed in the callback. We also provide the submitter function &lt;em&gt;sendApprovalRequest&lt;/em&gt;, receiving the callback ID and a &lt;em&gt;StepContext&lt;/em&gt;. In this function, we can perform the desired operation, like sending the talk approval request to the author. We won't implement this functionality but only log the callback ID, which we'll require later to send the callback. We can optionally pass the &lt;em&gt;WaitForCallbackConfig&lt;/em&gt;. In our case, we only set the &lt;em&gt;CallbackConfig&lt;/em&gt; with a timeout of 1 hour. This means that if no callback arrives within this period of time, the Lambda durable function will fail. We can also set the &lt;em&gt;StepConfig&lt;/em&gt;. Please review part 2 for more information. &lt;/p&gt;

&lt;p&gt;When we invoke the &lt;em&gt;waitForCallback&lt;/em&gt; method, the invocation will block until the callback arrives. After its arrival, the payload of the callback is deserialized into the object of type &lt;em&gt;UpcomingTalkApprovalStatus&lt;/em&gt;, and this object will be returned.&lt;/p&gt;

&lt;p&gt;Now we can build and package our application with &lt;em&gt;mvn clean package&lt;/em&gt; and deploy it with &lt;em&gt;sam deploy&lt;/em&gt;.  The deployment process can take up to 10 minutes because of the creation and mounting of S3 Files.&lt;/p&gt;

&lt;p&gt;To test our Lambda durable function, we can navigate to the Lambda service, search for the &lt;em&gt;AsyncAuthorContentExtractor&lt;/em&gt; function, and go to the "Test" tab.&lt;/p&gt;

&lt;p&gt;We need to pass the following sample JSON Event to it, which represents the author:&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;"firstName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Vadym"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lastName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Kazulkin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bla@bla.de"&lt;/span&gt;&lt;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;Then we can test it. After that, we go to the "Durable execution" tab and can see all the execution details:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvj1tfu7l7qxabcz7yw8z.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvj1tfu7l7qxabcz7yw8z.png" alt=" " width="800" height="652"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What we see here is that the first 2 steps (searching for YouTube videos and upcoming talks) have already been executed asynchronously. Now, our Lambda function is in the "wait for the approval" state. The invocation of the Lambda durable function is now completed. Why? Because waits suspend our function without incurring charges. &lt;/p&gt;

&lt;p&gt;Now let's send the reply (callback) to our Lambda durable function. We first need to grab the callback ID from the log. There are several ways to send the callback (success or failure). We can use the Lambda CLI &lt;a href="https://docs.aws.amazon.com/cli/latest/reference/lambda/send-durable-execution-callback-success.html" rel="noopener noreferrer"&gt;send-durable-execution-callback-success&lt;/a&gt; or &lt;a href="https://docs.aws.amazon.com/cli/latest/reference/lambda/send-durable-execution-callback-failure.html" rel="noopener noreferrer"&gt;execution-callback-failure&lt;/a&gt; commands. Alternatively, we can use the AWS SDK to do the same. Let's use the AWS Java SDK for it. First, we need to add the following dependency to &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-with-wait-for-callback-java-25/pom.xml" rel="noopener noreferrer"&gt;pom.xml&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;software.amazon.awssdk&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;lambda&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We implemented the functionality to send the callback in the &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-with-wait-for-callback-java-25/src/main/java/dev/vkazulkin/callback/SendDurableExecutionCallback.java" rel="noopener noreferrer"&gt;SendDurableExecutionCallback&lt;/a&gt; class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SendDurableExecutionCallback&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;CALLBACK_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="s"&gt;"Ab9hZXiWYXJuOmF3czpsYW1iZGE6dXMtZWFzdC0xOjI2NTYzNDI1NzYxMDpmdW5jdGlvbjpBdXRob3JDb250ZW50RXh0cmFjdG9yOiRMQVRFU1QvZHVyYWJsZS1leGVjdXRpb24vc2VhcmNoRm9yVmFkeW0xMzUvMmM2NzRiOWEtNTQ0OC0zZDVjLWI3MzUtNTQ1NWM5ODA5MWUwYWl4JDA2N2JkNmRkLTViN2MtNGZmYi05NmY4LTRkM2Y1Yjk3MWE0Zf8"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;LambdaClient&lt;/span&gt; &lt;span class="no"&gt;LAMBDA_CLIENT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
  &lt;span class="nc"&gt;LambdaClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Region&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;US_EAST_1&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;ObjectMapper&lt;/span&gt; &lt;span class="no"&gt;MAPPER&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;ObjectMapper&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;approvedStatus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;MAPPER&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeValueAsString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
         &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;UpcomingTalkApprovalStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UpcomingTalks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDefaultUpcomingTalks&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"approved"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;LAMBDA_CLIENT&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendDurableExecutionCallbackSuccess&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                  &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;callbackId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;CALLBACK_ID&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SdkBytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromUtf8String&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;approvedStatus&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
                  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, we need to configure the callback ID (please replace it with your individual one). Then, we create the Lambda client. Next, we invoke its &lt;em&gt;sendDurableExecutionCallbackSuccess&lt;/em&gt; method to send the successful callback. We pass the callback ID and payload as JSON. This payload should be deserializable into the object of type &lt;em&gt;UpcomingTalkApprovalStatus&lt;/em&gt; (see above). Similarly, we can send the failure callback by using the &lt;em&gt;sendDurableExecutionCallbackFailure&lt;/em&gt; operation. Please move this logic with all required dependencies to the service that sends the callbacks. Now let's send the callback (approximately 23 minutes later) and see what happens:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fet212hn5pb0xlw2bnfxj.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fet212hn5pb0xlw2bnfxj.png" alt=" " width="800" height="202"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We observe that the callback was successfully received and processed. After it, the last &lt;em&gt;writeAuthorContentToFile-step&lt;/em&gt; step was executed as well. But we also observe that execution of our Lambda durable function started from the beginning. What happened here exactly? After our function resumes from a pause or interruption, the system performs replay. During replay, our code runs from the beginning but skips over completed checkpoints, using stored results instead of re-executing completed operations. This replay mechanism ensures consistency while enabling long-running executions.&lt;/p&gt;

&lt;p&gt;Here is the picture, which describes the concept that I took from this &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" rel="noopener noreferrer"&gt;source&lt;/a&gt;:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb4lrx9jv7f51ujh8725g.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb4lrx9jv7f51ujh8725g.png" alt=" " width="800" height="777"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are also other individual operations that &lt;a href="https://github.com/aws/aws-durable-execution-sdk-java" rel="noopener noreferrer"&gt;AWS Lambda Durable Execution SDK for Java&lt;/a&gt; supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;wait&lt;/em&gt; - wait suspends the function and resumes after the specified duration. There is no charge during suspension.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;waitAsync&lt;/em&gt; - starts the wait timer but returns a DurableFuture immediately, allowing other operations to run concurrently. The execution only suspends when you call .get() on the future (if the wait hasn't completed yet).&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;createCallback&lt;/em&gt; - callbacks suspend execution until an external system sends a result. Use this for human approvals, webhooks, or any event-driven workflow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this part of the series, we explored how to use the AWS Lambda Durable Execution SDK for Java to implement wait and callback.  &lt;/p&gt;

&lt;p&gt;In the next part, we'll explore the "wait for condition" operation to poll a condition until it's met. Still, our Lambda durable function itself contains too much business logic. Ideally, it should simply be the orchestrator and contain as little business logic as possible. That's why, in the subsequent parts, we'll move the logic for each step into a separate Lambda function. With that, we'll explore how to invoke another Lambda function within the durable step. We'll also show how to invoke multiple Lambda functions in parallel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you like my content, please follow me on &lt;a href="https://github.com/Vadym79" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and give my repositories a star!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also check out my &lt;a href="https://vkazulkin.com" rel="noopener noreferrer"&gt;website&lt;/a&gt; for more technical content and upcoming public speaking activities.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>lambdadurablefunctions</category>
      <category>s3files</category>
    </item>
    <item>
      <title>Orchestrating workflows with Amazon S3 Files and AWS Lambda durable functions using Java SDK - Part 2 Durable synchronous and asynchronous steps</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 17 Aug 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1k6e</link>
      <guid>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1k6e</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1gp3"&gt;part 1&lt;/a&gt;, we introduced what we'll cover in this series and our sample application. We also gave a short introduction to the services that we'll use throughout this series: &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" rel="noopener noreferrer"&gt;AWS Lambda durable functions&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files.html#s3-files-what-is" rel="noopener noreferrer"&gt;Amazon S3 Files&lt;/a&gt;. In this part of the series, we'll explore how to use the AWS Lambda Durable Execution SDK for Java to create and execute durable steps synchronously and asynchronously.&lt;/p&gt;

&lt;h2&gt;
  
  
  First implementation attempt of our sample application
&lt;/h2&gt;

&lt;p&gt;Throughout this article series, we'll develop a simple application to show how to build multi-step applications and orchestrate workflows using AWS Lambda durable functions. This simple application provides the functionality to extract the content for the provided user. To this content belongs searching for YouTube videos of the authors and their upcoming talks. We'll store the result on Amazon S3 Files in JSON format. As this operation may potentially take time, we'll additionally provide access to the author content through the Amazon API Gateway:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0l2gsqbvyfq2dh2gjpzn.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0l2gsqbvyfq2dh2gjpzn.png" alt=" " width="510" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We'll start with the search for YouTube videos and upcoming talks of the author synchronously. For the sake of simplicity, we won't implement any persistent layer and store the static content in memory. We'll store the result on Amazon S3 Files in JSON format. &lt;/p&gt;

&lt;p&gt;You can find the code examples in my &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk" rel="noopener noreferrer"&gt;aws-s3-files-lambda-durable-functions-java-sdk&lt;/a&gt; repository. We'll go through all the examples in more detail in the subsequent articles.&lt;/p&gt;

&lt;p&gt;First of all, let's declare some important dependencies in &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-java-25/pom.xml" rel="noopener noreferrer"&gt;pom.xml&lt;/a&gt;. The most important one is the &lt;a href="https://github.com/aws/aws-durable-execution-sdk-java" rel="noopener noreferrer"&gt;AWS Lambda Durable Execution SDK for Java&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;software.amazon.lambda.durable&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;aws-durable-execution-sdk-java&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;2.0.0&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why do we need such an SDK?  &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html#durable-functions-how-it-works" rel="noopener noreferrer"&gt;Under the hood&lt;/a&gt;, durable functions are regular Lambda functions using a checkpoint/replay mechanism to track progress and support long-running operations through user-defined suspension points, commonly referred to as durable execution. After your function resumes from a pause or interruption, the system performs replay. During replay, your code runs from the beginning but skips over completed checkpoints, using stored results instead of re-executing completed operations. This replay mechanism ensures consistency while enabling long-running executions.&lt;/p&gt;

&lt;p&gt;To harness this checkpoint-and-replay mechanism in your applications, Lambda provides a durable execution SDK. The SDK abstracts away the complexity of managing checkpoints and replay, exposing simple primitives called durable operations that you use in your code. The SDK integrates seamlessly with your existing Lambda development workflow.&lt;/p&gt;

&lt;p&gt;To provide the implementation of our Lambda durable function, we need the class to extend &lt;em&gt;DurableHandler&lt;/em&gt; and implement only the method &lt;em&gt;handleRequest&lt;/em&gt;.  Let's do it for our &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-java-25/src/main/java/dev/vkazulkin/handler/AuthorContentExtractor.java" rel="noopener noreferrer"&gt;AuthorContentExtractor&lt;/a&gt; Lambda durable function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuthorContentExtractor&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;DurableHandler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Author&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;AuthorContent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;AbstractAuthorContentExtractor&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;AuthorContent&lt;/span&gt; &lt;span class="nf"&gt;handleRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Author&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;DurableContext&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
   &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStepConfig&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;       
   &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"searchForUpcomingTalks-step"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UpcomingTalks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stepCtx&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;searchForUpcomingTalks&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
   &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;youtubeVideos&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"searchForYouTubeVideos-step"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;YouTubeVideos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stepCtx&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;searchForYouTubeVideos&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

   &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;authorContent&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;AuthorContent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;youtubeVideos&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

   &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"writeAuthorContentToFile-step"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Void&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stepCtx&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeAuthorContentToFile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authorContent&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;      

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;authorContent&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
   &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's go step by step through what's happening here. First of all, we also implement the &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-java-25/src/main/java/dev/vkazulkin/handler/AbstractAuthorContentExtractor.java" rel="noopener noreferrer"&gt;AbstractAuthorContentExtractor&lt;/a&gt; interface. We place some common methods there that we'll use when improving our application.&lt;/p&gt;

&lt;p&gt;Now let's introduce the concept of durable steps. Steps run business logic with built-in retries and automatic checkpointing. Each step saves its result, so your function resumes from the last completed step after an interruption. With the SDK, you wrap your Lambda event handler, which then provides a &lt;em&gt;DurableContext&lt;/em&gt; alongside your event. This context gives you access to durable operations like steps. You write your function logic as normal sequential code, but instead of calling services directly, you wrap those calls in steps for automatic checkpointing and retries. When you need to pause execution, you add waits that suspend your function without incurring charges.  We'll show it in the next part of the series. The SDK handles all the complexity of state management and replay behind the scenes, so your code remains clean and readable.&lt;/p&gt;

&lt;p&gt;Here is the picture, which describes the concept that I took from this &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" rel="noopener noreferrer"&gt;source&lt;/a&gt;:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb4lrx9jv7f51ujh8725g.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb4lrx9jv7f51ujh8725g.png" alt=" " width="800" height="777"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We wrapped several operations into separate durable steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;searchForUpcomingTalks-step&lt;/li&gt;
&lt;li&gt;searchForYouTubeVideos-step&lt;/li&gt;
&lt;li&gt;writeAuthorContentToFile-step&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's look into the &lt;em&gt;searchForUpcomingTalks-step&lt;/em&gt; first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;step&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"searchForUpcomingTalks-step"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UpcomingTalks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
&lt;span class="n"&gt;stepCtx&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;searchForUpcomingTalks&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To create a step, we need to give it a name and the result type. We also have to give it the function to execute, receiving a &lt;em&gt;StepContext&lt;/em&gt;. The return type of the function should be the same as the result type. In our case, we execute the &lt;em&gt;searchForUpcomingTalks&lt;/em&gt; function, which returns the static list of the upcoming talks. Optionally, we can also pass the &lt;em&gt;StepConfig&lt;/em&gt;. In our case, we create it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stepConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StepConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;semanticsPerRetry&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StepSemantics&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AT_LEAST_ONCE_PER_RETRY&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;                 
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;retryStrategy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;RetryStrategies&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;exponentialBackoff&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="c1"&gt;// max attempts&lt;/span&gt;
      &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofSeconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;// initial delay  &lt;/span&gt;
      &lt;span class="nc"&gt;Duration&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofSeconds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;// max delay&lt;/span&gt;
      &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                      &lt;span class="c1"&gt;// backoff multiplier&lt;/span&gt;
      &lt;span class="nc"&gt;JitterStrategy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;FULL&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
   &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We create the &lt;em&gt;StepConfig&lt;/em&gt; by passing the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retry strategy: fixed delay, linear backoff, or exponential backoff. We pass the latter here.&lt;/li&gt;
&lt;li&gt;semantics per retry: at-least-once delivery (default, which we also pass). The step may be re-executed if interrupted. START checkpoint is fire-and-forget. At-most-once delivery per retry attempt. START checkpoint is awaited before user code runs.&lt;/li&gt;
&lt;li&gt;custom serializer for the step with &lt;em&gt;serDes&lt;/em&gt; method. We don't provide it here, as we're happy with the default one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;searchForYouTubeVideos-step&lt;/em&gt; step works the same. After we have collected the result of the search for the upcoming talks and YouTube videos, we use the &lt;em&gt;writeAuthorContentToFile-step&lt;/em&gt; step to serialize the result to JSON and write it to the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nc"&gt;Void&lt;/span&gt; &lt;span class="nf"&gt;writeAuthorContentToFile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AuthorContent&lt;/span&gt; &lt;span class="n"&gt;authorContent&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;authorContentAsJson&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OBJECT_MAPPER&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeValueAsString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;authorContent&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;fileName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authorContent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;firstName&lt;/span&gt;&lt;span class="o"&gt;()+&lt;/span&gt;&lt;span class="s"&gt;"-"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;authorContent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;author&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;lastName&lt;/span&gt;&lt;span class="o"&gt;()+&lt;/span&gt;&lt;span class="s"&gt;".json"&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
  &lt;span class="nc"&gt;Path&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Paths&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;WORKSPACE_MOUNT&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fileName&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;strToBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;authorContentAsJson&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBytes&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="nc"&gt;Files&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strToBytes&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use the standard Java Path API to do it.  This is because we use Amazon S3 Files, and it supports POSIX. Strictly speaking, we could also use S3 here, but I wanted to show the functionality of S3 Files.&lt;/p&gt;

&lt;p&gt;The directory where we store the file is passed as an environment variable &lt;em&gt;WORKSPACE_MOUNT&lt;/em&gt;. In our case, it is &lt;em&gt;/mnt/workspace&lt;/em&gt;. See the Infrastructure as Code explanation below, especially the &lt;em&gt;LocalMountPath&lt;/em&gt; setting of the file system configuration for the Lambda function.&lt;/p&gt;

&lt;p&gt;For infrastructure as code (IaC), I use AWS SAM, and you can find it &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-java-25/template.yaml" rel="noopener noreferrer"&gt;here&lt;/a&gt;. To indicate that our Lambda function is durable, we need to provide some additional &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-basic-concepts.html" rel="noopener noreferrer"&gt;durable config properties&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;AuthorContentExtractorFunction&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="s"&gt;...&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;FunctionName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AuthorContentExtractor&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;dev.vkazulkin.handler.AuthorContentExtractor::handleRequest&lt;/span&gt;
      &lt;span class="na"&gt;DurableConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ExecutionTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3600&lt;/span&gt;
        &lt;span class="na"&gt;RetentionPeriodInDays&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;
        &lt;span class="s"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Execution timeout. The execution timeout controls how long a durable execution can run from start to completion. This is different from the Lambda function timeout, which controls how long a single function invocation can run. A durable execution can span multiple Lambda function invocations as it progresses through checkpoints, waits, and replays. The execution timeout applies to the total elapsed time of the durable execution, not to individual function invocations. Set the Execution timeout value in seconds (default: 86400 seconds / 24 hours, minimum: 60 seconds, maximum: 31536000 seconds / 1 year).&lt;/li&gt;
&lt;li&gt;Retention period. The retention period controls how long Lambda retains execution history and checkpoint data after a durable execution completes. This data includes step results, execution state, and the complete checkpoint log. After the retention period expires, Lambda deletes the execution history and checkpoint data. You can no longer retrieve execution details or replay the execution. Set the Retention period value in days (default: 14 days, minimum: 1 day, maximum: 90 days).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a very detailed explanation of how to create S3 Files and mount them to the Lambda function, please read the brilliant article &lt;a href="https://edjgeek.com/blog/s3-files-lambda-agents/" rel="noopener noreferrer"&gt;Lambda Just Got a File System. I Put AI Agents on It&lt;/a&gt; by Eric Johnson. I mostly copied the IaC part from it and adjusted it to my needs. I only describe the main steps here:&lt;/p&gt;

&lt;p&gt;First, we reference the networking stack, which we place in a separate file. Then we create:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VPC networking for Lambda functions using S3 Files.&lt;/li&gt;
&lt;li&gt;VPC with private subnets (for Lambda + mount targets).&lt;/li&gt;
&lt;li&gt;Security groups for NFS traffic.&lt;/li&gt;
&lt;li&gt;Public subnet with a NAT gateway. We'll need them for internet access to Bedrock and Bedrock AgentCore Web Search Tool. In later parts, we'll use both services.
&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;NetworkingStack&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::Application&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;Location&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/main/resources/stacks/network.yaml&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we need to create an S3 Bucket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt; &lt;span class="na"&gt;WorkspaceBucket&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::S3::Bucket&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;BucketName &lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vadym-s3-files-workspace&lt;/span&gt;
      &lt;span class="na"&gt;BucketEncryption&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ServerSideEncryptionConfiguration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;ServerSideEncryptionByDefault&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;SSEAlgorithm&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AES256&lt;/span&gt;
      &lt;span class="na"&gt;PublicAccessBlockConfiguration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;BlockPublicAcls&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;BlockPublicPolicy&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;IgnorePublicAcls&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;RestrictPublicBuckets&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;VersioningConfiguration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Enabled&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please don't forget to rename the S3 Bucket. After that, we need to create S3 Files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;S3FileSystem&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::S3Files::FileSystem&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;Bucket&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;WorkspaceBucket.Arn&lt;/span&gt;
      &lt;span class="na"&gt;RoleArn&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;S3FilesRole.Arn&lt;/span&gt;
      &lt;span class="na"&gt;AcceptBucketWarning&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we reference an already created S3 Bucket. We also need to profile the appropriate IAM Role (see the SAM template for the IaC). Next, we need to create 2 different mount targets for the S3 File. They should use private subnets in different availability zones. Here is an example of one such mount target:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;MountTargetA&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::S3Files::MountTarget&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;FileSystemId&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;S3FileSystem.FileSystemId&lt;/span&gt;
      &lt;span class="na"&gt;SubnetId&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;NetworkingStack.Outputs.PrivateSubnetAId&lt;/span&gt;
      &lt;span class="na"&gt;SecurityGroups&lt;/span&gt;&lt;span class="pi"&gt;:&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;NetworkingStack.Outputs.MountTargetSGId&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we need to create an S3 Files access point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;S3FilesAccessPoint&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::S3Files::AccessPoint&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;FileSystemId&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;S3FileSystem.FileSystemId&lt;/span&gt;
      &lt;span class="na"&gt;PosixUser&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Uid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1000'&lt;/span&gt;
        &lt;span class="na"&gt;Gid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1000'&lt;/span&gt;
      &lt;span class="na"&gt;RootDirectory&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;/lambda&lt;/span&gt;
        &lt;span class="na"&gt;CreationPermissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;OwnerUid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1000'&lt;/span&gt;
          &lt;span class="na"&gt;OwnerGid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1000'&lt;/span&gt;
          &lt;span class="na"&gt;Permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;755'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;em&gt;CreationPermissions&lt;/em&gt; property is crucial. It auto-creates the &lt;em&gt;/lambda&lt;/em&gt; directory within our S3 bucket with the right ownership when a client first connects. Without it, the root directory is owned by root (UID 0), and Lambda (running as UID 1000 through the access point) can’t create subdirectories.&lt;/p&gt;

&lt;p&gt;Lastly, we need to provide configuration and give permissions to our Lambda function to use S3 Files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;AuthorContentExtractorFunction&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;DependsOn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MountTargetA&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;MountTargetB&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;FunctionName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AuthorContentExtractor&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;dev.vkazulkin.handler.AuthorContentExtractor::handleRequest&lt;/span&gt;
      &lt;span class="na"&gt;DurableConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;ExecutionTimeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3600&lt;/span&gt;
        &lt;span class="na"&gt;RetentionPeriodInDays&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;
      &lt;span class="na"&gt;VpcConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;SecurityGroupIds&lt;/span&gt;&lt;span class="pi"&gt;:&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;NetworkingStack.Outputs.LambdaSGId&lt;/span&gt;
        &lt;span class="na"&gt;SubnetIds&lt;/span&gt;&lt;span class="pi"&gt;:&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;NetworkingStack.Outputs.PrivateSubnetAId&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;NetworkingStack.Outputs.PrivateSubnetBId&lt;/span&gt;
      &lt;span class="na"&gt;FileSystemConfigs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;Arn&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;S3FilesAccessPoint.AccessPointArn&lt;/span&gt;
          &lt;span class="na"&gt;LocalMountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/mnt/workspace&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;Version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2012-10-17'&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;Sid&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;MountS3Files&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;s3files:ClientMount&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;s3files:ClientWrite&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;s3files:ClientRootAccess&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;S3FileSystem.FileSystemArn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we provide the VPC and file system configuration (including local mount path) and the policy for our Lambda function to use the S3 Files.&lt;/p&gt;

&lt;p&gt;Now we can build and package our application with &lt;em&gt;mvn clean package&lt;/em&gt; and deploy it with &lt;em&gt;sam deploy&lt;/em&gt;.  The deployment process can take up to 10 minutes because of the creation and mounting of S3 Files.&lt;/p&gt;

&lt;p&gt;To test our Lambda durable function, we can navigate to the Lambda service, search for the &lt;em&gt;AuthorContentExtractor&lt;/em&gt; function, and go to the "Test" tab:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbz7obubst4toraw8urlp.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbz7obubst4toraw8urlp.png" alt=" " width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We need to pass the following sample JSON Event to it, which represents the author:&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;"firstName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Vadym"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lastName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Kazulkin"&lt;/span&gt;&lt;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;Then we can test it. After that, we go to the "Durable execution" tab and can see all the execution details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input and output as JSON&lt;/li&gt;
&lt;li&gt;Logger output&lt;/li&gt;
&lt;li&gt;Details like durable operations (with individual inputs and outputs of each step) and event history. If something goes wrong, the step status will be set to failed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In our case, we invoked the durable function directly from the Lambda console. As an alternative, we can also use the Lambda SDK for it. We can also put an API Gateway in front of it if you wish and even invoke the Lambda function &lt;a href="https://dev.toSet%20up%20asynchronous%20invocation%20of%20the%20backend%20Lambda%20function"&gt;asynchronously&lt;/a&gt;. &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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffajhzt1bgyku05ikqjtl.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffajhzt1bgyku05ikqjtl.png" alt=" " width="800" height="610"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can also see the JSON file with author content in the S3 Bucket that we created. Alternatively, I implemented a &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-java-25/src/main/java/dev/vkazulkin/handler/GetAuthorContentResult.java" rel="noopener noreferrer"&gt;GetAuthorContentResult&lt;/a&gt; Lambda function with the name &lt;em&gt;GetAuthorContentResult&lt;/em&gt;. This Lambda function takes the author's first and last name and streams the content of the JSON file from the S3 bucket. This JSON file is stored in the subdirectory &lt;em&gt;/lambda&lt;/em&gt; of our created S3 bucket. I also created and put an &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-java-25/template.yaml" rel="noopener noreferrer"&gt;API Gateway&lt;/a&gt; in front of this Lambda function. Please use the &lt;em&gt;/result/{firstname}/{lastname}&lt;/em&gt; HTTP GET endpoint for it, for example /result/Vadym/Kazulkin. Be aware that it takes several minutes for the file to appear in the S3 Bucket after being written to S3 Files. If you need the result immediately, you can rewrite this Lambda to retrieve the result from S3 Files instead.&lt;/p&gt;

&lt;p&gt;This was a very simple application to explore the durable steps with the AWS Lambda Durable Execution SDK for Java. Let's improve our application a bit. Until now, we searched for the upcoming talks and YouTube videos sequentially. But we can do it in parallel. For that, we can use the &lt;em&gt;stepAsync&lt;/em&gt; operation instead of &lt;em&gt;step&lt;/em&gt;.  I provided a separate implementation, &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-java-25/src/main/java/dev/vkazulkin/handler/AsyncAuthorContentExtractor.java" rel="noopener noreferrer"&gt;AsyncAuthorContentExtractor&lt;/a&gt;, and the Lambda durable function with the name &lt;em&gt;AsyncAuthorContentExtractor&lt;/em&gt; for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;upcomingTalksFuture&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stepAsync&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"searchForUpcomingTalks-async-step"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UpcomingTalks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
&lt;span class="n"&gt;stepCtx&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;searchForUpcomingTalks&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;youtubeVideosFuture&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stepAsync&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"searchForYouTubeVideos-async-step"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;YouTubeVideos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
&lt;span class="n"&gt;stepCtx&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;searchForYouTubeVideos&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;upcomingTalks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;upcomingTalksFuture&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;youtubeVideos&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;youtubeVideosFuture&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, &lt;em&gt;stepAsync&lt;/em&gt; invocation returns a &lt;em&gt;DurableFuture&lt;/em&gt; of the result type - a future representing the step result. This operation is non-blocking; we only block when invoking the &lt;em&gt;get&lt;/em&gt; method on the result. Everything else remains the same in this asynchronous implementation as it was in the synchronous one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this part of the series, we explored how to use the AWS Lambda Durable Execution SDK for Java to create and execute durable steps synchronously and asynchronously. In the next part, we'll extend our application by adding and implementing &lt;em&gt;wait&lt;/em&gt; and &lt;em&gt;callback&lt;/em&gt; operations to it. Still, our Lambda durable function itself contains too much business logic. Ideally, it should simply be the orchestrator and contain as little business logic as possible. That's why in the later parts, we'll move the logic associated with each step into a separate Lambda function. We'll explore how to invoke another Lambda function within the durable step. We'll also show how to invoke multiple Lambda functions in parallel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you like my content, please follow me on &lt;a href="https://github.com/Vadym79" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and give my repositories a star!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also check out my &lt;a href="https://vkazulkin.com" rel="noopener noreferrer"&gt;website&lt;/a&gt; for more technical content and upcoming public speaking activities.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>lambdadurablefunctions</category>
      <category>s3files</category>
    </item>
    <item>
      <title>Orchestrating workflows with Amazon S3 Files and AWS Lambda durable functions using Java SDK - Part 1 Introduction to the series and services</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 10 Aug 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1gp3</link>
      <guid>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1gp3</guid>
      <description>&lt;h2&gt;
  
  
  Introduction to the series
&lt;/h2&gt;

&lt;p&gt;Throughout this article series, we'll develop a simple application to show how to build multi-step applications and orchestrate workflows using AWS Lambda durable functions. For it, we'll explore the &lt;a href="https://github.com/aws/aws-durable-execution-sdk-java" rel="noopener noreferrer"&gt;AWS Lambda Durable Execution SDK for Java&lt;/a&gt;. This simple application provides the functionality to extract the content for the provided user. This content involves searching for YouTube videos of the authors and their upcoming talks. We'll store the result on Amazon S3 Files in JSON format. As this operation may potentially take time, we'll additionally provide access to the author content through the Amazon API Gateway:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0l2gsqbvyfq2dh2gjpzn.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0l2gsqbvyfq2dh2gjpzn.png" alt=" " width="510" height="501"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We'll start with the simple application to search for YouTube videos and upcoming talks of the author synchronously. First, we'll use durable steps and then later add waits and callbacks. We won't implement any persistent layer and store the static content in memory. &lt;/li&gt;
&lt;li&gt;We'll then improve our application by adding asynchronous communication where it makes sense. For example, searching for YouTube videos and upcoming talks can be done in parallel.&lt;/li&gt;
&lt;li&gt;We'll then further improve our application by converting the AWS durable function to a pure orchestrator. This means we'll move the business logic for searching for YouTube videos and upcoming talks into separate Lambda functions. With that, our Lambda durable function will invoke those separate Lambda functions in parallel and combine the results&lt;/li&gt;
&lt;li&gt;Finally, we'll further extend our application to use &lt;a href="https://docs.aws.amazon.com/de_de/bedrock-agentcore/latest/devguide/gateway-target-connector-web-search-tool.html" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore Web Search Tool&lt;/a&gt; to search for the author content on the web. With that, our Lambda durable function becomes an orchestrator of the agentic AI workflows. As we'll use Java in this series, we'll explore how to run this business logic responsible for the Amazon Bedrock AgentCore Web Search Tool. As this tool is exposed as an MCP tool by the &lt;a href="https://docs.aws.amazon.com/de_de/bedrock-agentcore/latest/devguide/gateway.html" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore Gateway&lt;/a&gt;, we need a framework capable of providing AI functionality and supporting MCP. That's why we'll use Spring AI. As 2 operational environments for our agentic AI workflows, we'll explore an AWS Lambda function and &lt;a href="https://docs.aws.amazon.com/de_de/bedrock-agentcore/latest/devguide/agents-tools-runtime.html" rel="noopener noreferrer"&gt;Amazon Bedrock AgentCore Runtime&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can find the code examples in my &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk" rel="noopener noreferrer"&gt;aws-s3-files-lambda-durable-functions-java-sdk&lt;/a&gt; repository. We'll go through all the examples in more detail in the subsequent articles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction to the services used throughout the series
&lt;/h2&gt;

&lt;p&gt;Throughout this series, we'll use the following AWS services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/durable-functions.html" rel="noopener noreferrer"&gt;AWS Lambda durable functions&lt;/a&gt;. With Lambda durable functions, you can build resilient multi-step applications and AI workflows. These can execute for up to one year while maintaining reliable progress despite interruptions. When a durable function runs, this complete lifecycle is called a durable execution, which uses checkpoints to track progress and automatically recover from failures through replay, re-executing from the beginning while skipping completed work. Within each function, you use durable operations as fundamental building blocks. Steps execute business logic with built-in retries and progress tracking. Waits suspend execution without incurring compute charges, making them ideal for long-running processes. These processes include human-in-the-loop workflows or polling external dependencies. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/s3-files.html#s3-files-what-is" rel="noopener noreferrer"&gt;Amazon S3 Files&lt;/a&gt;. S3 Files is a shared file system that connects any AWS compute resource directly with your data in Amazon S3. It provides fast, direct access to all of your S3 data as files with full file system semantics and low-latency performance, without your data ever leaving S3. Every file-based application, agent, and team can access and work with your S3 data as a file system using the tools they already depend on. Built using Amazon EFS, S3 Files gives you the performance and simplicity of a file system with the scalability, durability, and cost-effectiveness of S3. You can read, write, and organize data using file and directory operations. At the same time, S3 Files manages the synchronization of changes between your bucket and file system.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We'll also introduce in the later parts of this series the following Amazon Bedrock AgentCore services or features: &lt;a href="https://docs.aws.amazon.com/de_de/bedrock-agentcore/latest/devguide/gateway.html" rel="noopener noreferrer"&gt;Gateway&lt;/a&gt;, &lt;a href="https://docs.aws.amazon.com/de_de/bedrock-agentcore/latest/devguide/gateway-target-connector-web-search-tool.html" rel="noopener noreferrer"&gt;Web Search Tool&lt;/a&gt;, and &lt;a href="https://docs.aws.amazon.com/de_de/bedrock-agentcore/latest/devguide/agents-tools-runtime.html" rel="noopener noreferrer"&gt;Runtime&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;If you like my content, please follow me on &lt;a href="https://github.com/Vadym79" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and give my repositories a star!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also check out my &lt;a href="https://vkazulkin.com" rel="noopener noreferrer"&gt;website&lt;/a&gt; for more technical content and upcoming public speaking activities.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>lambdadurablefunctions</category>
      <category>s3files</category>
    </item>
    <item>
      <title>Building AI Agents with Embabel, Spring AI and Amazon Bedrock AgentCore - Part 3 Develop Agents as web application with Embabel</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 03 Aug 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-3-develop-agents-as-2d94</link>
      <guid>https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-3-develop-agents-as-2d94</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-2-develop-agents-57nb"&gt;part 2&lt;/a&gt;, we developed our first AI agents with the Embabel shell. We learned many concepts of this framework, such as agents, actions, and goals. In this article, we'll slightly adjust our application to convert it into a web application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implement AI agents as a web application with Embabel
&lt;/h2&gt;

&lt;p&gt;You can look at my GitHub &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/tree/main/embabel-1.x-conference-app-agent-local" rel="noopener noreferrer"&gt;embabel-1.x-conference-app-agent-local&lt;/a&gt; repository for the example. We'll completely reuse the implementation of both agents, the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/agent/CreateTalksAndApplyForConferencesAgent.java" rel="noopener noreferrer"&gt;CreateTalksAndApplyForConferencesAgent&lt;/a&gt; and the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/agent/SearchForTalksAndApplyForConferencesAgent.java" rel="noopener noreferrer"&gt;SearchForTalksAndApplyForConferencesAgent&lt;/a&gt; that we implemented in &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-2-develop-agents-57nb"&gt;part 2&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Those agents are capable of responding to the following prompts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; "Please provide me with the list of conferences, including their IDs, with Java topics happening in 2027, with the call for papers open today. Also, provide me with the list of my talks with this topic in the title. Finally, for each conference and talk retrieved, apply individually for the conference." &lt;em&gt;SearchForTalksAndApplyForConferencesAgent&lt;/em&gt; is responsible for responding to this prompt.&lt;/li&gt;
&lt;li&gt;"Please create a talk with a cool title (max 60 characters long) and description (max 300 characters long) about using Spring AI on the Amazon Bedrock AgentCore service. Then provide me with the list of conferences, including their IDs, with Java topics happening in 2026 and 2027, with the call for papers open today. Finally, for each conference, apply individually for it with the talk just created." &lt;em&gt;CreateTalksAndApplyForConferencesAgent&lt;/em&gt; is responsible for responding to this prompt.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;First, let's check that we declared the following dependencies in &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/pom.xml" rel="noopener noreferrer"&gt;pom.xml&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt; &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.embabel.agent&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;embabel-agent-starter-bedrock&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;${embabel-agent.version}&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-web&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
 &lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, make sure not to declare the &lt;em&gt;embabel-agent-starter-shell&lt;/em&gt; dependency, as we don't use Embabel shell in this article.&lt;/p&gt;

&lt;p&gt;All we need to do now is to additionally implement the &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/controller/EmbabelAgentController.java" rel="noopener noreferrer"&gt;EmbabelAgentController&lt;/a&gt; controller.&lt;/p&gt;

&lt;p&gt;Let's first implement a generic endpoint that will accept both prompts. Let's first autowire and create some objects coming from Embabel that we'll use for this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmbabelAgentController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;AgentPlatform&lt;/span&gt; &lt;span class="n"&gt;agentPlatform&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;AgentInvocation&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;invocation&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ProcessOptions&lt;/span&gt; &lt;span class="n"&gt;processOptions&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;ProcessOptions&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withVerbosity&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;Verbosity&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withShowPrompts&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withShowLlmResponses&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withShowPlanning&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
     &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;withDebug&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;


&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;EmbabelAgentController&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;AgentPlatform&lt;/span&gt; &lt;span class="n"&gt;agentPlatform&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;agentPlatform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;agentPlatform&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invocation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AgentInvocation&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agentPlatform&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;processOptions&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;...&lt;/span&gt;

&lt;span class="o"&gt;}&lt;/span&gt;   
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, we autowire the Embabel's &lt;em&gt;AgentPlatform&lt;/em&gt; object and use it to create the &lt;em&gt;AgentInvocation&lt;/em&gt; object. We additionally pass the &lt;em&gt;ProcessOption&lt;/em&gt; object that is configured to show planning, LLM responses, and prompts. We also pass the type of the response - the domain object returned after the agents achieve their goals; in our case, the  &lt;a href="https://github.com/Vadym79/amazon-bedrock-agentcore-spring-ai/blob/main/embabel-1.x-conference-app-agent-local/src/main/java/dev/vkazulkin/embabel/domain/Domain.java" rel="noopener noreferrer"&gt;Domain.ConferenceApplications&lt;/a&gt; class:&lt;/p&gt;

&lt;p&gt;Now, let's implement the endpoint itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/generic"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"text/plain"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt; &lt;span class="nf"&gt;genericPrompt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestParam&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"request"&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;UserInput&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt; 
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invocation&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
         &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;last&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;    
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we wrap the prompt into Embabel's &lt;em&gt;UserInput&lt;/em&gt; object that we put into the map with all inputs required to run the agent. Then we use the &lt;em&gt;AgentInvocation&lt;/em&gt; object to run the agent with the specified input synchronously. We can also invoke &lt;em&gt;runAsync&lt;/em&gt; to run the agent asynchronously. Finally, we retrieve the result of the agent invocation with the specified type.&lt;/p&gt;

&lt;p&gt;Let's build the application with &lt;code&gt;mvn clean package&lt;/code&gt; and run it locally with &lt;code&gt;mvn spring-boot:run&lt;/code&gt;. We'll send the prompt number 1 to this endpoint. What we'll see is that Embabel picked the wrong agent, &lt;em&gt;CreateTalksAndApplyForConferencesAgent&lt;/em&gt;, to run. Why? As we don't use the Embabel shell, Embabel doesn't analyze the prompt and compute the score to determine what exact agent to run. It currently only looks into the return type of all agents and picks the first one that matches the type defined in the &lt;em&gt;AgentInvocation&lt;/em&gt; object. But both our agents have the same return type &lt;em&gt;Domain.ConferenceApplications&lt;/em&gt;. Their goals, in the end, are the same: to apply for the conferences. With that, for our 2 agents, there is a 50% probability that Embabel will pick the wrong agent to run.&lt;/p&gt;

&lt;p&gt;How can we fix that? For example, we can define the endpoint per agent. Let's do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/applyToConferencesWithExistingTalks"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"text/plain"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt; &lt;span class="nf"&gt;applyToConferencesWithExistingTalks&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestParam&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invokeAgent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
       &lt;span class="nc"&gt;SearchForTalksAndApplyForConferencesAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AGENT_NAME&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;   
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"/applyToConferencesWithNewTalks"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"text/plain"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt; &lt;span class="nf"&gt;applyToConferencesWithNewTalks&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestParam&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invokeAgent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;CreateTalksAndApplyForConferencesAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AGENT_NAME&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; 
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I additionally wrote 2 helper methods, &lt;em&gt;invokeAgent&lt;/em&gt; and &lt;em&gt;getAgentByName&lt;/em&gt;, to simplify the agent search by name and its subsequent invocation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt; &lt;span class="nf"&gt;invokeAgent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;agentName&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;        
     &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"request"&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;UserInput&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
     &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAgentByName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agentName&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
     &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;agentProcess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;agentPlatform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createAgentProcess&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
 &lt;span class="n"&gt;processOptions&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;agentProcess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;last&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Domain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ConferenceApplications&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt; &lt;span class="nf"&gt;getAgentByName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;agentName&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
     &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;optionalAgent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;agentPlatform&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;agents&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;filter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agentName&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findFirst&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

     &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt; &lt;span class="n"&gt;optionalAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&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="nf"&gt;RuntimeException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"agent with the name "&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;agentName&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;" not found"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
     &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;optionalAgent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's explain what happens in the &lt;em&gt;invokeAgent&lt;/em&gt; method. We first invoke the &lt;em&gt;getAgentByName&lt;/em&gt; method. It searches in the collection of agents that the agent platform knows for one that has a specified name. All classes annotated with Embabel's &lt;em&gt;Agent&lt;/em&gt; annotation will be registered in the agent platform. Then we use the &lt;em&gt;AgentPlatform&lt;/em&gt; object to create the &lt;em&gt;AgentProcess&lt;/em&gt; and pass the found agent, the already described process options, and inputs to it. Then we run the &lt;em&gt;AgentProcess&lt;/em&gt; and return its last output of type &lt;em&gt;Domain.ConferenceApplications.class&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Now we send prompt number 1 (see above) to the &lt;em&gt;applyToConferencesWithExistingTalks&lt;/em&gt; endpoint and prompt number 2 to the &lt;em&gt;applyToConferencesWithNewTalks&lt;/em&gt; endpoint. We'll see something similar as a result, as we saw in &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-embabel-spring-ai-and-amazon-bedrock-agentcore-part-2-develop-agents-57nb"&gt;part 2&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For example, here I use &lt;a href="https://httpie.io/" rel="noopener noreferrer"&gt;httpie&lt;/a&gt; to send prompt number 1: &lt;br&gt;
&lt;code&gt;http GET http://localhost:8080/applyToConferencesWithExistingTalks?prompt="Please provide me with the list of conferences including their IDs with Java topic happening in 2026 and 2027 with call for papers open today. Also provide me with the list of my talks with this topic in the title. Finally, for each conference and talk retrieved, apply individually for the conference." Content-Type:text/plain&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is the response of the agent:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxq4w3wz6pde5nonc5twv.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxq4w3wz6pde5nonc5twv.png" alt=" " width="799" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Having one endpoint per Embabel agent has its pros and cons. The biggest disadvantage is that a service like &lt;a href="https://dev.to/aws-heroes/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-5-deploy-mcp-client-for-1n11"&gt;AgentCore Runtime&lt;/a&gt; exposes only one HTTP POST &lt;em&gt;/invocations&lt;/em&gt; endpoint to send the prompt to the web application. This means that we'll need to think about how to design, implement, and deploy each Embabel agent on a separate AgentCore Runtime. Each agent belonging to the same web application will have a lot of shared logic with other agents. This is not very convenient. The biggest advantage is that you can scale each Embabel agent individually. AgentCore Runtime will do it for you.&lt;/p&gt;

&lt;p&gt;It's worth checking for the upcoming version of Embabel whether there are other options available &lt;a href="https://docs.embabel.com/embabel-agent/guide/1.0.0-SNAPSHOT/#reference.invoking" rel="noopener noreferrer"&gt;to invoke the agent&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;In this article, we covered how to develop a web application with Embabel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you like my content, please follow me on &lt;a href="https://github.com/Vadym79" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; and give my repositories a star!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also check out my &lt;a href="https://vkazulkin.com" rel="noopener noreferrer"&gt;website&lt;/a&gt; for more technical content and upcoming public speaking activities.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springai</category>
      <category>embabel</category>
      <category>bedrockagentcore</category>
    </item>
    <item>
      <title>EventBridge Scheduler DLQ'd My Successful Invokes</title>
      <dc:creator>Danielle Heberling</dc:creator>
      <pubDate>Sat, 01 Aug 2026 18:00:00 +0000</pubDate>
      <link>https://dev.to/aws-heroes/eventbridge-scheduler-dlqd-my-successful-invokes-4fhh</link>
      <guid>https://dev.to/aws-heroes/eventbridge-scheduler-dlqd-my-successful-invokes-4fhh</guid>
      <description>&lt;p&gt;A while back I wrote about &lt;a href="https://dev.to/aws-heroes/let-an-ai-agent-do-your-job-searching-558a"&gt;letting an AI agent do your job searching&lt;/a&gt;. That post ended with a quirk I couldn't explain. EventBridge Scheduler would invoke my agent, the agent would run fine, I'd get the email, and the invocation would land in the dead letter queue anyway. Every single time.&lt;/p&gt;

&lt;p&gt;I figured I was either doing something wrong or hitting an AgentCore bug. Turns out it was neither.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Actual Problem
&lt;/h2&gt;

&lt;p&gt;EventBridge Scheduler universal targets (the feature that lets Scheduler call almost any AWS API action directly, no Lambda in between) are synchronous. Scheduler makes the API call and waits for a response before deciding whether the invocation succeeded. The docs describe this as &lt;a href="https://docs.aws.amazon.com/scheduler/latest/UserGuide/what-is-scheduler.html" rel="noopener noreferrer"&gt;at-least-once delivery&lt;/a&gt;, where at least one delivery succeeds &lt;em&gt;with a response from the target&lt;/em&gt;. I glossed over that last part.&lt;/p&gt;

&lt;p&gt;My schedule was calling the Bedrock AgentCore &lt;code&gt;invokeAgentRuntime&lt;/code&gt; action, which blocks until the agent finishes. A search takes 30 to 75 seconds. Scheduler gives up around the 30 second mark and marks the invocation failed. The agent, which has no idea any of this happened, keeps going, finishes the search, and publishes to SNS. I get my email. The "failed" invocation goes to the DLQ. Here's what one of those messages looks like:&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6olsz1jsl6yc8xajknj.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fy6olsz1jsl6yc8xajknj.png" alt="EventBridge Scheduler DLQ message showing an ErrorCode and ErrorMessage after a successful AgentCore invocation." width="800" height="470"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everything downstream worked. The only broken part was Scheduler's opinion of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  About That 30 Seconds
&lt;/h2&gt;

&lt;p&gt;I went looking for this in the docs and could not find it. The &lt;a href="https://docs.aws.amazon.com/scheduler/latest/UserGuide/scheduler-quotas.html" rel="noopener noreferrer"&gt;EventBridge Scheduler quotas page&lt;/a&gt; covers number of schedules, API request rates, and invocation throughput. Nothing about how long Scheduler waits for a target to respond.&lt;/p&gt;

&lt;p&gt;The one timeout you &lt;em&gt;can&lt;/em&gt; configure is &lt;code&gt;MaximumEventAgeInSeconds&lt;/code&gt; in the &lt;a href="https://docs.aws.amazon.com/scheduler/latest/APIReference/API\_Target.html" rel="noopener noreferrer"&gt;retry policy&lt;/a&gt;. That's the maximum age of an event across retries, not a per-call limit. Different thing.&lt;/p&gt;

&lt;p&gt;So treat ~30 seconds as an observed number, not a contract. In my opinion the safer bet is designing against "Scheduler will not wait long" rather than a specific value.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;Instead of making the search faster, I made the response faster. The entrypoint now kicks the search off as a background task and returns immediately, so Scheduler gets its response well inside the window and the agent runs for as long as it needs to (up to AgentCore's 8 hour session cap, which my searches are nowhere near).&lt;/p&gt;

&lt;p&gt;AgentCore has first-class support for this. You register the background work with &lt;code&gt;add_async_task&lt;/code&gt;, the runtime reports &lt;code&gt;HealthyBusy&lt;/code&gt; on &lt;code&gt;/ping&lt;/code&gt; while it's in flight, and the session stays alive instead of getting reaped. That last part matters because AgentCore terminates sessions after 15 minutes of idle, and without task tracking your background work looks exactly like idle. The &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-long-run.html" rel="noopener noreferrer"&gt;async processing docs&lt;/a&gt; cover the pattern.&lt;/p&gt;

&lt;p&gt;Here's the relevant part of the entrypoint:&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="c1"&gt;# strong refs: asyncio only weakly references tasks
&lt;/span&gt;&lt;span class="n"&gt;_background_tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Task&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="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_tracked_job_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;company&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;title&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;location&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="bp"&gt;None&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 the job search as a tracked async task so ping reports HealthyBusy.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_async_task&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_search&lt;/span&gt;&lt;span class="sh"&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run_job_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete_async_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.entrypoint&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;invoke&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="nb"&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="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&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="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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Main entrypoint for the agent invocation.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# ...payload validation...
&lt;/span&gt;
    &lt;span class="k"&gt;if&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;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;sync&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;run_job_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Respond before EventBridge Scheduler's ~30s call timeout DLQs the invocation
&lt;/span&gt;    &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_tracked_job_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;_background_tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_done_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_background_tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;discard&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;accepted&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;search_criteria&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;company&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;location&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;Scheduler gets &lt;code&gt;{"status": "accepted"}&lt;/code&gt; in well under a second. The search runs in the background. No more DLQ.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Few Things to Watch For
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Strands call has to be awaited.&lt;/strong&gt; I was calling &lt;code&gt;agent(prompt)&lt;/code&gt; inside an &lt;code&gt;async def&lt;/code&gt; entrypoint, which blocks the event loop. Survivable when the entrypoint blocked anyway. But once the runtime needs to answer health checks mid-search, a blocking call starves everything. Switching to &lt;code&gt;await agent.invoke_async(prompt)&lt;/code&gt; was required, not a style preference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hold a reference to the task.&lt;/strong&gt; &lt;code&gt;asyncio&lt;/code&gt; only weakly references tasks from &lt;a href="https://docs.python.org/3/library/asyncio-task.html#asyncio.create\_task" rel="noopener noreferrer"&gt;&lt;code&gt;create_task&lt;/code&gt;&lt;/a&gt;, so an unreferenced task can get garbage collected mid-run. That's the &lt;code&gt;_background_tasks&lt;/code&gt; set above, lifted straight from the pattern in the Python docs. It usually works fine right up until it doesn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep a synchronous path.&lt;/strong&gt; Fire and forget is great for the scheduler and terrible for local development. A &lt;code&gt;"sync": true&lt;/code&gt; flag in the payload runs the search inline and returns the full result. That's what I use for curl and the AgentCore playground.&lt;/p&gt;

&lt;h2&gt;
  
  
  One More Thing About Retries
&lt;/h2&gt;

&lt;p&gt;In the original post I disabled retries, because Scheduler retrying a "failed" invocation meant duplicate agent runs and duplicate emails. The CDK target default is 185 retries with exponential backoff over up to 24 hours, so every scheduled search was set up to keep re-running (and re-billing) until I caught it.&lt;/p&gt;

&lt;p&gt;With the async path, I turned retries back on (&lt;code&gt;retryAttempts: 0&lt;/code&gt; to &lt;code&gt;2&lt;/code&gt;). Now Scheduler gets an ack right away, so it won’t keep retrying while the search is still running. There’s still a small chance of a duplicate if the ack gets lost, but that’s a trade-off I’m comfortable with here.&lt;/p&gt;

&lt;p&gt;There's a tradeoff here. Scheduler now only confirms your agent started. It says nothing about whether the search actually worked. If the search blows up 40 seconds in, Scheduler is happy and your DLQ stays empty. You need logs, metrics, or the notification itself to know the work finished. For me a missing email is a loud signal. If your silence is more ambiguous, plan for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  If You're Hitting This
&lt;/h2&gt;

&lt;p&gt;If you're invoking AgentCore Runtime from an EventBridge schedule and seeing DLQ messages for runs that clearly worked:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check how long your agent actually takes. Anything over ~30 seconds is suspect.&lt;/li&gt;
&lt;li&gt;Return an ack from your entrypoint instead of the result.&lt;/li&gt;
&lt;li&gt;Wrap the real work in &lt;code&gt;add_async_task&lt;/code&gt; / &lt;code&gt;complete_async_task&lt;/code&gt; so the runtime knows to keep the session alive.&lt;/li&gt;
&lt;li&gt;Make sure nothing in that background path blocks the event loop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Code is in the &lt;a href="https://github.com/deeheber/job-search-agent" rel="noopener noreferrer"&gt;job-search-agent repo&lt;/a&gt; and the change is &lt;a href="https://github.com/deeheber/job-search-agent/pull/51" rel="noopener noreferrer"&gt;PR #51&lt;/a&gt; if you want to see the whole diff.&lt;/p&gt;

&lt;p&gt;None of this is AgentCore specific. Any long-running universal target invoked from EventBridge Scheduler has the same problem. Agents just happen to be really good at being slow. 😅&lt;/p&gt;

&lt;p&gt;If you've hit this with a different long-running target, or if you've found the actual timeout documented somewhere I missed, I'd love to hear about it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>aws</category>
      <category>serverless</category>
      <category>tutorial</category>
    </item>
    <item>
      <title># Blog #9: I Gave My Minecraft Mod a Serverless Brain — And It Decides How Many Allies to Summon Based on How Screwed You Are</title>
      <dc:creator>Carlos Cortez 🇵🇪 [AWS Hero]</dc:creator>
      <pubDate>Sat, 01 Aug 2026 16:13:30 +0000</pubDate>
      <link>https://dev.to/aws-heroes/-blog-9-i-gave-my-minecraft-mod-a-serverless-brain-and-it-decides-how-many-allies-to-summon-5bid</link>
      <guid>https://dev.to/aws-heroes/-blog-9-i-gave-my-minecraft-mod-a-serverless-brain-and-it-decides-how-many-allies-to-summon-5bid</guid>
      <description>&lt;h2&gt;
  
  
  I Gave My Minecraft Mod a Serverless Brain — And It Decides How Many Allies to Summon Based on How Screwed You Are
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;Cloud Swords Mod — Blog 9&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I've been building a Minecraft mod that teaches cloud computing through gameplay. Swords named after AWS services. Energy systems that work like network infrastructure. Villagers that trade cloud certifications.&lt;/p&gt;

&lt;p&gt;But version 0.3.8 is where things got weird. I connected the mod to an actual AI agent running in the cloud. Now when you swing the Sword of Lambda, instead of spawning a fixed number of minions — an AI evaluates your combat situation and decides how many allies you need.&lt;/p&gt;

&lt;p&gt;The game never freezes. The AI never breaks balance. And the whole thing runs on a serverless backend that costs essentially nothing.&lt;/p&gt;

&lt;p&gt;Here's how I built it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Items That Make It Work
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Tome of API Key
&lt;/h3&gt;

&lt;p&gt;Every cloud connection starts with authentication. In the mod, that's the &lt;strong&gt;Tome of API Key&lt;/strong&gt; — a rare artifact found in dungeon chests (8% drop rate in bastions, end cities, nether fortresses).&lt;/p&gt;

&lt;p&gt;When you first pick it up, it generates a unique API key (&lt;code&gt;csk-XXXX&lt;/code&gt; format) and shows 100 charges. It glows with enchant shimmer. It's your passport to the cloud.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// On first inventory tick, generate unique key&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getNbt&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;contains&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api_key"&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"csk-"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="no"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;randomUUID&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;substring&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getNbt&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;putString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api_key"&lt;/span&gt;&lt;span class="o"&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;stack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getNbt&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;putInt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"charges"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tooltip shows your key preview, remaining charges, and connection status. When charges hit zero, the cloud refuses your invocations — just like hitting a rate limit on a real API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud API Requester (Block)
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Cloud API Requester&lt;/strong&gt; is the bridge between Minecraft and AWS. It's a crafted block (Iron + Circuit Board + Redstone + Ender Pearl + Data Core) with two slots:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your Tome of API Key&lt;/li&gt;
&lt;li&gt;The weapon you want to cloud-enable&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Drop both in, wait ~8 seconds (the "deployment timer"), and your weapon gets marked with &lt;code&gt;cloud_enabled=true&lt;/code&gt; in its NBT data. The tooltip now shows "☁ Cloud Enabled" in cyan.&lt;/p&gt;

&lt;p&gt;This is the CI/CD pipeline of the mod. You're deploying your weapon to the cloud.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud-Enabled Weapons
&lt;/h3&gt;

&lt;p&gt;Once cloud-enabled, the Sword of Lambda changes behavior completely. Instead of spawning a hardcoded number of minions, it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Sends your combat context to the backend (health, nearby mobs, biome, dimension)&lt;/li&gt;
&lt;li&gt;Shows "☁ Cloud invocation sent..." in the actionbar&lt;/li&gt;
&lt;li&gt;Waits for the AI's decision&lt;/li&gt;
&lt;li&gt;Applies it: "☁ Cloud decided: 5 minions (Critical threat — max invocations)"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're NOT cloud-enabled, the sword works exactly as before. Backward compatible. No cloud required.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────┐
│                     MINECRAFT CLIENT                         │
│                                                             │
│  Player swings Sword of Lambda (cloud-enabled)              │
│       │                                                     │
│       ▼                                                     │
│  CloudCombatHandler builds context:                         │
│  {player, weapon, health, biome, dimension, nearby_mobs}   │
│       │                                                     │
│       ▼                                                     │
│  CloudApiClient.sendEvent(context)  ← async, non-blocking  │
└───────┬─────────────────────────────────────────────────────┘
        │ POST /event
        ▼
┌─────────────────────────────────────────────────────────────┐
│                    BACKEND (CDK Stack)                        │
│                                                             │
│  API Gateway (rate limited: 100/day, 10/sec)                │
│       │                                                     │
│       ▼                                                     │
│  Lambda: IngestEvent                                        │
│  - Receives context                                         │
│  - Calls Strands Agent (or rule-based fallback)             │
│  - Stores decision in DynamoDB (TTL: 60s)                   │
│  - Returns {request_id}                                     │
│                                                             │
│  DynamoDB: CloudSwordsResults                               │
│  - Partition key: request_id                                │
│  - TTL auto-cleanup                                         │
│                                                             │
│  Lambda: PollResult                                         │
│  - GET /result/{request_id}                                 │
│  - Returns decision when ready                              │
└───────┬─────────────────────────────────────────────────────┘
        │ Poll every 2s
        ▼
┌─────────────────────────────────────────────────────────────┐
│  CloudApiClient receives decision                           │
│       │                                                     │
│       ▼                                                     │
│  CloudCombatHandler.applyDecision()                         │
│  - Summon "☁ Cloud Invocation" minions (named, cyan)        │
│  - Or apply buff (regeneration, resistance, strength)       │
│  - Or do nothing ("No action needed")                       │
└─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Async Pattern: Fire-and-Forget + Polling
&lt;/h2&gt;

&lt;p&gt;The game can NEVER freeze waiting for a network response. So I used the same pattern you'd use in any real-time application:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fire&lt;/strong&gt;: Send the event, get back a &lt;code&gt;request_id&lt;/code&gt; immediately&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forget&lt;/strong&gt;: Continue gameplay, don't block&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Poll&lt;/strong&gt;: Every 2 seconds (via &lt;code&gt;ServerTickEvents&lt;/code&gt;), check if the result is ready&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply&lt;/strong&gt;: When ready, queue the callback for the server thread
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CloudApiClient — non-blocking&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;sendEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JsonObject&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendAsync&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;HttpResponse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;BodyHandlers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ofString&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenAccept&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;requestId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parseRequestId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="n"&gt;startPolling&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requestId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;});&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Polling on server tick (every 40 ticks = 2 seconds)&lt;/span&gt;
&lt;span class="nc"&gt;ServerTickEvents&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;END_SERVER_TICK&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Execute queued callbacks on server thread&lt;/span&gt;
    &lt;span class="nc"&gt;Runnable&lt;/span&gt; &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="n"&gt;callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pendingCallbacks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;poll&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The threading fix was critical. Java's &lt;code&gt;HttpClient&lt;/code&gt; runs callbacks on its own async thread — but Minecraft entity spawning MUST happen on the server thread. Without the &lt;code&gt;ConcurrentLinkedQueue&lt;/code&gt; bridge, buffs wouldn't apply and entities would crash the game.&lt;/p&gt;




&lt;h2&gt;
  
  
  The AI Agent: Strands + Ollama Cloud
&lt;/h2&gt;

&lt;p&gt;The brain is a Strands Agent running &lt;code&gt;gpt-oss:20b&lt;/code&gt; via Ollama Cloud (free tier — no Bedrock cost for development):&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;strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands.models.ollama&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;OllamaModel&lt;/span&gt;

&lt;span class="n"&gt;SYSTEM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are a combat advisor for a Minecraft RPG mod.
Given player context, decide ONE action.

Available actions (respond ONLY with valid JSON):
- {&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="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summon&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="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: 1-5, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: 5-15, &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="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brief&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="s"&gt;action&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="s"&gt;buff&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="s"&gt;effect&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="s"&gt;regeneration|resistance|strength|speed&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="s"&gt;duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: 3-10, &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="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brief&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="s"&gt;action&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="s"&gt;none&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="s"&gt;reason&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="s"&gt;brief&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}

Rules:
- If health &amp;lt; 8 and many mobs: summon max allies or buff resistance
- If health &amp;gt; 15 and few mobs: action none or summon 1
- Scale response to threat level
- ONLY output the JSON object, nothing else&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OllamaModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.ollama.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-oss:20b&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;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent receives context like:&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;"player"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Carlos"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"weapon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sword_of_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;"health"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"nearby_mobs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"biome"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nether_wastes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dimension"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"minecraft:the_nether"&lt;/span&gt;&lt;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;And responds:&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="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"summon"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"count"&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="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"duration"&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;"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;"Critical threat"&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;The system prompt enforces bounded actions — the AI can't break game balance because it can only choose from a fixed set of actions with capped values.&lt;/p&gt;




&lt;h2&gt;
  
  
  The CDK Stack
&lt;/h2&gt;

&lt;p&gt;The production backend is a single CDK stack:&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CloudSwordsStack&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="nb"&gt;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="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="nb"&gt;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="c1"&gt;# DynamoDB for async results
&lt;/span&gt;        &lt;span class="n"&gt;results_table&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="nc"&gt;Table&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;DecisionResults&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;partition_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;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;request_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;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;STRING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;time_to_live_attribute&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ttl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;billing_mode&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="n"&gt;BillingMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PAY_PER_REQUEST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Lambda: receive event, decide, store
&lt;/span&gt;        &lt;span class="n"&gt;ingest_fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&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="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;IngestEvent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PYTHON_3_12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;handler.lambda_handler&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lambda/ingest&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;# Lambda: poll result
&lt;/span&gt;        &lt;span class="n"&gt;poll_fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&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="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;PollResult&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PYTHON_3_12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;handler.lambda_handler&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_asset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lambda/poll&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;# API Gateway with usage plan (rate limiting = gameplay mechanic)
&lt;/span&gt;        &lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&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="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;CloudSwordsApi&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_usage_plan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Basic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;throttle&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;rate_limit&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;burst_limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;quota&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;limit&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;period&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Period&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DAY&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;The usage plan isn't just infrastructure — it's a gameplay mechanic. 100 requests per day means you can't spam the cloud. You have to be strategic about when you invoke it. Just like real API budgets.&lt;/p&gt;




&lt;h2&gt;
  
  
  Local Development: The Mock Server
&lt;/h2&gt;

&lt;p&gt;For development, I run a mock server that integrates the Strands Agent directly:&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="c1"&gt;# mock_server.py — localhost:8777
&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="k"&gt;try&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;combat_advisor&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;create_agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decide&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_agent&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;✓ Strands Agent loaded&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="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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;⚠ Agent unavailable, using rule-based fallback&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;decide_action&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;if&lt;/span&gt; &lt;span class="n"&gt;agent&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;decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&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;# Fallback: simple rules
&lt;/span&gt;    &lt;span class="k"&gt;if&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;health&lt;/span&gt;&lt;span class="sh"&gt;"&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;8&lt;/span&gt; &lt;span class="ow"&gt;and&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;nearby_mobs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&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;action&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;summon&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;count&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;Critical&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;action&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;summon&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;count&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;Standard&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;p&gt;The mock server logs every interaction to &lt;code&gt;interactions.json&lt;/code&gt; — which becomes the eval dataset for Blog 10.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cloud Concepts Taught Through Gameplay
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Game Mechanic&lt;/th&gt;
&lt;th&gt;Cloud Concept&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tome of API Key&lt;/td&gt;
&lt;td&gt;API keys + authentication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100 charges&lt;/td&gt;
&lt;td&gt;Rate limiting / usage plans&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud API Requester (8s timer)&lt;/td&gt;
&lt;td&gt;CI/CD deployment pipeline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;cloud_enabled=true&lt;/code&gt; NBT&lt;/td&gt;
&lt;td&gt;Feature flags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST /event → request_id&lt;/td&gt;
&lt;td&gt;Async event processing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Poll every 2s&lt;/td&gt;
&lt;td&gt;Eventual consistency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI decides action&lt;/td&gt;
&lt;td&gt;Serverless compute (Lambda)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bounded actions (1-5 minions)&lt;/td&gt;
&lt;td&gt;Guardrails / IAM boundaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CDK stack&lt;/td&gt;
&lt;td&gt;Infrastructure as Code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mock server&lt;/td&gt;
&lt;td&gt;Local development environment&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Threading in Minecraft is brutal&lt;/strong&gt; — anything touching entities must be on the server thread. Async HTTP + game logic = careful queue management.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rate limiting as gameplay works&lt;/strong&gt; — players naturally strategize around limited cloud invocations. It teaches resource management without a lecture.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI guardrails through system prompts&lt;/strong&gt; — bounding the action space (1-5 minions, specific buffs) means the AI literally cannot break the game. The worst it can do is summon 5 minions when 3 would suffice.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fallback patterns matter&lt;/strong&gt; — if the cloud is down, the sword still works with local logic. Graceful degradation, taught through a video game.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The AI makes decisions. But are they good decisions? In Blog 10, I build an evaluation framework that judges the agent's combat advice — using another AI as the judge. Meta? Absolutely. Useful? Surprisingly yes.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Code:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mod: &lt;a href="https://github.com/ccortezb/cloud-swords-mod" rel="noopener noreferrer"&gt;cloud-swords-mod-1.20.1&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Backend: &lt;a href="https://github.com/ccortezb/cloud-swords-backend-cdk" rel="noopener noreferrer"&gt;cloud-swords-backend-cdk&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Agent: &lt;a href="https://github.com/ccortezb/cloud-swords-strands-agent" rel="noopener noreferrer"&gt;cloud-swords-strands-agent&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;I'm Carlos Cortez — cloud engineer, gamer, and apparently someone who connects Minecraft to AI agents for fun. This is Breaking the Cloud.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.linkedin.com/in/carloscortezcloud" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://github.com/ccortezb" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://dev.to/ccortezb"&gt;Dev.to&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>minecraft</category>
      <category>aws</category>
    </item>
    <item>
      <title>I Built a Cloud Caravan Merchants Progression System as a cloud Marketplace</title>
      <dc:creator>Carlos Cortez 🇵🇪 [AWS Hero]</dc:creator>
      <pubDate>Sat, 01 Aug 2026 16:10:01 +0000</pubDate>
      <link>https://dev.to/aws-heroes/i-built-a-cloud-caravan-merchants-progression-system-as-a-cloud-marketplace-k1k</link>
      <guid>https://dev.to/aws-heroes/i-built-a-cloud-caravan-merchants-progression-system-as-a-cloud-marketplace-k1k</guid>
      <description>&lt;p&gt;Every cloud platform has a marketplace. AWS has the AWS Marketplace, Azure has its own, GCP too. They're places where you find specialized vendors offering services you can't build yourself — and the more you buy, the more they offer you.&lt;/p&gt;

&lt;p&gt;We built that in Minecraft: &lt;strong&gt;Cloud Caravan Merchants&lt;/strong&gt;. Three specialized traders that travel with donkeys carrying loot chests, offer 5 base trades each, and evolve into Elite merchants when you complete their missions — unlocking premium trades including Elytra, Tridents, and Totems of Undying.&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/YOUR_DEVTO_IMAGE_URL" 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/YOUR_DEVTO_IMAGE_URL" alt="Cloud Caravan with 3 merchants and donkeys" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎮 &lt;strong&gt;The learning angle&lt;/strong&gt;: The Elite progression system mirrors how cloud marketplace vendors work — start with basic offerings, prove you're a serious customer (complete trades), and unlock enterprise-tier products. The 3 merchant types map to cloud specializations: infrastructure (Packet Runner), platform (Protocol Broker), and application layer (Quantum Courier).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3 Merchants — 3 Cloud Layers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Merchant&lt;/th&gt;
&lt;th&gt;Color&lt;/th&gt;
&lt;th&gt;Specialty&lt;/th&gt;
&lt;th&gt;Cloud Layer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Packet Runner&lt;/td&gt;
&lt;td&gt;Cyan&lt;/td&gt;
&lt;td&gt;Raw materials &amp;amp; resources&lt;/td&gt;
&lt;td&gt;Infrastructure (IaaS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protocol Broker&lt;/td&gt;
&lt;td&gt;Orange&lt;/td&gt;
&lt;td&gt;Equipment &amp;amp; upgrades&lt;/td&gt;
&lt;td&gt;Platform (PaaS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quantum Courier&lt;/td&gt;
&lt;td&gt;Purple&lt;/td&gt;
&lt;td&gt;Runes, tomes &amp;amp; magic&lt;/td&gt;
&lt;td&gt;Application (SaaS)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each spawns with a donkey carrying a loot chest with starter items. It's a welcome package — like free tier credits when you sign up for a cloud platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Spawn Egg — Full Caravan Unit
&lt;/h2&gt;

&lt;p&gt;The custom spawn egg doesn't just spawn a merchant. It spawns the entire unit:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Merchant entity (custom type, wandering trader model)&lt;/li&gt;
&lt;li&gt;Donkey (tamed, with chest)&lt;/li&gt;
&lt;li&gt;Loot in the chest (via loot table)&lt;/li&gt;
&lt;li&gt;Leash connecting donkey to merchant
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Override&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ActionResult&lt;/span&gt; &lt;span class="nf"&gt;useOnBlock&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ItemUsageContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Spawn merchant&lt;/span&gt;
    &lt;span class="nc"&gt;CloudMerchantEntity&lt;/span&gt; &lt;span class="n"&gt;merchant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;merchantType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;merchant&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setPosition&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getX&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getY&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getZ&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;spawnEntity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merchant&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Spawn donkey with chest&lt;/span&gt;
    &lt;span class="nc"&gt;DonkeyEntity&lt;/span&gt; &lt;span class="n"&gt;donkey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EntityType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;DONKEY&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;donkey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setTame&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;donkey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setHasChest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Fill chest via NBT (inventory must be properly sized)&lt;/span&gt;
    &lt;span class="nc"&gt;NbtCompound&lt;/span&gt; &lt;span class="n"&gt;donkeyNbt&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;NbtCompound&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;donkey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeNbt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;donkeyNbt&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;donkeyNbt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;putBoolean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ChestedHorse"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;NbtList&lt;/span&gt; &lt;span class="n"&gt;itemsList&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;NbtList&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// ... add loot items to NBT&lt;/span&gt;
    &lt;span class="n"&gt;donkey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readNbt&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;donkeyNbt&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;spawnEntity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;donkey&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. Leash&lt;/span&gt;
    &lt;span class="n"&gt;donkey&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;attachLeash&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;merchant&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Donkey Chest Bug — A War Story
&lt;/h3&gt;

&lt;p&gt;Fun debugging story: donkeys in 1.20.1 have a &lt;code&gt;SimpleInventory&lt;/code&gt; field called &lt;code&gt;items&lt;/code&gt;. When you call &lt;code&gt;setHasChest(true)&lt;/code&gt;, it's supposed to expand the inventory from 2 slots (saddle + armor) to 17 slots (+ 15 chest slots). But the expansion happens in &lt;code&gt;onChestedStatusChanged()&lt;/code&gt;, which recreates the inventory.&lt;/p&gt;

&lt;p&gt;If you try to put items in the chest BEFORE the inventory is properly sized, you get &lt;code&gt;ArrayIndexOutOfBoundsException&lt;/code&gt;. If you put them AFTER spawning, the inventory might not be synced to the client yet.&lt;/p&gt;

&lt;p&gt;The solution: write the chest contents via NBT. Set &lt;code&gt;ChestedHorse: true&lt;/code&gt; and &lt;code&gt;Items: [...]&lt;/code&gt; in the NBT compound, then call &lt;code&gt;readNbt()&lt;/code&gt;. This forces the entity to reconstruct its inventory with the correct size AND the items already in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Elite Progression — The Loyalty Program
&lt;/h2&gt;

&lt;p&gt;Complete all 5 base trades (use them until sold out) and the merchant evolves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Base (5 trades) → Elite ★ (7 trades) → Elite ★★ (9 trades) → Elite ★★★ (11 trades)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each promotion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adds 2 new trades with premium items&lt;/li&gt;
&lt;li&gt;Changes the merchant's name to gold with stars&lt;/li&gt;
&lt;li&gt;Triggers a Totem of Undying particle burst&lt;/li&gt;
&lt;li&gt;Adds ambient END_ROD particles
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;checkElitePromotion&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;requiredTrades&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;eliteLevel&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;completedTrades&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;requiredTrades&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;eliteLevel&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;eliteLevel&lt;/span&gt;&lt;span class="o"&gt;++;&lt;/span&gt;
        &lt;span class="n"&gt;applyEliteName&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// "★★ Protocol Broker ★★"&lt;/span&gt;
        &lt;span class="n"&gt;addEliteTrades&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getOffers&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;eliteLevel&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Celebration particles&lt;/span&gt;
        &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;spawnParticles&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ParticleTypes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TOTEM_OF_UNDYING&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Elite Trades — Premium Vanilla Items
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cycle&lt;/th&gt;
&lt;th&gt;Packet Runner&lt;/th&gt;
&lt;th&gt;Protocol Broker&lt;/th&gt;
&lt;th&gt;Quantum Courier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;★&lt;/td&gt;
&lt;td&gt;Quantum Shards, Spyglass&lt;/td&gt;
&lt;td&gt;Netherlink Plating, Enchanted Golden Apple&lt;/td&gt;
&lt;td&gt;Deployed Runes, Ender Eyes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;★★&lt;/td&gt;
&lt;td&gt;Cloud Edge Pearls, Echo Shards&lt;/td&gt;
&lt;td&gt;Cloudsteel Plating, Trident&lt;/td&gt;
&lt;td&gt;Grimoire of Architect, XP Bottles ×16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;★★★&lt;/td&gt;
&lt;td&gt;Nether Star, Totem of Undying&lt;/td&gt;
&lt;td&gt;Netherite ×2, Elytra&lt;/td&gt;
&lt;td&gt;Quantum Runes ×16, End Crystals&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The endgame trades offer items that are normally very hard to obtain (Elytra, Trident) in exchange for Cloud Credits and rare materials. It's the cloud marketplace premium tier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Natural Spawning — The Caravan Arrives
&lt;/h2&gt;

&lt;p&gt;Every ~1 hour of real time, there's a 15% chance a caravan spawns 48-96 blocks from a random player:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;spawnCaravan&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ServerWorld&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;BlockPos&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Random&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;caravanSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;nextInt&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="c1"&gt;// 1, 2, or 3 merchants&lt;/span&gt;
    &lt;span class="c1"&gt;// Shuffle types to avoid duplicates&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MerchantType&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;available&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;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MerchantType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
    &lt;span class="nc"&gt;Collections&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;shuffle&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;available&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&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="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;caravanSize&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Spawn merchant + donkey at offset positions&lt;/span&gt;
        &lt;span class="n"&gt;spawnUnit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;available&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A full caravan (3 merchants) gives you access to all specializations at once. It's like a cloud conference — all the vendors in one place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Never Despawn — Persistent Infrastructure
&lt;/h2&gt;

&lt;p&gt;Unlike vanilla wandering traders that disappear after 40 minutes, Cloud Merchants are permanent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setPersistent&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setDespawnDelay&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Integer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;MAX_VALUE&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you find (or spawn) a merchant, they stay forever. They're your infrastructure — you invested in them, they stick around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom Textures — Visual Identity
&lt;/h2&gt;

&lt;p&gt;Each merchant uses a recolored wandering trader texture generated with Python PIL:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;recolor&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;hue_shift&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Shift hue of non-skin pixels&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;pixel&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&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="nf"&gt;is_skin_tone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pixel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rgb_to_hsv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pixel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;hue_shift&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
            &lt;span class="n"&gt;pixel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hsv_to_rgb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Packet Runner: cyan shift (networking)&lt;/li&gt;
&lt;li&gt;Protocol Broker: orange shift (fire/security)&lt;/li&gt;
&lt;li&gt;Quantum Courier: purple shift (quantum/magic)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Rune Table Recipes — Mod Interop
&lt;/h2&gt;

&lt;p&gt;We also added recipes for the Rune Crafting Altar (from the Runes mod):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rune&lt;/th&gt;
&lt;th&gt;Altar Input&lt;/th&gt;
&lt;th&gt;Output&lt;/th&gt;
&lt;th&gt;Savings&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Packet&lt;/td&gt;
&lt;td&gt;Cloud Shard + Redstone&lt;/td&gt;
&lt;td&gt;×2&lt;/td&gt;
&lt;td&gt;-1 Lapis, +1 output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protocol&lt;/td&gt;
&lt;td&gt;Packet Rune + Blazing Shard&lt;/td&gt;
&lt;td&gt;×2&lt;/td&gt;
&lt;td&gt;-1 Gold, +1 output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quantum&lt;/td&gt;
&lt;td&gt;Protocol Rune + Quantum Shard&lt;/td&gt;
&lt;td&gt;×2&lt;/td&gt;
&lt;td&gt;-1 Ender Pearl, +1 output&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is mod interop — using another mod's crafting system to provide alternative recipes. Like using a third-party service that's cheaper than building your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Challenge&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Donkey chest inventory size&lt;/td&gt;
&lt;td&gt;Write items via NBT, not direct slot access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entity renderer missing = crash&lt;/td&gt;
&lt;td&gt;Register renderer in ClientModInitializer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wandering trader despawn timer&lt;/td&gt;
&lt;td&gt;Override with MAX_VALUE despawnDelay&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spawn egg model missing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;template_spawn_egg&lt;/code&gt; parent model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trade completion tracking&lt;/td&gt;
&lt;td&gt;NBT persistence (CompletedTrades, EliteLevel)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Series Wrap-Up
&lt;/h2&gt;

&lt;p&gt;Over 8 posts, we went from "what if AWS services were Minecraft swords" to a full tech-magic mod with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;7 cloud-themed swords with unique abilities&lt;/li&gt;
&lt;li&gt;28 spells across 9 spellbooks&lt;/li&gt;
&lt;li&gt;4 armor sets with set bonuses&lt;/li&gt;
&lt;li&gt;Multiblock machines with BFS energy networks&lt;/li&gt;
&lt;li&gt;7 villager professions with a Cloud Credits economy&lt;/li&gt;
&lt;li&gt;3 Caravan Merchants with Elite progression&lt;/li&gt;
&lt;li&gt;Full compatibility with 650+ mod modpacks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mod teaches cloud computing by making you &lt;em&gt;experience&lt;/em&gt; the concepts. You don't read about Auto Scaling — you feel your damage stack. You don't study fault tolerance — you dodge attacks with Quantum Armor. You don't memorize service tiers — you progress through them.&lt;/p&gt;

&lt;p&gt;That's the power of game-based learning. And we built it all with AI as a co-pilot.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The complete mod is on &lt;a href="https://github.com/ccortezb/breakingthecloud/tree/main/minecraft/minecraft/cloud-swords-mod-1.20.1" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Clone it, build it, play it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connect with me:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/carloscortezcloud" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://x.com/ccortezb" rel="noopener noreferrer"&gt;X/Twitter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ccortezb" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/ccortezb"&gt;Dev.to&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm Carlos Cortez, this is &lt;em&gt;Breaking the Cloud&lt;/em&gt;, and this series proved that the best way to learn cloud is to play it. See you in the next adventure! 🎮☁️⚔️&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>minecraft</category>
      <category>aws</category>
    </item>
    <item>
      <title>Designing an Armor Progression That Mirrors Cloud Certification Levels</title>
      <dc:creator>Carlos Cortez 🇵🇪 [AWS Hero]</dc:creator>
      <pubDate>Sat, 01 Aug 2026 16:09:22 +0000</pubDate>
      <link>https://dev.to/aws-heroes/designing-an-armor-progression-that-mirrors-cloud-certification-levels-4k6f</link>
      <guid>https://dev.to/aws-heroes/designing-an-armor-progression-that-mirrors-cloud-certification-levels-4k6f</guid>
      <description>&lt;p&gt;In cloud computing, you level up through certifications: Practitioner → Associate → Professional → Specialty. Each level unlocks more capabilities and proves deeper expertise. We built the same thing as armor sets in Minecraft.&lt;/p&gt;

&lt;p&gt;4 armor sets, each with a set bonus, a dodge mechanic, and a clear progression path that mirrors going from cloud beginner to architect.&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/YOUR_DEVTO_IMAGE_URL" 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/YOUR_DEVTO_IMAGE_URL" alt="4 Cloud Armor sets displayed" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎮 &lt;strong&gt;The learning angle&lt;/strong&gt;: Dataflow Armor gives Speed (network optimization). Netherlink gives Fire Resistance (firewall protection). Quantum gives damage resistance (fault tolerance). Cloudsteel gives flight (cloud-native freedom). The set bonuses ARE the cloud concepts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  4 Armor Sets — 4 Cloud Maturity Levels
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Set&lt;/th&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Armor&lt;/th&gt;
&lt;th&gt;Toughness&lt;/th&gt;
&lt;th&gt;Cloud Level&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dataflow&lt;/td&gt;
&lt;td&gt;Post-Iron&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;1.5&lt;/td&gt;
&lt;td&gt;Practitioner — basic networking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netherlink&lt;/td&gt;
&lt;td&gt;Post-Diamond&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;2.5&lt;/td&gt;
&lt;td&gt;Associate — security focus&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quantum&lt;/td&gt;
&lt;td&gt;Post-Netherite&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;3.5&lt;/td&gt;
&lt;td&gt;Professional — fault tolerance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudsteel&lt;/td&gt;
&lt;td&gt;Endgame&lt;/td&gt;
&lt;td&gt;24&lt;/td&gt;
&lt;td&gt;4.0&lt;/td&gt;
&lt;td&gt;Specialty — full mastery&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each set has a unique visual identity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dataflow&lt;/strong&gt;: Cyan/teal with circuit line patterns + bright nodes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Netherlink&lt;/strong&gt;: Dark red/orange with ember glow edges&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quantum&lt;/strong&gt;: Purple with glitch pixels (digital distortion)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudsteel&lt;/strong&gt;: White/silver with cloud wisps + luminous edges&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Set Bonuses — Cloud Capabilities
&lt;/h2&gt;

&lt;p&gt;When you wear all 4 pieces of a set, you get passive effects:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Set&lt;/th&gt;
&lt;th&gt;Effects&lt;/th&gt;
&lt;th&gt;Dodge %&lt;/th&gt;
&lt;th&gt;Cloud Concept&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dataflow&lt;/td&gt;
&lt;td&gt;Speed I + Haste I&lt;/td&gt;
&lt;td&gt;10%&lt;/td&gt;
&lt;td&gt;Network optimization — faster throughput&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Netherlink&lt;/td&gt;
&lt;td&gt;Fire Resistance + Strength I (Nether)&lt;/td&gt;
&lt;td&gt;13%&lt;/td&gt;
&lt;td&gt;Firewall — immune to fire attacks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quantum&lt;/td&gt;
&lt;td&gt;Resistance I&lt;/td&gt;
&lt;td&gt;15%&lt;/td&gt;
&lt;td&gt;Fault tolerance — reduced impact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloudsteel&lt;/td&gt;
&lt;td&gt;Slow Falling + Jump Boost II&lt;/td&gt;
&lt;td&gt;20%&lt;/td&gt;
&lt;td&gt;Cloud-native — freedom of movement&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The Dodge System
&lt;/h3&gt;

&lt;p&gt;Every set has a chance to &lt;strong&gt;completely nullify incoming damage&lt;/strong&gt;. When triggered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Portal particles burst around the player&lt;/li&gt;
&lt;li&gt;Enderman teleport sound plays&lt;/li&gt;
&lt;li&gt;Damage is set to zero
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;QuantumDodgeHandler&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ALLOW_DAMAGE event — return false to cancel damage&lt;/span&gt;
        &lt;span class="nc"&gt;DamageEvents&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ALLOW&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;source&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;PlayerEntity&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

            &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;dodgeChance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getSetDodgeChance&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dodgeChance&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRandom&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;nextFloat&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;dodgeChance&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="c1"&gt;// Dodge! Cancel damage&lt;/span&gt;
                &lt;span class="n"&gt;spawnDodgeParticles&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;});&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Higher-tier armor = higher dodge chance. It's probabilistic fault tolerance — like how multi-AZ deployments reduce (but don't eliminate) the chance of downtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crafting Progression — The Certification Path
&lt;/h2&gt;

&lt;p&gt;You don't jump straight to Cloudsteel. The progression uses the Smithing Table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Iron Armor + Dataflow Plating + Cloud Shard → Dataflow Armor
Diamond Armor + Netherlink Plating + Blazing Shard → Netherlink Armor
Netherite Armor + Quantum Plating + Quantum Shard → Quantum Armor
Quantum Armor + Cloudsteel Plating + Cloud Edge Pearl → Cloudsteel Armor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each tier requires the previous tier's armor as base. You can't skip levels — just like certifications build on each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  Platings (4 ingots each)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;III
I_I  →  1 Plating
___
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple shaped recipe. 4 platings per set (helmet, chest, legs, boots) × 4 ingots = 16 ingots per full set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessories Integration — Optional Dependencies
&lt;/h2&gt;

&lt;p&gt;The mod works with or without the Accessories mod installed. When present, the Cloud Engineer Lens can be equipped in the face trinket slot instead of taking the helmet slot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AccessoriesCompat&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;isEquipped&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PlayerEntity&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Item&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Runtime check — only call if mod is loaded&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AccessoriesCapability&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;component&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEquipped&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Detection priority: Accessories slot → Trinkets slot → Helmet → Offhand → Mainhand.&lt;/p&gt;

&lt;p&gt;This is the &lt;strong&gt;optional dependency pattern&lt;/strong&gt; — your mod works standalone, but integrates with other mods when available. Like how a cloud service works alone but integrates with others via APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spell Book Slot — Cross-System Integration
&lt;/h2&gt;

&lt;p&gt;All 18 tomes (9 base + 9 empowered) implement a &lt;code&gt;SpellBookItem&lt;/code&gt; interface and register in the &lt;code&gt;trinkets:charm/spell_book&lt;/code&gt; tag. This means they can be equipped in the Spell Book accessory slot from other magic mods.&lt;/p&gt;

&lt;p&gt;Cross-mod compatibility isn't just nice to have — it's essential for modpack inclusion. Same as cloud services: if your service doesn't integrate with the ecosystem, nobody uses it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nether Star Duplication — Endgame Recipe
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4 Cloud Edge Pearls + 4 Data-Infused Quantum Shards + 1 Nether Star → 2 Nether Stars
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An endgame recipe that avoids Wither farming. Expensive inputs, but saves time. It's like Reserved Instances — pay more upfront in materials, save time in the long run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Challenge&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Set bonus detection every tick is expensive&lt;/td&gt;
&lt;td&gt;Check every 20 ticks (1 second), not every tick&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dodge feels unfair in PvP&lt;/td&gt;
&lt;td&gt;Balance with low percentages (10-20%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accessories as optional dep&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;compileOnly&lt;/code&gt; + runtime class check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Armor textures&lt;/td&gt;
&lt;td&gt;Recolor vanilla iron/netherite with unique effects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Progression too fast&lt;/td&gt;
&lt;td&gt;Require previous tier as base (sequential)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;We have swords, spells, machines, and armor. But what about NPCs that sell you things? In the final post of this series, we'll build &lt;strong&gt;Cloud Caravan Merchants&lt;/strong&gt; — wandering traders with donkeys, loot chests, and an Elite progression system. Because every cloud platform needs a marketplace.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Connect with me:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/carloscortezcloud" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://x.com/ccortezb" rel="noopener noreferrer"&gt;X/Twitter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ccortezb" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/ccortezb"&gt;Dev.to&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm Carlos Cortez, this is &lt;em&gt;Breaking the Cloud&lt;/em&gt;, and today we armored up with cloud certifications. See you in the next one! 🛡️&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>minecraft</category>
      <category>aws</category>
    </item>
    <item>
      <title>I Built a Magic System Where Every Spell is a Cloud Operation</title>
      <dc:creator>Carlos Cortez 🇵🇪 [AWS Hero]</dc:creator>
      <pubDate>Sat, 01 Aug 2026 15:13:45 +0000</pubDate>
      <link>https://dev.to/aws-heroes/i-built-a-magic-system-where-every-spell-is-a-cloud-operation-1jfd</link>
      <guid>https://dev.to/aws-heroes/i-built-a-magic-system-where-every-spell-is-a-cloud-operation-1jfd</guid>
      <description>&lt;p&gt;What does casting a spell have in common with calling an API? More than you'd think. Both consume resources (mana/runes vs. API credits), both have cooldowns (cast time vs. rate limits), both have tiers (spell levels vs. service tiers), and both produce effects at a distance.&lt;/p&gt;

&lt;p&gt;So we built &lt;strong&gt;Cloud Spellbooks&lt;/strong&gt; — 9 tomes, 28 spells, 5 rune types, and a full upgrade path. Every spell is named after a cloud operation, and every mechanic maps to how cloud services actually work.&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/YOUR_DEVTO_IMAGE_URL" 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/YOUR_DEVTO_IMAGE_URL" alt="9 Cloud Tomes arranged by tier" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🎮 &lt;strong&gt;The learning angle&lt;/strong&gt;: When you cast "Auto Scale" and it does AoE damage that scales with targets, you're experiencing what Auto Scaling &lt;em&gt;does&lt;/em&gt;. When "Region Failover" fully heals you with a 5-minute cooldown, you understand why failover is a last resort with a long recovery time. The game mechanics ARE the cloud concepts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Architecture: Spell Engine Integration
&lt;/h2&gt;

&lt;p&gt;We didn't build a spell system from scratch. We integrated with &lt;strong&gt;Spell Engine&lt;/strong&gt; — a Fabric mod that provides data-driven spell registration, the Binding Table mechanic, and compatibility with other magic mods.&lt;/p&gt;

&lt;p&gt;Our spells are defined as JSON:&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;"school"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arcane"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"range"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;6.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;"cast"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;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;"channel_ticks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;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="nl"&gt;"cost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &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_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;"cloudswords:packet_rune"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"count"&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;"cooldown"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.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;"impact"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="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;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &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;"spell_engine:damage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"damage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"spell_power_coefficient"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &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;"spell_engine:status_effect"&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_effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"effect_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;"minecraft:glowing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"duration"&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="p"&gt;}&lt;/span&gt;&lt;span class="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;This is Infrastructure as Code for magic. Define your spell's behavior declaratively, and the engine handles execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  9 Tomes, 3 Tiers — Like Service Tiers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Tomes&lt;/th&gt;
&lt;th&gt;Rune Cost&lt;/th&gt;
&lt;th&gt;Spell Power&lt;/th&gt;
&lt;th&gt;Cloud Analogy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;T1&lt;/td&gt;
&lt;td&gt;Hacker's Notebook, Codex of Uptime&lt;/td&gt;
&lt;td&gt;Packet Rune&lt;/td&gt;
&lt;td&gt;+2&lt;/td&gt;
&lt;td&gt;Free tier — basic operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T2&lt;/td&gt;
&lt;td&gt;Pipeline, SRE Codex, Cloud Captain, DevOps&lt;/td&gt;
&lt;td&gt;Protocol Rune&lt;/td&gt;
&lt;td&gt;+4&lt;/td&gt;
&lt;td&gt;Standard tier — production workloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T3&lt;/td&gt;
&lt;td&gt;Serverless Bible, Grimoire of Architect, Zero Downtime&lt;/td&gt;
&lt;td&gt;Quantum Rune&lt;/td&gt;
&lt;td&gt;+7&lt;/td&gt;
&lt;td&gt;Enterprise tier — mission-critical&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each tier requires a more expensive rune to cast. Just like cloud services — basic operations are cheap, enterprise features cost more.&lt;/p&gt;

&lt;h2&gt;
  
  
  28 Spells — Cloud Operations as Game Mechanics
&lt;/h2&gt;

&lt;p&gt;Here are some highlights showing how cloud concepts become spells:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Spell&lt;/th&gt;
&lt;th&gt;Tome&lt;/th&gt;
&lt;th&gt;Effect&lt;/th&gt;
&lt;th&gt;Cloud Concept&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ping&lt;/td&gt;
&lt;td&gt;Hacker's Notebook&lt;/td&gt;
&lt;td&gt;Damage + Glowing&lt;/td&gt;
&lt;td&gt;Network ping — reveals target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DDoS Flood&lt;/td&gt;
&lt;td&gt;Hacker's Notebook&lt;/td&gt;
&lt;td&gt;Triple rapid hit&lt;/td&gt;
&lt;td&gt;Distributed denial — overwhelming force&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Firewall Burst&lt;/td&gt;
&lt;td&gt;Codex of Uptime&lt;/td&gt;
&lt;td&gt;Resistance + Fire Res&lt;/td&gt;
&lt;td&gt;Firewall — blocks incoming&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto Scale&lt;/td&gt;
&lt;td&gt;Tome of Pipeline&lt;/td&gt;
&lt;td&gt;AoE damage radius 6&lt;/td&gt;
&lt;td&gt;Auto Scaling — handles multiple targets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Incident Response&lt;/td&gt;
&lt;td&gt;SRE Codex&lt;/td&gt;
&lt;td&gt;Regen III + Absorption&lt;/td&gt;
&lt;td&gt;Incident response — emergency healing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Load Balance&lt;/td&gt;
&lt;td&gt;Cloud Captain&lt;/td&gt;
&lt;td&gt;Strength + Speed to allies&lt;/td&gt;
&lt;td&gt;Load balancer — distributes power&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CI/CD Sprint&lt;/td&gt;
&lt;td&gt;DevOps Manifesto&lt;/td&gt;
&lt;td&gt;Speed III + Haste II&lt;/td&gt;
&lt;td&gt;CI/CD — everything faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Nuke&lt;/td&gt;
&lt;td&gt;Serverless Bible&lt;/td&gt;
&lt;td&gt;Massive AoE&lt;/td&gt;
&lt;td&gt;Nuclear option — destroy everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quantum Tunnel&lt;/td&gt;
&lt;td&gt;Grimoire of Architect&lt;/td&gt;
&lt;td&gt;Teleport 20 blocks&lt;/td&gt;
&lt;td&gt;Quantum networking — instant transfer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region Failover&lt;/td&gt;
&lt;td&gt;Zero Downtime&lt;/td&gt;
&lt;td&gt;Full heal + Res IV&lt;/td&gt;
&lt;td&gt;Failover — complete recovery, long CD&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The cooldowns tell a story too: "Ping" is 3 seconds (quick diagnostic), "Region Failover" is 5 minutes (disaster recovery is expensive and slow).&lt;/p&gt;

&lt;h2&gt;
  
  
  Rune System — API Credits
&lt;/h2&gt;

&lt;p&gt;Runes are consumed when you cast spells. They're your API credits:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rune&lt;/th&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Crafting&lt;/th&gt;
&lt;th&gt;Analogy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Packet Rune&lt;/td&gt;
&lt;td&gt;T1&lt;/td&gt;
&lt;td&gt;Cloud Shard + Redstone + Lapis&lt;/td&gt;
&lt;td&gt;Basic API call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Protocol Rune&lt;/td&gt;
&lt;td&gt;T2&lt;/td&gt;
&lt;td&gt;Packet Rune + Blazing Shard + Gold&lt;/td&gt;
&lt;td&gt;Standard API call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quantum Rune&lt;/td&gt;
&lt;td&gt;T3&lt;/td&gt;
&lt;td&gt;Protocol Rune + Quantum Shard + Ender Pearl&lt;/td&gt;
&lt;td&gt;Enterprise API call&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Higher-tier spells consume more expensive runes. You can't spam "Cloud Nuke" because Quantum Runes are expensive to craft — just like you can't spam expensive API calls without budget consequences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upgrade Paths — Service Evolution
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hacker's Notebook → Tome of the Pipeline → The Serverless Bible
Codex of Uptime → The SRE Codex → Grimoire of Zero Downtime
                → Tome of the Cloud Captain → Grimoire of the Architect
                → DevOps Manifesto (independent)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upgrades use the Smithing Table: Tome + Compiled/Deployed Rune + vanilla item. The path from T1 to T3 mirrors how cloud services evolve — you start with basic tools and upgrade to enterprise solutions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Empowered Tomes — Premium Tier
&lt;/h2&gt;

&lt;p&gt;Each tome has an "Empowered" variant (+2 extra spell power) crafted with rare materials:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;T1 Empowered: + Amethyst Shard&lt;/li&gt;
&lt;li&gt;T2 Empowered: + Blaze Rod
&lt;/li&gt;
&lt;li&gt;T3 Empowered: + Nether Star&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's like going from standard to reserved instances — same service, better performance, higher upfront cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Right-Click Casting — The UX
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SpellTome&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Item&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;TypedActionResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ItemStack&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;World&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;PlayerEntity&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Hand&lt;/span&gt; &lt;span class="n"&gt;hand&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 1. Check rune in inventory&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;hasRune&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;getRequiredRune&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendMessage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;literal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"§c✗ Missing "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;getRuneName&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;TypedActionResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="c1"&gt;// 2. Check cooldown&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isOnCooldown&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;TypedActionResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// 3. Consume rune&lt;/span&gt;
        &lt;span class="n"&gt;consumeRune&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;getRequiredRune&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="c1"&gt;// 4. Cast spell&lt;/span&gt;
        &lt;span class="n"&gt;castSpell&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// 5. Apply cooldown&lt;/span&gt;
        &lt;span class="n"&gt;applyCooldown&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;player&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;TypedActionResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stack&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple flow: verify resources → consume → execute → cooldown. It's the same pattern as any API call: authenticate → charge → process → rate limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Challenge&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;28 spells is a lot of JSON&lt;/td&gt;
&lt;td&gt;Data-driven approach — one template, many configs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Balancing cooldowns&lt;/td&gt;
&lt;td&gt;Map to real-world operation frequency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rune economy&lt;/td&gt;
&lt;td&gt;Make T1 cheap, T3 expensive — natural gating&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spell Engine compatibility&lt;/td&gt;
&lt;td&gt;Use &lt;code&gt;is_proxy: true&lt;/code&gt; for Binding Table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visual identity per tome&lt;/td&gt;
&lt;td&gt;Isometric textures with unique colors + symbols&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;Magic is great, but it needs infrastructure to support it. In the next post, we'll build a &lt;strong&gt;multiblock Data Melter&lt;/strong&gt; with BFS energy transfer through cables — because even in a fantasy world, you need power grids.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Connect with me:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/in/carloscortezcloud" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://x.com/ccortezb" rel="noopener noreferrer"&gt;X/Twitter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ccortezb" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/ccortezb"&gt;Dev.to&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm Carlos Cortez, this is &lt;em&gt;Breaking the Cloud&lt;/em&gt;, and today we cast cloud operations as spells. See you in the next one! ✨&lt;/p&gt;

</description>
      <category>programming</category>
      <category>minecraft</category>
      <category>gaming</category>
      <category>aws</category>
    </item>
    <item>
      <title>Monthly Amazon Location Service Updates - 2026.07</title>
      <dc:creator>Yasunori Kirimoto</dc:creator>
      <pubDate>Sat, 01 Aug 2026 02:10:06 +0000</pubDate>
      <link>https://dev.to/aws-heroes/monthly-amazon-location-service-updates-202607-45a3</link>
      <guid>https://dev.to/aws-heroes/monthly-amazon-location-service-updates-202607-45a3</guid>
      <description>&lt;h3&gt;
  
  
  Monthly Amazon Location Service Updates - 2026.07
&lt;/h3&gt;



&lt;p&gt;This is a summary of the July updates for Amazon Location Service.&lt;/p&gt;



&lt;h2&gt;
  
  
  2026.07 Updates
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/07/amazon-location-service-enhanced-address-search/" rel="noopener noreferrer"&gt;Amazon Location Service enhances Places APIs with new address and search options&lt;/a&gt;&lt;br&gt;
This release introduces the AddressNamesMode parameter to control how address components are returned, the AddressTranslations parameter to return place name translations in 50+ languages, and the TravelMode parameter to optimize search results for users in motion. It also adds the DriveThrough attribute indicating whether a place offers drive-through service, and a Parsing.AdditionalInfo response field in the Geocode API that details how the input address was interpreted.&lt;/p&gt;


&lt;h2&gt;
  
  
  Other Info
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://location.aws.com" rel="noopener noreferrer"&gt;Amazon Location Service Demo&lt;/a&gt;&lt;br&gt;
Official Amazon Location Service demo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/location/latest/developerguide" rel="noopener noreferrer"&gt;Amazon Location Service Developer Guide&lt;/a&gt;&lt;br&gt;
Official Amazon Location Service Documentation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/aws-geospatial" rel="noopener noreferrer"&gt;AWS Geospatial&lt;/a&gt;&lt;br&gt;
Official AWS Geospatial samples.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/mug-jp/maplibregljs-amazon-location-service-starter" rel="noopener noreferrer"&gt;maplibregljs-amazon-location-service-starter&lt;/a&gt;&lt;br&gt;
Build environment to get started with Amazon Location Service.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/dayjournal"&gt;dev.to&lt;/a&gt;&lt;br&gt;
Articles on Amazon Location Service.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://day-journal.com/memo/tags/Amazon-Location-Service" rel="noopener noreferrer"&gt;tags - Amazon Location Service&lt;/a&gt;&lt;br&gt;
&lt;a href="https://day-journal.com/memo/tags/Try" rel="noopener noreferrer"&gt;tags - Try&lt;/a&gt;&lt;br&gt;
Notes on Amazon Location Service. (Japanese)&lt;/p&gt;


&lt;h2&gt;
  
  
  Related Articles
&lt;/h2&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/aws-heroes/monthly-amazon-location-service-updates-202606-19k2" class="crayons-story__hidden-navigation-link"&gt;Monthly Amazon Location Service Updates - 2026.06&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;
          &lt;a class="crayons-logo crayons-logo--l" href="/aws-heroes"&gt;
            &lt;img alt="AWS Heroes logo" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F2491%2Ff0c1a659-c959-42cd-bb12-cd25909dd9db.png" class="crayons-logo__image" width="504" height="504"&gt;
          &lt;/a&gt;

          &lt;a href="/dayjournal" class="crayons-avatar  crayons-avatar--s absolute -right-2 -bottom-2 border-solid border-2 border-base-inverted  "&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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F723587%2F6a4ad5ac-4836-4acc-8c61-0a0e3185429f.jpg" alt="dayjournal profile" class="crayons-avatar__image" width="400" height="400"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/dayjournal" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Yasunori Kirimoto
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Yasunori Kirimoto
                
              
              &lt;div id="story-author-preview-content-4118158" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/dayjournal" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F723587%2F6a4ad5ac-4836-4acc-8c61-0a0e3185429f.jpg" class="crayons-avatar__image" alt="" width="400" height="400"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Yasunori Kirimoto&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;span&gt;
              &lt;span class="crayons-story__tertiary fw-normal"&gt; for &lt;/span&gt;&lt;a href="/aws-heroes" class="crayons-story__secondary fw-medium"&gt;AWS Heroes&lt;/a&gt;
            &lt;/span&gt;
          &lt;/div&gt;
          &lt;a href="https://dev.to/aws-heroes/monthly-amazon-location-service-updates-202606-19k2" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 11&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/aws-heroes/monthly-amazon-location-service-updates-202606-19k2" id="article-link-4118158"&gt;
          Monthly Amazon Location Service Updates - 2026.06
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/amazonlocationservice"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;amazonlocationservice&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/amazonlocationserviceupdates"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;amazonlocationserviceupdates&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/aws-heroes/monthly-amazon-location-service-updates-202606-19k2#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              &lt;span class="hidden s:inline"&gt;Add&amp;nbsp;Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            1 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;




&lt;p&gt;&lt;a href="https://spotify.link/Hz9CHCuXAXb" rel="noopener noreferrer"&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%2Fm9qrnxbzxq95nih5fefo.png" width="650" height="217"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtube.com/@norivlog_ch" rel="noopener noreferrer"&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%2Fq87a67x9yapshd534duh.png" width="650" height="217"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>amazonlocationservice</category>
      <category>amazonlocationserviceupdates</category>
    </item>
  </channel>
</rss>
