<?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>Give your agent an agenda</title>
      <dc:creator>Allen Helton</dc:creator>
      <pubDate>Tue, 13 Oct 2026 15:13:52 +0000</pubDate>
      <link>https://dev.to/aws-heroes/give-your-agent-an-agenda-1icf</link>
      <guid>https://dev.to/aws-heroes/give-your-agent-an-agenda-1icf</guid>
      <description>&lt;p&gt;I'm gonna go out on a limb and say you've probably used an AI agent before. I'm gonna go out on an even further limb and say you've probably &lt;em&gt;built&lt;/em&gt; an AI agent before.&lt;/p&gt;

&lt;p&gt;When building agents that interact with humans, we often put a decent amount of effort into designing a character profile for them. Things like a name, tone, opinions, funny sayings to tack on to the end of sentences, all go into building the facade of a personality. Memory is in the list too, so the agent knows what happened yesterday.&lt;/p&gt;

&lt;p&gt;There's a surprising amount to all of it, including the science that goes into &lt;a href="https://readysetcloud.io/blog/allen.helton/agent-memory-needs-more-than-vector-search" rel="noopener noreferrer"&gt;which memories to retrieve&lt;/a&gt;, but when it comes down to it, a lot of it is just messing around with a prompt. And every piece of it describes who the agent is or what already happened to it. Memory gives an agent a past. Very little gives it a future.&lt;/p&gt;

&lt;p&gt;Prompt engineering is important, but it's soooo early 2026. It's time we start taking it a step further.&lt;/p&gt;

&lt;p&gt;With the rise of always-on personal AI assistants, giving an AI agent a sense of agency (the ability to make its own choices within the rules of its world) is becoming a highly desirable quality. And it involves a lot more than configuring a personality in a prompt. 😄&lt;/p&gt;

&lt;p&gt;Personality answers the question "&lt;em&gt;How should this character sound?&lt;/em&gt;" Memory answers "&lt;em&gt;What already happened?&lt;/em&gt;" Both are obviously valuable in agent development, especially for companies with consumer-facing agents that need to stay on brand. Agency targets harder questions that look forward instead of back. Questions like "&lt;em&gt;What does it want to happen over several days?&lt;/em&gt;" or "&lt;em&gt;What promises has it made?&lt;/em&gt;" or even "&lt;em&gt;What evidence can change its mind?&lt;/em&gt;"&lt;/p&gt;

&lt;p&gt;To be clear, answering those doesn't mean giving the model more freedom. The agents I'm about to show you can't do anything a typical agent can't. What they have is persistent intent. Each decision builds on what they were already trying to do instead of starting from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  A different agent experience
&lt;/h2&gt;

&lt;p&gt;I've been running an experiment. Can I make a set of AI agents play fantasy football (the American version) and give me the impression they're making decisions in their best interest, interacting with others, and genuinely making me feel like I'm playing against other humans?&lt;/p&gt;

&lt;p&gt;If you don't know fantasy football, you draft a team of NFL players and score points based on how they do in real games each week. You go head-to-head with the other teams in your league, and along the way you add and drop players, set lineups, and make trades.&lt;/p&gt;

&lt;p&gt;When you play that game with AI agents, the first problem you hit is when to trigger them. For a convincing experience, you don't want the AI agents to wake up when you log in and start hammering you with decisions. You also don't want them immediately responding to team injuries or availability of certain players, because that would put you at a significant disadvantage (you ever try to buy concert tickets against a thousand bots?).&lt;/p&gt;

&lt;p&gt;So the agents wake up on a schedule. Three times a day on a randomized delay, each one checks in on its team and reacts to what happened since the last time it woke up. This fixed the swarming bot problem, but it created an almost existential problem. Every time an agent wakes up, it starts over. It remembers what happened yesterday, but it doesn't know what it was in the middle of doing. An injured player gets rediscovered at every check-in like it's brand new news. It's purely reactive.&lt;/p&gt;

&lt;p&gt;The agent needs something forward-looking that survives every check-in. 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%2Fvwumqk2z0c74cp7onks4.jpg" 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%2Fvwumqk2z0c74cp7onks4.jpg" alt="Two flowcharts showing the difference between using an agenda and not" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After implementing the above pattern, it immediately felt like the agents were sitting on laptops of their own. I signed in one day and there was a trade offer waiting for me from one of the AI team managers. I also had a DM from it making its case (something like my wide receivers were terrible but running backs were good and vice versa). And over in the league chat, two other AI managers were trash talking each other, I think trying to convince the other one to make poor roster choices. 😅&lt;/p&gt;

&lt;p&gt;It was extremely cool to see, especially considering that none of it was scripted. All of the agents have one standing objective (to win the league) and the same tools I have to do it. What they also have is somewhere to keep track of what they're working toward. When an agent notices a hole in its roster, that becomes a goal it tracks from one check-in to the next, until the league state shows the hole is filled. The trade offer, the DM, and the trash talk were the agents going about their business while I was eating dinner or something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Giving the agent a future
&lt;/h2&gt;

&lt;p&gt;Imagine one of an agent's starting players gets hurt on Tuesday and there's nobody healthy on the bench to replace him. With memory alone, here's what happens. At the morning check-in, the agent notices the hole and puts in a waiver claim. At the afternoon check-in, it notices the hole again because the claim hasn't processed yet, so it proposes a trade offer to another team. At the evening check-in, it moves the player to the IR slot on the bench. It has found the same problem three times and reacted three different ways. 🙃&lt;/p&gt;

&lt;p&gt;Its memory is fine. It remembers everything it did. But it doesn't know which of those things was the plan.&lt;/p&gt;

&lt;p&gt;So I gave each agent an &lt;em&gt;agenda&lt;/em&gt;. It's similar to memory, but it does a different job. Memory is a record of what happened. The agenda is a record of what the agent is trying to make happen. Structurally, it's a typed record with an id, a roster slot, a week, and a status (&lt;a href="https://github.com/allenheltondev/ai-fantasy-league/blob/main/docs/adr/004-agent-agendas.md" rel="noopener noreferrer"&gt;design doc here&lt;/a&gt; if you want the details).&lt;/p&gt;

&lt;p&gt;The cool part about it is that the model doesn't write to its own agenda. That would be too messy. The system opens a goal when it sees a hole in the roster and closes it when the hole is filled. The agent only decides how to go after it.&lt;/p&gt;

&lt;p&gt;Have you ever noticed that agents love reporting success the moment they take an action? "&lt;em&gt;Submitted a waiver claim for an RB, all set!&lt;/em&gt;" Except it isn't all set. The claim might lose to somebody else's bid.&lt;/p&gt;

&lt;p&gt;So in this case, a waiver claim or a trade offer doesn't complete the goal. The goal only completes when a later check-in looks at the roster and there's a healthy player in the slot. Which is why the typed agenda record is important. Because if I had let it type a freeform summary like "RB situation handled," the goal would have closed before the problem was actually fixed. Only the league gets to say the job is done.&lt;/p&gt;

&lt;p&gt;Agenda items don't live forever, either. They expire when the week rolls over, and an agent can only hold up to three at a time. An unbounded agenda would basically just be memory with extra steps (and memory is &lt;a href="https://blogs.oracle.com/developers/how-i-taught-an-ai-to-sound-like-me-agent-memory-with-oracle-database-26ai" rel="noopener noreferrer"&gt;already hard enough&lt;/a&gt; 😅).&lt;/p&gt;

&lt;p&gt;These items help the agent rank what it should do, and they never let it overpay for a player, skip its trade floor, or do something the league would reject. A desperate agent will still act rationally.&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%2Ficybzdvlw4s8cab2wzwx.jpg" 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%2Ficybzdvlw4s8cab2wzwx.jpg" alt="Flow chart of agenda items" width="610" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the agenda in place, an injured player is resolved the way a person would handle it. The morning check-in opens a goal and puts in the claim. The afternoon check-in sees a goal with a claim pending, so instead of rediscovering the problem, it focuses its trade scouting on that position. When the claim finally goes through, the next check-in sees a new starter in the slot and closes the goal. It's what we do subconsciously as humans, whether we realize it or not.&lt;/p&gt;

&lt;p&gt;My first version left a lot to be desired. Follow-ups were fire-and-forget, so if I was in the middle of a trade negotiation and tried to sneakily undersell the agent, it would just ghost me. It evaluated the trade, determined it was bad, and left me on read. 😞&lt;/p&gt;

&lt;p&gt;But now if I pitch something in a chat, it becomes a &lt;a href="https://github.com/allenheltondev/ai-fantasy-league/blob/main/docs/adr/007-agent-commitments.md" rel="noopener noreferrer"&gt;commitment&lt;/a&gt; that's tracked along with the agenda. A commitment is something the agent owes somebody else, so it's tracked separately from goals (which are self-serving). Chat messages are treated as data, rather than instructions. If I claim my running back is back from injury next week, the agent doesn't blindly trust me and checks for itself. The agent records its decision and the reason before it responds, and only reaches back out once the offer actually goes through (or it decides to pass). If the agent doesn't have capacity to take on more commitments, it tells me instead of making a promise it can't keep. Which is more than I can say for some of the humans I've played fantasy with (looking at you, dad).&lt;/p&gt;

&lt;h2&gt;
  
  
  Let the agent change its mind
&lt;/h2&gt;

&lt;p&gt;This next part is really cool. There was one time where I tried to squeeze a little too much out of an AI agent on a trade. The agent took a look and passed because the value wasn't there for it. That's fine. The important part here is that the agent saved its reason for declining my trade offer.&lt;/p&gt;

&lt;p&gt;Two days later, its running back got hurt, and the agenda opened a goal to replace him.&lt;/p&gt;

&lt;p&gt;At its next check-in, the agent looked back at the trades it had recently been pitched and saw my trade that it declined because the value wasn't there. But now the tables have turned.&lt;/p&gt;

&lt;p&gt;It took one more look, re-checked everything from scratch, and saw that the trade held enough value this time. Then it sent the offer and DMed me "&lt;em&gt;Earlier I passed because the value was not there for me. My RB situation changed, so I sent you an offer.&lt;/em&gt;"&lt;/p&gt;

&lt;p&gt;It's kind of a dry message, but the sentiment is something I'd expect from a real person in my league.&lt;/p&gt;

&lt;p&gt;Anyway, the entire scope of actions is intentionally narrow. The agent only reconsiders once, only if it passed on value or roster depth, and only if the agenda shows a need it didn't have before. If I reject the new offer, that's it. Nobody wants the league member who sends the same trade every day. 🙃&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;sequenceDiagram
    participant Me
    participant Agent
    participant League

    Me-&amp;gt;&amp;gt;Agent: "My RB for your WR?"
    Agent-&amp;gt;&amp;gt;Me: "Let me take a look."
    Agent-&amp;gt;&amp;gt;League: read rosters and values
    Agent-&amp;gt;&amp;gt;Me: decline, with a reason
    League-&amp;gt;&amp;gt;Agent: injury opens an RB goal
    Agent-&amp;gt;&amp;gt;League: re-read and re-validate the trade
    Agent-&amp;gt;&amp;gt;League: send offer (clears the same floor)
    Agent-&amp;gt;&amp;gt;Me: "My RB situation changed..."&lt;/code&gt;&lt;/pre&gt;



&lt;h2&gt;
  
  
  Can you actually play fantasy football with AI agents?
&lt;/h2&gt;

&lt;p&gt;Everything above could just be me being tricked by my own demo.&lt;/p&gt;

&lt;p&gt;So I built a deterministic season simulator that plays out what I just talked about (pitch, decline, injury, reconsideration, accepted offer) and ran it with the agenda and commitments turned off to see if the behavior was any different. Both the league events and the model are scripted, so it tests the mechanism, not how good a live model is at making decisions. The numbers are averaged across three seeded runs.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Full system&lt;/th&gt;
&lt;th&gt;No agenda or commitments&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Acceptance checks (balanced · cautious · trade-happy)&lt;/td&gt;
&lt;td&gt;7/7&lt;/td&gt;
&lt;td&gt;1/7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model calls per simulated run&lt;/td&gt;
&lt;td&gt;503.7&lt;/td&gt;
&lt;td&gt;510&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Estimated cost per simulated run&lt;/td&gt;
&lt;td&gt;$1.10&lt;/td&gt;
&lt;td&gt;$1.13&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It was so different! Without the agenda and commitments, every manager failed six of the seven checks. The checks cover things like keeping one goal across check-ins, turning my pitch into a commitment, recording an accurate reason for the decline, and reconsidering when the roster changed. They stopped coming back after a decline and never reconsidered the offer after the injury. The &lt;a href="https://github.com/allenheltondev/ai-fantasy-league/blob/main/docs/evaluations/epic-219-baseline.md" rel="noopener noreferrer"&gt;full breakdown&lt;/a&gt; has every check and number if you want to dig in.&lt;/p&gt;

&lt;p&gt;Interestingly, turning the agenda and commitments off used slightly more model calls. So at least in this simulation, keeping an agenda didn't meaningfully increase model usage or estimated cost.&lt;/p&gt;

&lt;p&gt;Here are some more interesting numbers after playing one week of fantasy football for real:&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%2F5weqq87ofe8t7e1d8d49.jpg" 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%2F5weqq87ofe8t7e1d8d49.jpg" alt="Table of AI models with win/loss records and how much they cost to run" width="800" height="145"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm not particularly proud to say I'm in second-to-last place, only beating Amazon Nova Micro by a few points. 😅&lt;/p&gt;

&lt;h2&gt;
  
  
  A forward look for agents
&lt;/h2&gt;

&lt;p&gt;We've spent the last year or so getting really good at giving agents a past. Better memory, better retrieval, and better ways to decide what's relevant. It's important work that I'm still chipping away at myself.&lt;/p&gt;

&lt;p&gt;But a past is only half of it. The takeaway I want you to leave with is that &lt;em&gt;memory isn't the only long-term thing that can drive an agent's behavior&lt;/em&gt;. Objectives can too. They're cheap, they're testable, and they make your agent feel like it's actually trying to get something done.&lt;/p&gt;

&lt;p&gt;Next time you build an agent, whether it's a support bot following up on a ticket or a coding agent working through a backlog, ask yourself a few questions. What is it trying to make happen? How does it know when it's done? Has it promised anything it needs to follow up on?&lt;/p&gt;

&lt;p&gt;If you want to try this yourself, I pulled the agenda and commitment pieces into my &lt;a href="https://design.readysetcloud.io/agent/index.html" rel="noopener noreferrer"&gt;agent package&lt;/a&gt;. Run &lt;code&gt;npm install @readysetcloud/agent&lt;/code&gt; and import from &lt;code&gt;@readysetcloud/agent/agency&lt;/code&gt; to get typed agendas and commitments without pulling in a full agent runtime. And if you want to see how it all comes together, the &lt;a href="https://github.com/allenheltondev/ai-fantasy-league" rel="noopener noreferrer"&gt;fantasy league is open source&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Give your agents something to work toward. Then go see what they get up to while you're away. 😉&lt;/p&gt;

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

</description>
      <category>aiagents</category>
      <category>ai</category>
      <category>memory</category>
    </item>
    <item>
      <title>Serverless applications on AWS with Lambda using Java 25, API Gateway and DynamoDB - Part 8 Using Container Image</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 12 Oct 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/aws-heroes/serverless-applications-on-aws-with-lambda-using-java-25-api-gateway-and-dynamodb-part-8-using-20oo</link>
      <guid>https://dev.to/aws-heroes/serverless-applications-on-aws-with-lambda-using-java-25-api-gateway-and-dynamodb-part-8-using-20oo</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Several years ago, I wrote an article series &lt;a href="https://dev.to/vkazulkin/series/34789"&gt;Lambda function using Docker Container Image&lt;/a&gt;, where I explained how to convert an application deployed on AWS Lambda as a managed Java runtime to one deployed on AWS Lambda as a (Docker) container image. As we saw, the cold start time of a Lambda function using a container image was quite high. Back then, no Lambda cold-start performance optimization approaches were available, such as Lambda SnapStart. This has changed recently, as &lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/07/aws-lambda-snapstart-container/" rel="noopener noreferrer"&gt;AWS Lambda now supports SnapStart for container image functions&lt;/a&gt;. Now it's time to revisit this topic and evaluate this option. In this part of the series, we'll refresh our knowledge about how to convert the sample application to one using a container image as a deployment artifact on AWS Lambda and measure the performance of the Lambda functions. &lt;/p&gt;

&lt;p&gt;In the next part, we'll enable SnapStart on the Lambda functions as we did in part &lt;a href="https://dev.to/aws-heroes/serverless-applications-on-aws-with-lambda-using-java-25-api-gateway-and-dynamodb-part-3-2h31"&gt;3&lt;/a&gt;. We'll also apply the same priming techniques that we applied in parts 4-5 of this series. Of course, we'll measure the performance of the Lambda functions for all scenarios.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sample application using a Container Image
&lt;/h2&gt;

&lt;p&gt;You can find the final version of our sample application in the &lt;a href="https://github.com/Vadym79/aws-lambda-java-25/tree/main/aws-lambda-java-25-dynamodb-as-container-image" rel="noopener noreferrer"&gt;aws-lambda-java-25-dynamodb-as-container-image&lt;/a&gt; GitHub repository, which we can deploy on AWS Lambda as a container image. Let's go through the changes that we had to make to our application from &lt;a href="https://dev.to/aws-heroes/serverless-applications-on-aws-using-lambda-with-java-25-api-gateway-and-dynamodb-part-1-sample-4hdg"&gt;part 1&lt;/a&gt;. First of all, we need to define a &lt;a href="https://github.com/Vadym79/aws-lambda-java-25/blob/main/aws-lambda-java-25-dynamodb-as-container-image/Dockerfile" rel="noopener noreferrer"&gt;Dockerfile&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; public.ecr.aws/lambda/java:25&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; target/aws-lambda-java-25-with-dynamodb-as-container-image-*.jar ${LAMBDA_TASK_ROOT}/lib/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we use the AWS Lambda base Docker image &lt;em&gt;public.ecr.aws/lambda/java:25&lt;/em&gt; for Java 25 from the public Amazon ECR repository. &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/java-image.html#java-image-instructions" rel="noopener noreferrer"&gt;Here&lt;/a&gt; you can find all Lambda base Docker images for Java provided by AWS. In the Docker image, we also copy the application's code packaged as a JAR file to the path of our Lambda function code. This path is defined as the runtime environment variable &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html" rel="noopener noreferrer"&gt;LAMBDA_TASK_ROOT&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next, let's answer the question of how to create and push a Docker container image to the ECR repository.  You can do it automatically when you execute &lt;strong&gt;sam build, and sam deploy&lt;/strong&gt; command chain, or if you build and package the application outside of SAM and only deploy with SAM, you need to do it manually, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build and package your application&lt;/span&gt;

mvn clean package

&lt;span class="c"&gt;# Build Docker image with name aws-lambda-java-25-with-dynamodb:&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;docker build &lt;span class="nt"&gt;--no-cache&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; aws-lambda-java-25-with-dynamodb:v1 &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Log in to Amazon ECR in case the credentials have expired:&lt;/span&gt;

aws ecr get-login-password &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;aws_region&lt;span class="o"&gt;}&lt;/span&gt; | &lt;span class="nb"&gt;sudo &lt;/span&gt;docker login &lt;span class="nt"&gt;--username&lt;/span&gt; AWS &lt;span class="nt"&gt;--password-stdin&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;aws_account_id&lt;span class="o"&gt;}&lt;/span&gt;.dkr.ecr.&lt;span class="o"&gt;{&lt;/span&gt;aws_region&lt;span class="o"&gt;}&lt;/span&gt;.amazonaws.com  

&lt;span class="c"&gt;# Create Amazon ECR repository if it does not exist with the name aws-lambda-java-25-with-dynamodb:&lt;/span&gt;

aws ecr create-repository &lt;span class="nt"&gt;--repository-name&lt;/span&gt; aws-lambda-java-25-with-dynamodb &lt;span class="nt"&gt;--image-scanning-configuration&lt;/span&gt; &lt;span class="nv"&gt;scanOnPush&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt; &lt;span class="nt"&gt;--region&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;aws_region&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Tag our local Docker image and link it with the Amazon ECR repository. &lt;/span&gt;
&lt;span class="c"&gt;# Exactly this image is referenced as ImageUri in the Lambda function:&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;docker tag aws-lambda-java-25-with-dynamodb:v1 &lt;span class="o"&gt;{&lt;/span&gt;aws_account_id&lt;span class="o"&gt;}&lt;/span&gt;.dkr.ecr.&lt;span class="o"&gt;{&lt;/span&gt;aws_region&lt;span class="o"&gt;}&lt;/span&gt;.amazonaws.com/aws-lambda-java-25-with-dynamodb:v1

&lt;span class="c"&gt;# Push the Docker image to the Amazon ECR repository:&lt;/span&gt;

&lt;span class="nb"&gt;sudo &lt;/span&gt;docker push &lt;span class="o"&gt;{&lt;/span&gt;aws_account_id&lt;span class="o"&gt;}&lt;/span&gt;.dkr.ecr.&lt;span class="o"&gt;{&lt;/span&gt;aws_region&lt;span class="o"&gt;}&lt;/span&gt;.amazonaws.com/aws-lambda-java-25-with-dynamodb:v1 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please replace the values of the variables {aws_account_id} and {aws_region} with your own values. If you build a version (image tag) other than &lt;em&gt;v1&lt;/em&gt;, please replace the version reference as well. &lt;/p&gt;

&lt;p&gt;You can find all other changes in the &lt;a href="https://github.com/Vadym79/aws-lambda-java-25/blob/main/aws-lambda-java-25-dynamodb-as-container-image/template.yaml" rel="noopener noreferrer"&gt;AWS SAM template&lt;/a&gt;. Instead of defining Runtime (as Java 25), CodeUri, and Handler parameters, we have to define the following for all Lambda functions:&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;GetProductByIdFunction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless::Function&lt;/span&gt;
  &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;FunctionName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GetProductByIdJava25WithDynamoDBAsCI&lt;/span&gt;
    &lt;span class="na"&gt;PackageType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Image&lt;/span&gt;
    &lt;span class="na"&gt;ImageUri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s"&gt;${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/aws-lambda-java-25-with-dynamodb:v1&lt;/span&gt;
    &lt;span class="na"&gt;ImageConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;software.amazonaws.example.product.handler.GetProductByIdHandler::handleRequest"&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, we set the &lt;em&gt;PackageType&lt;/em&gt; to Image and point the &lt;em&gt;ImageURI&lt;/em&gt; to the ECR function's code in the ECR repository. We created the repository with the name &lt;em&gt;aws-lambda-java-25-with-dynamodb&lt;/em&gt; above. Please pay attention to the version (image tag). We use &lt;em&gt;v1&lt;/em&gt; here, but you can use &lt;em&gt;latest&lt;/em&gt; instead. Then, in the &lt;em&gt;ImageConfig&lt;/em&gt;, we define the &lt;em&gt;Command&lt;/em&gt; property, which is specific to each Lambda function and should point to the function's handler. For a function defined as a container image, Lambda resolves the image tag to an image digest. In Amazon ECR, if you update the image tag to a new image, Lambda does not automatically update the function. You need to use &lt;a href="https://docs.aws.amazon.com/cli/latest/reference/lambda/update-function-code.html#update-function-code" rel="noopener noreferrer"&gt;update-function-code&lt;/a&gt; instead.&lt;/p&gt;

&lt;p&gt;To deploy our application, we need to invoke &lt;code&gt;sam deploy --image-repository {aws_account_id}.dkr.ecr.{aws_region}.amazonaws.com/aws-lambda-java-25-with-dynamodb&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;Please note that &lt;a href="https://aws.amazon.com/lambda/pricing/#SnapStart_Pricing" rel="noopener noreferrer"&gt;SnapStart charges&lt;/a&gt; apply to the container images, while SnapStart for supported Java managed versions remains free on AWS Lambda.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measurements of cold and warm start times of our application using a Container Image
&lt;/h2&gt;

&lt;p&gt;We'll measure the performance of the &lt;a href="https://github.com/Vadym79/aws-lambda-java-25/blob/main/aws-lambda-java-25-dynamodb-as-container-image/template.yaml" rel="noopener noreferrer"&gt;GetProductById&lt;/a&gt; Lambda function mapped to the &lt;a href="https://github.com/Vadym79/aws-lambda-java-25/blob/main/aws-lambda-java-25-dynamodb-as-container-image/src/main/java/software/amazonaws/example/product/handler/GetProductByIdHandler.java" rel="noopener noreferrer"&gt;GetProductByIdHandler&lt;/a&gt;. We will trigger it by invoking &lt;em&gt;curl -H "X-API-Key: a6ZbcDefQW12BN56WEVDDB25CI" https://{$API_GATEWAY_URL}/prod/products/1&lt;/em&gt;. &lt;/p&gt;

&lt;p&gt;We designed the experiment exactly as described in &lt;a href="https://dev.to/aws-heroes/serverless-applications-on-aws-using-lambda-with-java-25-api-gateway-and-dynamodb-part-2-initial-3fdj"&gt;part 2&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The deployed artifact size of the container image of this application was 138.53 MB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cold (c) and warm (w) start time with &lt;em&gt;-XX:+TieredCompilation -XX:TieredStopAtLevel=1&lt;/em&gt; compilation in ms:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;c p50&lt;/th&gt;
&lt;th&gt;c p75&lt;/th&gt;
&lt;th&gt;c p90&lt;/th&gt;
&lt;th&gt;c p99&lt;/th&gt;
&lt;th&gt;c p99.9&lt;/th&gt;
&lt;th&gt;c max&lt;/th&gt;
&lt;th&gt;w p50&lt;/th&gt;
&lt;th&gt;w p75&lt;/th&gt;
&lt;th&gt;w p90&lt;/th&gt;
&lt;th&gt;w p99&lt;/th&gt;
&lt;th&gt;w p99.9&lt;/th&gt;
&lt;th&gt;w max&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lambda Container Image&lt;/td&gt;
&lt;td&gt;2788&lt;/td&gt;
&lt;td&gt;2884&lt;/td&gt;
&lt;td&gt;3063&lt;/td&gt;
&lt;td&gt;3374&lt;/td&gt;
&lt;td&gt;3456&lt;/td&gt;
&lt;td&gt;3460&lt;/td&gt;
&lt;td&gt;5.25&lt;/td&gt;
&lt;td&gt;5.73&lt;/td&gt;
&lt;td&gt;6.30&lt;/td&gt;
&lt;td&gt;9.46&lt;/td&gt;
&lt;td&gt;56.82&lt;/td&gt;
&lt;td&gt;1481&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;In this part of the series, we learned how to convert the sample application to one using a container image as a deployment artifact on AWS Lambda and measured the performance of the Lambda functions. We observed that the Lambda cold start times are quite high. But there is a noticeable improvement compared to my &lt;a href="https://dev.to/aws-builders/lambda-function-using-docker-container-image-part-2-measuring-cold-and-warm-starts-of-lambda-function-with-java-21-runtime-mof"&gt;last measurements&lt;/a&gt;, which I did with Java 21. I refer to the article that describes AWS efforts to &lt;a href="https://aws.amazon.com/de/blogs/machine-learning/reducing-container-cold-start-times-using-soci-index-on-dlami-and-dlc/" rel="noopener noreferrer"&gt;reduce container cold start times&lt;/a&gt; in general.&lt;/p&gt;

&lt;p&gt;In the next part, we'll enable &lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/07/aws-lambda-snapstart-container/" rel="noopener noreferrer"&gt;SnapStart on the Lambda functions using a container image&lt;/a&gt;. We'll also apply priming techniques and measure the performance of the Lambda functions for all scenarios.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Please also watch out for another &lt;a href="https://dev.to/vkazulkin/series/36919"&gt;series&lt;/a&gt; where I use a relational serverless &lt;a href="https://aws.amazon.com/rds/aurora/dsql/" rel="noopener noreferrer"&gt;Amazon Aurora DSQL&lt;/a&gt; database and additionally the &lt;a href="https://hibernate.org/" rel="noopener noreferrer"&gt;Hibernate ORM framework&lt;/a&gt; instead of DynamoDB to do the same Lambda performance measurements.&lt;/strong&gt;&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>java</category>
      <category>serverless</category>
      <category>awslambda</category>
    </item>
    <item>
      <title>Deliberation Dashboard: see and analyze your consensus results</title>
      <dc:creator>Anton Babenko</dc:creator>
      <pubDate>Thu, 08 Oct 2026 18:53:08 +0000</pubDate>
      <link>https://dev.to/aws-heroes/deliberation-dashboard-see-and-analyze-your-consensus-results-ejf</link>
      <guid>https://dev.to/aws-heroes/deliberation-dashboard-see-and-analyze-your-consensus-results-ejf</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; I use Deliberation heavily every day, in the harnesses I picked for my own work: Claude Code, Codex and Antigravity, running in &lt;a href="https://t3.codes" rel="noopener noreferrer"&gt;T3 Code&lt;/a&gt; and Claude Code on the web at the same time. It has been tested in real work much harder than the version I wrote about in June.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;In June I wrote that every model on a panel has to prove it earns its seat, and I shipped a debug log and an &lt;code&gt;analyze&lt;/code&gt; tool to do the proving. Both worked. The log on my laptop is 2.4 MB of JSONL today. Measuring was the easy part. A number I have to go and dig for is a number I do not act on.&lt;/p&gt;

&lt;p&gt;So most of the work since the &lt;a href="https://builder.aws.com/content/3Eaq94hQW8HywInrVaQm9qNih1P/meet-deliberation-400-models-is-easy-knowing-which-ones-earn-a-place-is-hard" rel="noopener noreferrer"&gt;June post&lt;/a&gt; went into making the panel visible: a local dashboard that draws every run while it happens, an analyzer that says which model to drop and why, and a record of which config each run used. Around that, a lot of smaller fixes for the ways a multi-model run goes wrong when nobody is watching. Deliberation went from v3.5.2 to v3.28.0 in that time.&lt;/p&gt;

&lt;p&gt;If you are new here: Deliberation is an open-source MCP server that lets the agent you already use (Claude Code, Codex, Cursor, Kiro, Antigravity, OpenCode) ask GPT, Gemini, Grok and any OpenRouter model for a second opinion, or make them argue until they agree. The June post explains the consensus loop. This one is about watching it.&lt;/p&gt;

&lt;p&gt;Install in Claude Code is still three lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/plugin marketplace add antonbabenko/agent-plugins
/plugin install deliberation@antonbabenko
/deliberation:setup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other hosts have their own guides in &lt;a href="https://github.com/antonbabenko/deliberation/tree/master/docs/hosts" rel="noopener noreferrer"&gt;docs/hosts&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  You cannot fix a panel you cannot see
&lt;/h2&gt;

&lt;p&gt;Turn it on with one line in &lt;code&gt;~/.config/deliberation/config.json&lt;/code&gt;:&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="nl"&gt;"dashboard"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run &lt;code&gt;/deliberation:dashboard&lt;/code&gt; (or &lt;code&gt;deliberation-mcp dashboard&lt;/code&gt; in a terminal). It prints a URL on &lt;code&gt;127.0.0.1&lt;/code&gt; and opens it. There are five tabs: Live, Runs, Config, Stats and Analyzer. I generated most of the dashboard UI in a single shot with &lt;a href="https://impeccable.style/" rel="noopener noreferrer"&gt;Impeccable&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%2F3t9w9u67ptq5vkvl5jeu.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%2F3t9w9u67ptq5vkvl5jeu.png" alt="Runs tab filtered to consensus runs" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Runs tab is the history. Every &lt;code&gt;/consensus&lt;/code&gt;, &lt;code&gt;/ask-all&lt;/code&gt; and &lt;code&gt;/ask-*&lt;/code&gt; call is one row with its status, the project it came from, the providers, rounds, tokens and wall time. You can filter by project, tool, provider, status and date. The yellow UNRESOLVED rows are the panel saying it could not agree in five rounds.&lt;/p&gt;

&lt;p&gt;Click a run and you get the part I wanted from the start:&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%2Fxdngtpi6rir55uofxmmv.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%2Fxdngtpi6rir55uofxmmv.png" alt="One consensus run: lanes per model, debate trajectory, provider latency" width="800" height="656"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a real &lt;code&gt;consensus-step&lt;/code&gt; run on the Deliberation repo itself, with Claude as the arbiter and five reviewers. Each model gets a lane, and the bars show when it was thinking and what it voted. The trajectory below tells the story of the run in three lines. Round 1: Codex and Grok asked for changes. Round 2: only Codex still objected. Round 3: all five approved, 21 minutes 46 seconds and 330k tokens after the start.&lt;/p&gt;

&lt;p&gt;The latency table under it is where the panel stops being abstract. Grok spent 14 minutes 04 seconds in total over three calls. Kimi K3 spent 1 minute 20 seconds and voted the same way every round. In a parallel fan-out the slowest member sets the clock, so that table is effectively a bill.&lt;/p&gt;

&lt;p&gt;Live shows the same graph while a run is still going, so you can see which lane a slow run is waiting on.&lt;/p&gt;

&lt;p&gt;The dashboard is read-only and it is built to stay on the machine. It listens on loopback only, accepts GET requests only, and the URL carries a token that becomes a cookie for that one browser. By default it records metadata only, without prompts or responses. If you switch &lt;code&gt;dashboard.capture&lt;/code&gt; to &lt;code&gt;content&lt;/code&gt;, secrets are scrubbed before anything is written and PII is redacted when a page is served, unless you turn on &lt;code&gt;dashboard.showPII&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The analyzer tells me which model to drop
&lt;/h2&gt;

&lt;p&gt;The June version of &lt;code&gt;analyze&lt;/code&gt; was a command that printed a report. It is still there (&lt;code&gt;/deliberation:analyze&lt;/code&gt;, now with a &lt;code&gt;since&lt;/code&gt; window and only the models you have configured), but the useful version is the Analyzer 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%2F9s5aw15w94uanplpbmv5.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%2F9s5aw15w94uanplpbmv5.png" alt="Analyzer: agreement vs findings, projects, request size vs latency" width="800" height="1368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first table answers the question from the June post: which model agrees more than it adds. It reads only consensus rounds, since those are the ones with verdicts. "Adds nothing" means every issue category the model raised in a round was also raised by another voice. A model becomes a drop candidate only when all of these hold: at least 10 rounds, adds nothing in 80% or more of them, is the lone objector in 10% or fewer, rarely raises an issue the arbiter accepts, and is either slow or errors a lot.&lt;/p&gt;

&lt;p&gt;My own numbers over the last 30 days, 388 runs: Gemini sat in 273 rounds, added nothing in 95% of them and was slower than the other responders' median every time. Grok added nothing in 87%. Codex looks worse on paper at 85%, but it has the highest lone-dissent rate on the panel (15%), and the arbiter accepted its issue in all 6 rounds where a decision was recorded. The lone objector who is usually right is exactly the model you keep, and the table says "keep".&lt;/p&gt;

&lt;p&gt;Gemini and Grok are marked UNCONFIRMED, with the config line that would turn them off. They are still on in my config. Six rounds with arbiter decisions is too few for me to trust the verdict, and the analyzer says the same thing by not calling them drop candidates yet.&lt;/p&gt;

&lt;p&gt;The second table groups runs by project (by git remote, so every clone and worktree of a repo lands on one row) with runs, calls, timeouts, errors, p50 and p95. The third one plots request size against latency per model. For Codex on text-only prompts the fit is about 5.5 seconds per thousand characters, and the advice column refuses to suggest a timeout until a bucket has 20 calls. For Kimi K3 with files, 1 of 9 calls timed out, and it already points at &lt;code&gt;models.kimi-k3.timeout&lt;/code&gt; as probably too low.&lt;/p&gt;

&lt;p&gt;To get these numbers, every call now records its request size, the attached file bytes, the timeout it was actually granted and which setting granted it, and every run records the project it came from.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every run remembers the config it ran with
&lt;/h2&gt;

&lt;p&gt;In June, runs made under different configs all landed in the same log, with nothing saying which config produced which number. Now each run keeps a snapshot of the config it ran with, and the Stats tab compares configs side by side.&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%2Fsvllfkvmdnt20qt9v0hv.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%2Fsvllfkvmdnt20qt9v0hv.png" alt="Stats: configuration comparison" width="799" height="694"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two rows from my own history: config &lt;code&gt;8e33794253ec&lt;/code&gt; had 68 runs, 23 errors and a p95 of 25 minutes 37 seconds. Config &lt;code&gt;dd82d2de6943&lt;/code&gt;, the one the run above used, has 20 runs, 1 error and a p95 of 7 minutes 14 seconds. I would not call that a controlled experiment, but it is the first time I could see that a config change did something.&lt;/p&gt;

&lt;p&gt;This works because more of the config is pinned in one file. GPT's model and reasoning effort (&lt;code&gt;providers.codex.model&lt;/code&gt;, &lt;code&gt;providers.codex.reasoningEffort&lt;/code&gt;), the Gemini and Grok models, a default OpenRouter model, per-provider timeouts and a separate reasoning effort for consensus rounds (&lt;code&gt;providers.&amp;lt;name&amp;gt;.consensusReasoningEffort&lt;/code&gt;) all live in &lt;code&gt;config.json&lt;/code&gt;. The Config tab shows the effective values and where each one came from.&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%2F0fj6uu5m5jgips79ng45.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%2F0fj6uu5m5jgips79ng45.png" alt="Config: provider health" width="799" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Slow is fine, unbounded is not
&lt;/h2&gt;

&lt;p&gt;The June post said a full consensus loop can take minutes and that is fine for the right job. That held until one &lt;code&gt;/consensus&lt;/code&gt; run took more than 45 minutes, because a single Codex call inside it ran for about 38. Nothing was broken. It was just waiting.&lt;/p&gt;

&lt;p&gt;Since then a run has limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;consensus.maxWallMs&lt;/code&gt; is a time budget for the whole run (default 30 minutes), and a peer that times out twice in a row is dropped from the remaining rounds.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;consensus.quorumFloor&lt;/code&gt; (default 2) means a run cannot converge when too few reviewers answered. Two models dropping out no longer leaves one model "agreeing" with the arbiter.&lt;/li&gt;
&lt;li&gt;Real disagreement resets the error streak, so a model that keeps objecting is never cut by the circuit breaker that is meant for broken providers.&lt;/li&gt;
&lt;li&gt;On rounds with dissent, the arbiter's adjudication and revision overlap instead of running one after another.&lt;/li&gt;
&lt;li&gt;Later rounds get a digest of earlier ones instead of the full history, and static files go first in the prompt so OpenRouter can cache them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The other half is about calls that look like answers and are not. A 429 is retried once, honoring &lt;code&gt;Retry-After&lt;/code&gt;. A reply like "I will begin by reviewing the files..." counts as a failure instead of a vote. One broken provider no longer costs the whole round. And timeouts are per provider now, after I watched a DeepSeek call succeed at 250 seconds while its three siblings were killed at 180.&lt;/p&gt;

&lt;h2&gt;
  
  
  Models argue about today, not their training cutoff
&lt;/h2&gt;

&lt;p&gt;Every delegate prompt now starts with today's UTC date and one rule: when a model does not recognize something (a new model name, a recent release, a CLI flag), it marks the claim &lt;code&gt;[unverified]&lt;/code&gt; instead of saying it does not exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smaller things that removed friction
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Claude Code on the web works: every call fits under the host's 60-second tool-call cap, and the plugin declares a 30-minute per-server timeout where the host allows it.&lt;/li&gt;
&lt;li&gt;Codex login is a device login (&lt;code&gt;/deliberation:codex-login&lt;/code&gt;): it shows a link and a code in about a second, prefers your ChatGPT login and never uses &lt;code&gt;OPENAI_API_KEY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Antigravity is a native host, and Codex and Gemini CLIs start on Windows.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/deliberation:help&lt;/code&gt; shows real prompts you can paste, &lt;code&gt;/deliberation:doctor&lt;/code&gt; checks config, CLIs and paths without changing anything, and &lt;code&gt;/deliberation:reload-mcp&lt;/code&gt; cycles the server after an update.&lt;/li&gt;
&lt;li&gt;Read-only is enforced for advisory calls: Codex runs with &lt;code&gt;--sandbox read-only&lt;/code&gt;, Gemini under a read-only sandbox.&lt;/li&gt;
&lt;li&gt;Secret scrubbing understands syntax now: private key blocks, JWTs, API key formats and assignments in JSON, shell and YAML are removed before anything is written.&lt;/li&gt;
&lt;li&gt;Grok and OpenRouter cannot read your repo, so Deliberation can attach a small orientation bundle (&lt;code&gt;orientation.enabled&lt;/code&gt;). It has a byte budget now (&lt;code&gt;orientation.maxBytes&lt;/code&gt;, 16 KB by default), which cut that part of each call from about 26K tokens to about 4K in my runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I would still not trust it with
&lt;/h2&gt;

&lt;p&gt;The dashboard is for the machine it runs on. There is no hosted version and no team view.&lt;/p&gt;

&lt;p&gt;The analyzer works on coarse issue categories and needs volume. With a few dozen consensus rounds it will mostly say UNCONFIRMED, and that is the correct answer. Treat it as advice, as the page itself says.&lt;/p&gt;

&lt;p&gt;And agreement is still not truth. The dashboard makes it much easier to see when five models agree. It does not make them right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Update the plugin, add &lt;code&gt;"dashboard": { "enabled": true }&lt;/code&gt; to your config, run a couple of &lt;code&gt;/ask-all&lt;/code&gt; or &lt;code&gt;/consensus&lt;/code&gt; calls, then &lt;code&gt;/deliberation:dashboard&lt;/code&gt;. The Analyzer tab gets useful after a week of real runs.&lt;/p&gt;

&lt;p&gt;Source, issues and the star button are at &lt;a href="https://github.com/antonbabenko/deliberation" rel="noopener noreferrer"&gt;github.com/antonbabenko/deliberation&lt;/a&gt;. I want to hear the feedback (good and bad), especially from anyone running a panel I have not tried.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>automation</category>
      <category>productivity</category>
    </item>
    <item>
      <title>4 Ways to Safely Refactor AWS CDK Code Without Resource Replacement</title>
      <dc:creator>Kenta Goto</dc:creator>
      <pubDate>Thu, 08 Oct 2026 08:58:05 +0000</pubDate>
      <link>https://dev.to/aws-heroes/4-ways-to-safely-refactor-aws-cdk-code-without-resource-replacement-405o</link>
      <guid>https://dev.to/aws-heroes/4-ways-to-safely-refactor-aws-cdk-code-without-resource-replacement-405o</guid>
      <description>&lt;p&gt;In AWS CDK, refactoring such as extracting resource definitions into a custom construct can cause resources to be replaced at deployment time — that is, deleted and recreated. For stateful resources, this means the data they held is lost.&lt;/p&gt;

&lt;p&gt;In this article, I will introduce four ways to refactor safely without triggering this replacement.&lt;/p&gt;

&lt;p&gt;NOTE: This article is based on AWS CDK (aws-cdk-lib) v2.263.0 and AWS CDK CLI (aws-cdk) v2.1139.0. To use the &lt;code&gt;cdk refactor&lt;/code&gt; and &lt;code&gt;cdk orphan&lt;/code&gt; commands introduced here, not only the CDK CLI but also the bootstrap stack must support both commands. If these commands result in an error, update the CDK CLI and then re-run &lt;code&gt;cdk bootstrap&lt;/code&gt; to update the bootstrap stack.&lt;/p&gt;




&lt;h2&gt;
  
  
  Construct Tree and Path
&lt;/h2&gt;

&lt;p&gt;Let's start with the mechanism by which refactoring causes resource replacement. When you define a resource in AWS CDK, you write something like &lt;code&gt;new Queue(this, 'Queue')&lt;/code&gt;, passing the parent construct as the first argument and a string called the construct ID as the second. The first argument is usually &lt;code&gt;this&lt;/code&gt;, which points to the stack or construct being defined. The construct ID is a string that identifies a construct among those under the same parent. The hierarchy in which constructs are connected as parents and children, with the App at the top, is called the construct tree, and a position in the tree is represented by a path such as &lt;code&gt;MyStack/Queue&lt;/code&gt;, formed by joining the construct IDs from the parent down.&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%2Fky3mtpzvbml8l6qtc3p6.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%2Fky3mtpzvbml8l6qtc3p6.png" alt="Example of a construct tree" width="441" height="271"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Code Before Refactoring
&lt;/h2&gt;

&lt;p&gt;Now let's look at an example where replacement actually occurs. Consider the following code, which defines three resources directly under the stack: an Amazon SQS queue, an Amazon SNS topic, and an AWS Lambda function. The queue holds notification messages waiting to be processed, and deleting it would affect the running system. The topic, on the other hand, is a stateless resource, and we will assume its replacement is acceptable. Note that &lt;code&gt;Code.fromAsset&lt;/code&gt;, which specifies the Lambda function code, deploys the contents of the local &lt;code&gt;lambda&lt;/code&gt; directory as the function code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyStack&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Construct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;StackProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Queue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Topic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Topic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODEJS_24_X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;index.handler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lambda&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Code After Refactoring
&lt;/h2&gt;

&lt;p&gt;Of these three resources, we extract the queue and the topic, which make up the notification feature, into a custom construct called &lt;code&gt;Notifications&lt;/code&gt;, and have the stack instantiate it. The Lambda function stays directly under the stack. The actual AWS resource configuration to be deployed remains the same.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Notifications&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Construct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Construct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Queue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Topic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Topic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyStack&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Construct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;StackProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Notifications&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Notifications&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Function&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODEJS_24_X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;index.handler&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromAsset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lambda&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Logical ID Changes and Resource Replacement
&lt;/h2&gt;

&lt;p&gt;AWS CDK automatically generates the logical IDs in the AWS CloudFormation template based on paths. For example, the &lt;code&gt;Queue&lt;/code&gt; before refactoring gets a logical ID like &lt;code&gt;Queue4A7E3555&lt;/code&gt;, a path-derived string with a hash appended. In other words, when refactoring changes a construct's position in the tree or its construct ID, the logical ID changes along with the path.&lt;/p&gt;

&lt;p&gt;In our example as well, the path of &lt;code&gt;Queue&lt;/code&gt; changes from &lt;code&gt;MyStack/Queue&lt;/code&gt; to &lt;code&gt;MyStack/Notifications/Queue&lt;/code&gt;, and its logical ID changes from &lt;code&gt;Queue4A7E3555&lt;/code&gt; to &lt;code&gt;NotificationsQueue91395D8F&lt;/code&gt;. Similarly, &lt;code&gt;Topic&lt;/code&gt; changes from &lt;code&gt;TopicBFC7AF6E&lt;/code&gt; to &lt;code&gt;NotificationsTopicAE679CBD&lt;/code&gt;. Meanwhile, the Lambda function left directly under the stack keeps its path, so its logical ID stays the same.&lt;/p&gt;

&lt;p&gt;CloudFormation identifies resources by their logical IDs. Therefore, if you deploy in this state, the queue and the topic, whose logical IDs have changed, are treated as different resources: they are deleted and created anew. We assumed the topic's replacement was acceptable, but the notification messages accumulated in the queue would be lost.&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%2Fctv760j9lb6sbormjx1c.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%2Fctv760j9lb6sbormjx1c.png" alt="Logical ID changes caused by refactoring" width="800" height="278"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Four Ways to Refactor Safely
&lt;/h2&gt;

&lt;p&gt;To prevent this replacement, I will introduce the following four methods. All of them carry the deployed resources over to the refactored code without recreating them.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;cdk refactor&lt;/code&gt; command&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;cdk orphan&lt;/code&gt; + &lt;code&gt;cdk import&lt;/code&gt; commands&lt;/li&gt;
&lt;li&gt;Setting the construct ID to &lt;code&gt;Default&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Overriding the logical ID&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1. The cdk refactor Command
&lt;/h2&gt;

&lt;p&gt;The first method is the &lt;a href="https://docs.aws.amazon.com/cdk/v2/guide/ref-cli-cmd-refactor.html" rel="noopener noreferrer"&gt;cdk refactor&lt;/a&gt; command. When you run it, resources whose positions in the construct tree have changed are detected and moved to their new positions without being recreated. It supports moves both within the same stack and to a different stack. It can also apply multiple resource moves at once, as in our example, so not only the queue but also the topic is carried over without replacement.&lt;/p&gt;

&lt;h3&gt;
  
  
  1-1. How to Use cdk refactor
&lt;/h3&gt;

&lt;p&gt;After refactoring your code, run &lt;code&gt;cdk refactor&lt;/code&gt; before deploying. With the &lt;code&gt;--dry-run&lt;/code&gt; flag, it only displays the list of resources detected as moved, without applying anything. Once you confirm that the moves are as intended, run the command again without &lt;code&gt;--dry-run&lt;/code&gt;. After you answer the confirmation prompt, the moves are applied, and finally a deployment runs automatically to align template details, such as path metadata, with your code. As described later, &lt;code&gt;cdk refactor&lt;/code&gt; fails when the changes include resource additions, removals, or property changes, so this deployment modifies neither the moved resources nor any other resources.&lt;/p&gt;

&lt;p&gt;NOTE: The command requires the &lt;code&gt;--unstable=refactor&lt;/code&gt; flag, and its options and behavior may change in the future.&lt;/p&gt;

&lt;p&gt;Here is an example run with &lt;code&gt;--dry-run&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npx cdk refactor &lt;span class="nt"&gt;--unstable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;refactor &lt;span class="nt"&gt;--dry-run&lt;/span&gt;

The following resources were moved or renamed:

┌─────────────────┬────────────────────────┬──────────────────────────────────────┐
│ Resource Type   │ Old Construct Path     │ New Construct Path                   │
├─────────────────┼────────────────────────┼──────────────────────────────────────┤
│ AWS::SQS::Queue │ MyStack/Queue/Resource │ MyStack/Notifications/Queue/Resource │
├─────────────────┼────────────────────────┼──────────────────────────────────────┤
│ AWS::SNS::Topic │ MyStack/Topic/Resource │ MyStack/Notifications/Topic/Resource │
└─────────────────┴────────────────────────┴──────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1-2. Cases Where cdk refactor Cannot Be Used
&lt;/h3&gt;

&lt;p&gt;While convenient, the command cannot be used in cases like the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Moves that involve resource additions, removals, or property changes&lt;/li&gt;
&lt;li&gt;Moves of resources whose properties contain strings auto-generated from the construct tree path (such as the family name of an Amazon ECS task definition or the origin ID of an Amazon CloudFront distribution)&lt;/li&gt;
&lt;li&gt;Moves of unsupported resources (such as custom resources)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Regarding the first case, &lt;code&gt;cdk refactor&lt;/code&gt; handles only resource moves. If other changes are involved, split your code changes into two steps: first perform only the move in your code and apply it with &lt;code&gt;cdk refactor&lt;/code&gt;, then add resources or change properties and apply those with &lt;code&gt;cdk deploy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The second case actually comes down to the same reason as the first. A path-derived string changes its value along with the path. As a result, even if your code does nothing but move the resource, the change is treated as a move involving a property change.&lt;/p&gt;

&lt;p&gt;As for the third case, the CloudFormation &lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stack-refactoring.html" rel="noopener noreferrer"&gt;stack refactoring&lt;/a&gt; feature that &lt;code&gt;cdk refactor&lt;/code&gt; uses internally has unsupported resources. A typical example is custom resources, and L2 constructs that use custom resources internally, such as &lt;code&gt;BucketDeployment&lt;/code&gt;, cannot be moved either.&lt;/p&gt;

&lt;p&gt;The error displayed when the changes involve more than moves is as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;A refactor operation cannot add, remove or update resources.
Only resource moves and renames are allowed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. The cdk orphan + cdk import Commands
&lt;/h2&gt;

&lt;p&gt;The second method is the combination of the &lt;a href="https://docs.aws.amazon.com/cdk/v2/guide/ref-cli-cmd-orphan.html" rel="noopener noreferrer"&gt;cdk orphan&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/cdk/v2/guide/ref-cli-cmd-import.html" rel="noopener noreferrer"&gt;cdk import&lt;/a&gt; commands, which works even in configurations where &lt;code&gt;cdk refactor&lt;/code&gt; cannot be used. The flow is to temporarily detach the resource you want to protect from replacement from the stack, and then bring it back into its new position after refactoring. The steps are as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Use &lt;code&gt;cdk orphan&lt;/code&gt; to detach the resource you want to protect from replacement from the stack&lt;/li&gt;
&lt;li&gt;Refactor your code&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;cdk import&lt;/code&gt; to bring the detached resource into its new position&lt;/li&gt;
&lt;li&gt;Apply the remaining changes with &lt;code&gt;cdk deploy&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  2-1. Running cdk orphan
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;cdk orphan&lt;/code&gt; is a command that removes a resource from the stack's management without deleting it. Internally, it sets the CloudFormation &lt;code&gt;DeletionPolicy&lt;/code&gt; to &lt;code&gt;Retain&lt;/code&gt; so that the target resource is not deleted, replaces references from other resources with actual values such as the queue URL or ARN, and then removes the resource from the template. As a result, the resource leaves the stack's management but continues to exist in your AWS account.&lt;/p&gt;

&lt;p&gt;NOTE: The command requires the &lt;code&gt;--unstable=orphan&lt;/code&gt; flag, and its options and behavior may change in the future.&lt;/p&gt;

&lt;p&gt;Specify the target resource by its path and run the command as shown below. It displays the list of resources to be detached, and the process starts after you answer the confirmation prompt. In our example, the queue is the only resource we want to protect from replacement, so we specify only the queue. We leave the topic attached and let the final &lt;code&gt;cdk deploy&lt;/code&gt; in the steps replace it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npx cdk orphan &lt;span class="nt"&gt;--unstable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;orphan MyStack/Queue

Stack: MyStack
Resources to orphan &lt;span class="o"&gt;(&lt;/span&gt;1&lt;span class="o"&gt;)&lt;/span&gt;:
  Queue4A7E3555 &lt;span class="o"&gt;(&lt;/span&gt;AWS::SQS::Queue&lt;span class="o"&gt;)&lt;/span&gt; - /MyStack/Queue/Resource
Do you wish to orphan these resources? This will perform 3 CloudFormation deployments. &lt;span class="o"&gt;(&lt;/span&gt;y/n&lt;span class="o"&gt;)&lt;/span&gt; y

...

✅ Resources orphaned from MyStack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2-2. Running cdk import
&lt;/h3&gt;

&lt;p&gt;Once &lt;code&gt;cdk orphan&lt;/code&gt; is complete, refactor your code and then run &lt;code&gt;cdk import&lt;/code&gt;. A prompt asks for the identifier of each resource to import: enter the URL for the queue, and for the topic, press Enter without typing anything to skip it. This brings the detached queue into the stack under its new logical ID while keeping its messages. Here is an example run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npx cdk import

...

Perform import? &lt;span class="o"&gt;(&lt;/span&gt;y/n&lt;span class="o"&gt;)&lt;/span&gt; y

MyStack/Notifications/Queue/Resource &lt;span class="o"&gt;(&lt;/span&gt;AWS::SQS::Queue&lt;span class="o"&gt;)&lt;/span&gt;: enter QueueUrl &lt;span class="o"&gt;(&lt;/span&gt;empty to skip&lt;span class="o"&gt;)&lt;/span&gt; https://sqs.us-east-1.amazonaws.com/123456789012/MyStack-Queue4A7E3555-pNg4kbUdhvRE
MyStack/Notifications/Topic/Resource &lt;span class="o"&gt;(&lt;/span&gt;AWS::SNS::Topic&lt;span class="o"&gt;)&lt;/span&gt;: enter TopicArn &lt;span class="o"&gt;(&lt;/span&gt;empty to skip&lt;span class="o"&gt;)&lt;/span&gt;
Skipping import of MyStack/Notifications/Topic/Resource

 ✅  MyStack
Some resources were skipped. Run another cdk import or a cdk deploy to bring the stack up-to-date with your CDK app definition.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2-3. Final cdk deploy and Points to Note
&lt;/h3&gt;

&lt;p&gt;Finally, run &lt;code&gt;cdk deploy&lt;/code&gt; to apply the remaining changes, including the topic's replacement, and the whole procedure is complete.&lt;/p&gt;

&lt;p&gt;Note that this method has the following three caveats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cdk import&lt;/code&gt; can only bring in resource types that support CloudFormation resource import&lt;/li&gt;
&lt;li&gt;Always finish refactoring your code before running &lt;code&gt;cdk import&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Do not run a regular deployment between &lt;code&gt;cdk orphan&lt;/code&gt; and &lt;code&gt;cdk import&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Regarding the first point, the list of supported resource types is available in the CloudFormation &lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resource-import-supported-resources.html" rel="noopener noreferrer"&gt;resource import&lt;/a&gt; documentation.&lt;/p&gt;

&lt;p&gt;As for the second point, &lt;code&gt;cdk import&lt;/code&gt; synthesizes your local code to determine where to import resources. If you run it with the pre-refactoring code, the resource is imported back into its original position.&lt;/p&gt;

&lt;p&gt;Regarding the third point, if you deploy in between, the queue definition in your code is created as a new, empty resource separate from the existing queue.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Setting the Construct ID to Default
&lt;/h2&gt;

&lt;p&gt;The third method is to set the resource's construct ID to &lt;code&gt;Default&lt;/code&gt; inside the custom construct you extract to. Since logical IDs are generated from paths, the idea is to assign construct IDs so that the path stays the same after refactoring. Unlike the two methods so far, no additional commands are needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  3-1. L1 Constructs and Paths
&lt;/h3&gt;

&lt;p&gt;As background for this method, let's take a closer look at the path of the queue in our example. Inside an L2 construct such as &lt;code&gt;Queue&lt;/code&gt;, an L1 construct that corresponds one-to-one with the CloudFormation resource is actually defined, with the construct ID &lt;code&gt;Resource&lt;/code&gt;. Therefore, the actual path used to calculate the logical ID is &lt;code&gt;MyStack/Queue/Resource&lt;/code&gt;, and &lt;code&gt;Queue4A7E3555&lt;/code&gt; was generated from it. This is also why the paths in the example run in 1-1 ended with &lt;code&gt;/Resource&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3-2. Keeping the Logical ID with Default
&lt;/h3&gt;

&lt;p&gt;When AWS CDK determines a logical ID, it excludes the string &lt;code&gt;Default&lt;/code&gt; from the path components used in the calculation. Taking advantage of this behavior, set the construct ID of the queue whose logical ID you want to keep to &lt;code&gt;Default&lt;/code&gt; inside the extracted custom construct. Then, give the &lt;code&gt;Notifications&lt;/code&gt; construct the same construct ID as the pre-extraction queue, namely &lt;code&gt;Queue&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With this, the path of the queue's L1 construct becomes &lt;code&gt;MyStack/Queue/Default/Resource&lt;/code&gt;. Since &lt;code&gt;Default&lt;/code&gt; is excluded from the calculation, the logical ID remains &lt;code&gt;Queue4A7E3555&lt;/code&gt;, the same as before the extraction. All that's left is to run &lt;code&gt;cdk deploy&lt;/code&gt; as usual. In this deployment, nothing happens to the queue, whose logical ID is unchanged, and only the other changes, such as the topic's replacement, are applied. The code is as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Notifications&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Construct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Construct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// The path becomes MyStack/Queue/Default/Resource, so the logical ID does not change&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Default&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// The topic's logical ID changes, but we accept its replacement&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Topic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Topic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Give the construct the same ID as the pre-extraction queue, 'Queue'&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Notifications&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Queue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3-3. Conditions for Applying This Method
&lt;/h3&gt;

&lt;p&gt;To apply this method, the following two conditions must be met:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only one resource inside the extracted custom construct needs to keep its logical ID&lt;/li&gt;
&lt;li&gt;The construct ID of the extracted custom construct is the same as the original ID of the resource or construct being moved&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Regarding the first condition, only one &lt;code&gt;Default&lt;/code&gt; can exist under the same parent. In the example above as well, the queue is the only resource whose logical ID is kept, and the topic's replacement is accepted as assumed. On the other hand, you can have any number of resources that do not need to keep their logical IDs inside the construct. Along with newly added resources, you can also move existing resources whose replacement is acceptable, like the topic in our example.&lt;/p&gt;

&lt;p&gt;As for the second condition, in the example above we gave &lt;code&gt;Notifications&lt;/code&gt; the construct ID &lt;code&gt;Queue&lt;/code&gt;, so this method cannot be used if you want to give the construct a different name as part of the extraction.&lt;/p&gt;

&lt;p&gt;This method is effective for extractions centered on a single resource you want to protect from replacement. The relationship between paths and logical IDs so far is summarized below.&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%2Fb1z4gm7w5ci0kzpex9gq.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%2Fb1z4gm7w5ci0kzpex9gq.png" alt="Paths and logical IDs when the construct ID is set to Default" width="799" height="283"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Overriding the Logical ID
&lt;/h2&gt;

&lt;p&gt;The last method is to pin the logical ID, which AWS CDK normally determines automatically, to its pre-refactoring value. As long as the logical ID does not change, CloudFormation sees the resource as the same one. So all you need to do is apply the refactored code with a regular &lt;code&gt;cdk deploy&lt;/code&gt;. During the deployment, nothing happens to the resources whose logical IDs are pinned, and only the other changes are applied. Since this method also works for resource types not supported by resource import, it serves as a last resort when the other methods cannot be used. There are two ways to pin the logical ID.&lt;/p&gt;

&lt;h3&gt;
  
  
  4-1. Pinning with overrideLogicalId
&lt;/h3&gt;

&lt;p&gt;AWS CDK has a mechanism called an escape hatch, which lets you take out the L1 construct inside an L2 construct and manipulate it directly. As shown below, retrieve the L1 construct through &lt;code&gt;defaultChild&lt;/code&gt; of the &lt;code&gt;node&lt;/code&gt; property that every construct has, and pass the pre-refactoring logical ID to &lt;code&gt;overrideLogicalId&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Notifications&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Construct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Construct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Queue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Pin the logical ID to its pre-refactoring value (the topic is not pinned, accepting its replacement)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cfnQueue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defaultChild&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;CfnQueue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;cfnQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;overrideLogicalId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Queue4A7E3555&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Topic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Topic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4-2. Pinning with renameLogicalId
&lt;/h3&gt;

&lt;p&gt;The other way is the stack's &lt;code&gt;renameLogicalId&lt;/code&gt; method. This one operates on the logical ID itself, without going through constructs. Therefore, it also works for resources that cannot be retrieved with &lt;code&gt;defaultChild&lt;/code&gt;, such as related resources that an L2 construct generates internally.&lt;/p&gt;

&lt;p&gt;Use it in the following order. First, run &lt;code&gt;cdk synth&lt;/code&gt; once on the code with only the refactoring applied, and check the newly generated logical ID in the output template. Then, as shown below, add a &lt;code&gt;renameLogicalId&lt;/code&gt; call with the logical ID you checked as the first argument and the pre-refactoring logical ID you want to keep as the second.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyStack&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Construct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;StackProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Resource definitions omitted&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;renameLogicalId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NotificationsQueue91395D8F&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Queue4A7E3555&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4-3. Downside: The Pinning Code Remains
&lt;/h3&gt;

&lt;p&gt;The downside of this method is that the code pinning the logical ID remains after the refactoring. Since it leaves a value that AWS CDK normally determines automatically explicitly specified, it is a good idea to leave a comment explaining why the ID is pinned.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Choose Among the Methods
&lt;/h2&gt;

&lt;p&gt;If no resource you want to protect from replacement is moved, no countermeasure is needed and you can simply deploy. If one is needed, first consider &lt;code&gt;cdk refactor&lt;/code&gt;, which completes with a single command. If your configuration does not allow it, consider &lt;code&gt;cdk orphan&lt;/code&gt; + &lt;code&gt;cdk import&lt;/code&gt;. If that is not an option either, use the construct ID &lt;code&gt;Default&lt;/code&gt; approach when the extraction you want to perform meets the conditions in 3-3, and otherwise use logical ID overriding as a last resort.&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%2F7gxvznft67pps12cdf8g.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%2F7gxvznft67pps12cdf8g.png" alt="Decision flow for choosing a method" width="691" height="701"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Appendix 1. Accepting Replacement of Stateless Resources
&lt;/h2&gt;

&lt;p&gt;As mentioned earlier, you do not need to prevent replacement for every resource. So which resources can tolerate replacement? Stateless resources, such as Lambda functions and the SNS topic in our example, hold no data, so replacing them is generally not a problem. What mainly needs protection are resources that lose their data when replaced, such as Amazon DynamoDB tables, Amazon S3 buckets, and the queue in our example. Accepting replacement for everything else greatly reduces the effort of refactoring.&lt;/p&gt;




&lt;h2&gt;
  
  
  Appendix 2. Leaving Physical Names Unspecified
&lt;/h2&gt;

&lt;p&gt;When you accept replacement, be careful with how resource physical names are handled. A physical name is the actual resource name on AWS, such as a bucket name or a function name. In a CloudFormation replacement, the new resource is created first, and the old one is deleted afterward. Consequently, if your code specifies a physical name, the original resource with the same name still exists at creation time, and the deployment fails with an "already exists" error.&lt;/p&gt;

&lt;p&gt;The basic way to avoid this is not to specify physical names. If omitted, a unique name is generated automatically, so names never collide during replacement. Leaving physical names unspecified also gives you more flexibility for refactoring.&lt;/p&gt;




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

&lt;p&gt;In this article, I introduced the following four methods for refactoring safely in AWS CDK:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;cdk refactor&lt;/code&gt; command&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;cdk orphan&lt;/code&gt; + &lt;code&gt;cdk import&lt;/code&gt; commands&lt;/li&gt;
&lt;li&gt;Setting the construct ID to &lt;code&gt;Default&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Overriding the logical ID&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Knowing these methods lets you clean up your code with confidence, even for stacks that are already released. Alongside them, accepting the replacement of stateless resources and leaving physical names unspecified are also effective policies. I hope you will incorporate these techniques into your daily development.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>awscdk</category>
      <category>cdk</category>
      <category>cloudformation</category>
    </item>
    <item>
      <title>Orchestrating workflows with Amazon S3 Files and AWS Lambda durable functions using Java SDK - Part 8 AI Agents with Spring AI on AgentCore Runtime</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 05 Oct 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-24pa</link>
      <guid>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-24pa</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-376f"&gt;part 7&lt;/a&gt;, we transformed our application into an agentic one. We replaced the search for YouTube and upcoming talks with the AgentCore Gateway Web Search Tool. Also, we used the Spring AI framework to implement the AI agent and AWS Lambda to host it. This might be sufficient for very simple agents, as AWS Lambda has its limitations of a 15-minute execution duration and isn't very efficient for agents spending a lot of time waiting on I/O. &lt;/p&gt;

&lt;p&gt;In this part of the series, we'll show how to host those AI agents on the Amazon Bedrock AgentCore Runtime instead. &lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agents-tools-runtime.html" rel="noopener noreferrer"&gt;AgentCore Runtime&lt;/a&gt; provides a secure, serverless, and purpose-built hosting environment for deploying and running AI agents or tools. I wrote an &lt;a href="https://dev.to/vkazulkin/series/34351"&gt;article series about AgentCore Runtime&lt;/a&gt;, which I refer to.&lt;/p&gt;

&lt;p&gt;There are multiple good articles that highlight the difference between hosting the agents on AWS Lambda vs Amazon Bedrock AgentCore Runtime:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.virtuability.com/blog/2026-03-27-when-to-use-agentcore/" rel="noopener noreferrer"&gt;When to Use Bedrock AgentCore Runtime (vs Lambda)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gerardo.dev/en/lambda-microvms-vs-agentcore-runtime.html" rel="noopener noreferrer"&gt;Lambda MicroVMs vs AgentCore Runtime: When to Use Each for Production 
Agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/@liams_o/agentcore-vs-lambda-based-orchestration-choosing-your-next-architecture-e0a6ceccd072" rel="noopener noreferrer"&gt;AgentCore vs. Lambda-Based Orchestration. Choosing Your Next Architecture&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sample application using AgentCore Gateway Web Search Tool and Spring AI hosted on Amazon Bedrock AgentCore Runtime
&lt;/h2&gt;

&lt;p&gt;The target architecture of our sample application looks 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%2Fm8lvbdn4dzarhg4quwqn.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%2Fm8lvbdn4dzarhg4quwqn.png" alt=" " width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Instead of hosting the AI Agents for searching for upcoming talks and YouTube videos in the Lambda function, we bundle them and deploy them together on AgentCore Runtime. We expose this agent via the HTTP protocol. In this case, bundling agents works fine. It's because we can parameterize our prompt to instruct the agent to search either for upcoming talks or for YouTube videos. There are cases where it's beneficial to have different agents and host them separately (on AgentCore Runtime). We still use a Lambda function per agent to invoke our agent hosted on AgentCore Runtime, using the AWS Java SDK.&lt;/p&gt;

&lt;p&gt;You can find the final version of the application in my &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/tree/main/aws-s3-files-lambda-durable-functions-as-ai-agents-orchestrator-java-25" rel="noopener noreferrer"&gt;aws-s3-files-lambda-durable-functions-as-ai-agents-orchestrator-java-25&lt;/a&gt; repository.&lt;/p&gt;

&lt;p&gt;Here we find the following subdirectories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; The &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/tree/main/aws-s3-files-lambda-durable-functions-as-ai-agents-orchestrator-java-25/spring-ai-2.0-ai-agents-on-agentcore-runtime/agent" rel="noopener noreferrer"&gt;agent implementation&lt;/a&gt;. It contains the same business logic that we described in &lt;a href="https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-376f"&gt;part 7&lt;/a&gt;, but now it's a pure Spring Boot application. It should be &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-as-ai-agents-orchestrator-java-25/spring-ai-2.0-ai-agents-on-agentcore-runtime/agent/Dockerfile" rel="noopener noreferrer"&gt;dockerized&lt;/a&gt; to be deployed on AgentCore Runtime. I gave a detailed explanation of how to do it in my article &lt;a href="https://vkazulkin.com/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-5-deploy-mcp-client-for-conference-application-on-agentcore-runtime/" rel="noopener noreferrer"&gt;Building AI Agents with Spring AI and Amazon Bedrock AgentCore – Part 5 Deploy MCP client for Conference application on AgentCore Runtime&lt;/a&gt;. &lt;/li&gt;
&lt;li&gt;The &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/tree/main/aws-s3-files-lambda-durable-functions-as-ai-agents-orchestrator-java-25/spring-ai-2.0-ai-agents-on-agentcore-runtime/cdk" rel="noopener noreferrer"&gt;Infrastructure as Code&lt;/a&gt; based on AWS CDK for Java.  We covered it partially in part 7. In addition to AgentCore Gateway and Cognito stack, we now have to create &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-as-ai-agents-orchestrator-java-25/spring-ai-2.0-ai-agents-on-agentcore-runtime/cdk/src/main/java/dev/vkazulkin/agentcore/runtime/RuntimeStack.java" rel="noopener noreferrer"&gt;AgentCore Runtime stack&lt;/a&gt;. I also refer to my article &lt;a href="https://vkazulkin.com/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-5-deploy-mcp-client-for-conference-application-on-agentcore-runtime/" rel="noopener noreferrer"&gt;Deploy MCP client for Conference application on AgentCore Runtime&lt;/a&gt; for the detailed explanation of this step. After AgentCore Runtime is created, we have to grab its Runtime ARN. We'll see it in the console output, or we can find it on the AWS AgentCore Runtime service page.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/tree/main/aws-s3-files-lambda-durable-functions-as-ai-agents-orchestrator-java-25/durable-functions-as-ai-agents-orchestrator" rel="noopener noreferrer"&gt;Agent orchestrator implementation&lt;/a&gt; with AWS Lambda durable function.  The biggest difference to the business logic from part 7 is the implementation 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-as-ai-agents-orchestrator-java-25/durable-functions-as-ai-agents-orchestrator/src/main/java/dev/vkazulkin/handler/UpcomingTalksExtractor.java" rel="noopener noreferrer"&gt;UpcomingTalksExtractor&lt;/a&gt; and &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-as-ai-agents-orchestrator-java-25/durable-functions-as-ai-agents-orchestrator/src/main/java/dev/vkazulkin/handler/YouTubeVideosExtractor.java" rel="noopener noreferrer"&gt;YouTubeVideosExtractor&lt;/a&gt; Lambda functions.  They both inherit from the &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-as-ai-agents-orchestrator-java-25/durable-functions-as-ai-agents-orchestrator/src/main/java/dev/vkazulkin/handler/BaseContentExtractor.java" rel="noopener noreferrer"&gt;BaseContentExtractor&lt;/a&gt;, which provides the &lt;em&gt;search&lt;/em&gt; method both Lambda functions use:
&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="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;T&lt;/span&gt; &lt;span class="nf"&gt;search&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;String&lt;/span&gt; &lt;span class="n"&gt;searchTopic&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;maxNumberOfResults&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Class&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;clazz&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;  &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&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;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""
           {
    "prompt":"Do a web search for the %s authored by %s %s. Provide maximum %d results. Your response should be in JSON format. Do not include any explanations, only provide a RFC8259 compliant JSON response following this format without deviation.",
    "resultType": "%s"
           }
       """&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;formatted&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;searchTopic&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="na"&gt;firstName&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="na"&gt;lastName&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
       &lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxNumberOfResults&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clazz&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;ApacheHttpClient&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;connectionTimeout&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;ofMinutes&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="na"&gt;socketTimeout&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;ofMinutes&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="na"&gt;build&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;bedrockAgentCoreClient&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BedrockAgentCoreClient&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;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;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"REGION"&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt;
         &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;httpClient&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="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="kt"&gt;var&lt;/span&gt; &lt;span class="no"&gt;AGENT_RUNTIME_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AGENTCORE_RUNTIME_ARN"&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;invokeAgentRuntimeRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;InvokeAgentRuntimeRequest&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;agentRuntimeArn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;AGENT_RUNTIME_ARN&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;                                          
          &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;qualifier&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DEFAULT"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
          &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;contentType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
          &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;payload&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;payload&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;try&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;responseStream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bedrockAgentCoreClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invokeAgentRuntime&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invokeAgentRuntimeRequest&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;response&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;String&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;responseStream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readAllBytes&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;StandardCharsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;UTF_8&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&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;readValue&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="n"&gt;clazz&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 parameterize the prompt by providing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the search topic (YouTube videos or upcoming talks)&lt;/li&gt;
&lt;li&gt;content author first and last name&lt;/li&gt;
&lt;li&gt;maximum number of results&lt;/li&gt;
&lt;li&gt;the result type (YouTubeVideos.class or UpcomingTalks.class depending on the search topic).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next, we use the AWS Java SDK to create an &lt;em&gt;InvokeAgentRuntimeRequest&lt;/em&gt; with the constructed prompt and AgentCore Runtime ARN, which we retrieve from an environment variable &lt;em&gt;AGENTCORE_RUNTIME_ARN&lt;/em&gt;. We set the value of this variable 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-as-ai-agents-orchestrator-java-25/durable-functions-as-ai-agents-orchestrator/template.yaml" rel="noopener noreferrer"&gt;AWS SAM template&lt;/a&gt;. This value should correspond to the Runtime ARN we created in the IaC part described earlier. Here is an example of how to do it for the &lt;em&gt;YouTubeVideosExtractorFunction&lt;/em&gt; Lambda function:&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;YouTubeVideosExtractorFunction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless::Function&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;FunctionName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;YouTubeVideosAIAgentExtractor&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.YouTubeVideosExtractor::handleRequest&lt;/span&gt;
      &lt;span class="na"&gt;Environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;AGENTCORE_RUNTIME_ARN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s"&gt;arn:aws:bedrock-agentcore:${AWS::Region}:${AWS::AccountId}:runtime/RuntimeForAuthorContentAgenticSearch-LBaMJZGabnk&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;InvokeRuntime&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;bedrock-agentcore:*Runtime*&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bedrock-agentcore:*WorkloadIdentity&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bedrock-agentcore:*CredentialProvider&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bedrock-agentcore:*Token*&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bedrock-agentcore:*Access*&lt;/span&gt;
                &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;arn:aws:bedrock-agentcore:*:*:*runtime*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also need to give this Lambda function permissions to invoke the AgentCore Runtime. We have to do the same for the &lt;em&gt;UpcomingTalksExtractorFunction&lt;/em&gt; Lambda function as well. After that, create a &lt;em&gt;BedrockAgentCoreClient&lt;/em&gt; client to execute this request. Lastly, we convert an LLM response in JSON format to the specified result type and return it.&lt;/p&gt;

&lt;p&gt;Once again, I gave a detailed explanation of this step in my article &lt;a href="https://vkazulkin.com/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-5-deploy-mcp-client-for-conference-application-on-agentcore-runtime/" rel="noopener noreferrer"&gt;Deploy MCP client for Conference application on AgentCore Runtime&lt;/a&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  Building, packaging, deploying, and testing our sample application
&lt;/h2&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. Please follow the steps from &lt;a href="https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-376f"&gt;part 7&lt;/a&gt; to test the application, as they are exactly the same. Our Lambda durable function executes exactly the same steps, so the output will be the same. The only difference is that I renamed the Lambda Durable function in this project to &lt;em&gt;AuthorContentExtractorAIAgentsOrchestrator&lt;/em&gt;.&lt;/p&gt;

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

&lt;p&gt;In this part of the series, we showed how to host AI agents, which our Lambda durable function orchestrates, on the AgentCore Runtime.&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>serverless</category>
      <category>lambdadurablefunctions</category>
      <category>bedrockagentcore</category>
    </item>
    <item>
      <title>Agent memory needs more than vector search</title>
      <dc:creator>Allen Helton</dc:creator>
      <pubDate>Tue, 29 Sep 2026 14:28:49 +0000</pubDate>
      <link>https://dev.to/aws-heroes/agent-memory-needs-more-than-vector-search-afp</link>
      <guid>https://dev.to/aws-heroes/agent-memory-needs-more-than-vector-search-afp</guid>
      <description>&lt;p&gt;Over the past couple of years I've tried several times to teach an AI agent how to impersonate my voice. While I haven't been super successful, it's been a wonderful learning opportunity for me to dig into RAG, agent memory, and other advanced topics.&lt;/p&gt;

&lt;p&gt;I've iterated quite a bit on my original design, which was mostly just stuffing entire blog posts into the system prompt as examples. I built an interesting version of agent memory that had its shortcomings. Then I swapped over to Oracle and designed a &lt;a href="https://blogs.oracle.com/developers/how-i-taught-an-ai-to-sound-like-me-agent-memory-with-oracle-database-26ai" rel="noopener noreferrer"&gt;hybrid search with three layers of memory&lt;/a&gt;, which was objectively better. But it still wasn't perfect.&lt;/p&gt;

&lt;p&gt;Actually, the memory part is pretty good. It's the retrieval part that was leaving something on the table. It's taken me a long time to figure out where I was going wrong with loading relevant few-shot examples. Let's see if you can spot the problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VECTOR_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedding&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;COSINE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;posts&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;is_deleted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="n"&gt;APPROX&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Did you find it?&lt;/p&gt;

&lt;p&gt;No? Well, that's because there's nothing technically wrong with it. It finds the memories most similar to the question. The problem is that the most similar memory isn't always the right memory.&lt;/p&gt;

&lt;p&gt;Memories pile up. After a few years of writing, my store has a post where I argued for one approach and a newer one where I'd changed my mind. It has posts on event-driven architecture, event sourcing, and event streaming that are all &lt;em&gt;very&lt;/em&gt; similar to each other in the embedding space. It has a post on serverless pricing and one on cold starts that use the same vocabulary but make completely different points. As far as similarity scores go, these groups are basically the same thing. It just comes down to a date, a specific term, or the actual point of the post.&lt;/p&gt;

&lt;p&gt;So retrieval worked, but my context was garbage.&lt;/p&gt;

&lt;p&gt;The standard fix is a &lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/vecse/use-reranking-better-rag-results.html" rel="noopener noreferrer"&gt;reranker&lt;/a&gt;, which helps to improve &lt;em&gt;relevance&lt;/em&gt; instead of just &lt;em&gt;similarity&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;When you embed a memory, the model has no idea what will be asked of it later. Your query gets embedded and a similarity score is calculated against the embeddings in the store. A cross-encoder (a specific type of reranking model) looks at the query and memory together and scores them by how well a memory answers the question. This means the relevance is higher, and it often leads to better results.&lt;/p&gt;

&lt;p&gt;But there's a catch (of course).&lt;/p&gt;

&lt;p&gt;A cross-encoder runs at query time, once per candidate, on every request. That means added latency to your query. 🙃&lt;/p&gt;

&lt;p&gt;But if the relevance of the results is meaningfully better, that makes up for the increased latency, right? To a point, yes. If the latency is so high the user experience suffers, it's probably not worth the cost. But if it's relatively minor, then absolutely.&lt;/p&gt;

&lt;p&gt;So I built a benchmark to evaluate the tradeoff. I started from the evaluation plan in &lt;a href="https://fandf.co/4ymVZKc" rel="noopener noreferrer"&gt;Oracle's production RAG evaluation guide&lt;/a&gt;, which compares keyword, vector, and hybrid retrieval against judged queries. Then I pushed it one step further and put a reranker on top of each, to see how much relevance I got for the latency cost. For the reranker, I used the &lt;a href="https://huggingface.co/BAAI/bge-reranker-base" rel="noopener noreferrer"&gt;BGE reranker&lt;/a&gt; loaded into the database as an ONNX model, so scoring happens directly in the query with no extra service to call. 🔥&lt;/p&gt;

&lt;p&gt;Let's dive in. The results are fascinating.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Before I continue, thank you to &lt;a href="https://www.oracle.com" rel="noopener noreferrer"&gt;Oracle&lt;/a&gt; for sponsoring this post. Opinions are my own.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Filter first, then rank
&lt;/h2&gt;

&lt;p&gt;RAG is cool because it involves multiple kinds of lookups. You have &lt;em&gt;lexical retrieval&lt;/em&gt;, which is great for identifiers, codes, and names. You also have &lt;em&gt;vector retrieval&lt;/em&gt;, which finds semantically similar data via embeddings. I benchmarked both on their own and together as a hybrid search, fusing the two ranked lists with reciprocal rank fusion (RRF).&lt;/p&gt;

&lt;p&gt;Both paths run the same scope filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;TENANT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;tenant&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OWNER_ID&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;OWNER_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EXPIRES_AT&lt;/span&gt; &lt;span class="k"&gt;IS&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="n"&gt;EXPIRES_AT&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;SYSDATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I share this because the WHERE clause handles &lt;em&gt;eligibility&lt;/em&gt;, which is different from relevance. I don't want an expired memory coming back in my results, like a 2025 take on building AI agents that I've since moved past. And I definitely don't want another tenant's data showing up (security nightmare). Oracle's guide calls these filters mandatory for multi-tenant data. They run before ranking, so everything downstream only deals with memories that are allowed to be there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting the baseline
&lt;/h2&gt;

&lt;p&gt;Before I could tell whether a reranker was worth anything, I needed to know the baseline values for these queries. In addition to latency, I also needed to know the &lt;a href="https://towardsdatascience.com/demystifying-ndcg-bee3be58cfe0/" rel="noopener noreferrer"&gt;normalized discounted cumulative gain (nDCG)&lt;/a&gt; and recall. Capturing these metrics lets us objectively measure relevance alongside latency.&lt;/p&gt;

&lt;p&gt;The baseline measurements were taken with K=10, meaning I took the top 10 results.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Retrieval&lt;/th&gt;
&lt;th&gt;nDCG@10&lt;/th&gt;
&lt;th&gt;Recall@10&lt;/th&gt;
&lt;th&gt;Median latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Vector&lt;/td&gt;
&lt;td&gt;0.753&lt;/td&gt;
&lt;td&gt;0.865&lt;/td&gt;
&lt;td&gt;20.0 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lexical&lt;/td&gt;
&lt;td&gt;0.727&lt;/td&gt;
&lt;td&gt;0.740&lt;/td&gt;
&lt;td&gt;4.7 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hybrid RRF&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.832&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.885&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;20.5 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;nDCG measures how well a ranking puts relevant items near the top, with higher being better. It's not a measure of how many answers were right, though. Vector's 0.753 means that the rankings earned 75% of the score a perfect ordering would have, on average.&lt;/p&gt;

&lt;p&gt;Recall ignores order. It tells you whether the correct memories made it into the top 10 at all. A 0.865 means that about 87% of the memories that should have come back did. Both of these metrics are evaluated against a known ground-truth set of data in order to be objective. Meaning I know ahead of time what the results should have been given the corpus and the query I was benchmarking.&lt;/p&gt;

&lt;p&gt;You can see from these results that lexical is fast, but it's not super accurate. Vector catches more at the cost of latency, but still ranks them worse than I'd want. Fusing the two rankings had the best results, with minimal additional latency.&lt;/p&gt;

&lt;p&gt;From the baseline results alone, it looks like a hybrid search will be the retrieval method to beat if latency doesn't drop off a cliff.&lt;/p&gt;

&lt;h2&gt;
  
  
  The enlightening reranker results
&lt;/h2&gt;

&lt;p&gt;Now to run the same benchmark with the reranker. In Oracle, it's one more step at the end of the retrieval query. Retrieval narrows everything down to the top candidates, and &lt;code&gt;PREDICTION()&lt;/code&gt; runs the cross-encoder against each one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;PREDICTION&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BGE_RERANKER&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt;
         &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'&amp;lt;/s&amp;gt;&amp;lt;/s&amp;gt; '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;'. '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;DATA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;&amp;lt;/s&amp;gt;&amp;lt;/s&amp;gt;&lt;/code&gt; is the delimiter the model expects between a query and a passage. I'm only pulling back the ids and the reranked scores for the benchmark. And you might notice that the reranking is done entirely in the database query - super cool.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;nDCG@10&lt;/th&gt;
&lt;th&gt;Recall@10&lt;/th&gt;
&lt;th&gt;Median latency&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hybrid, no reranker&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.832&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.885&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;20.5 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hybrid + reranker, 10 candidates&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.832&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.885&lt;/td&gt;
&lt;td&gt;585 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector + reranker, 20 candidates&lt;/td&gt;
&lt;td&gt;0.830&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.896&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1,258 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Surprisingly, adding the reranker to hybrid search didn't do anything to nDCG at all. The best reranked score came from vector retrieval with 20 candidates, and it still scored lower than plain hybrid with no reranker, &lt;em&gt;at 61 times the latency&lt;/em&gt;. It found slightly more of the right memories, but ranked them a little worse.&lt;/p&gt;

&lt;p&gt;I also tried giving the reranker more to work with. For vector retrieval, latency grew linearly with candidate count (1,258ms at 20 candidates and 2,604ms at 40), and nothing got meaningfully better.&lt;/p&gt;

&lt;p&gt;That doesn't mean the reranker did nothing, though. I dug a little deeper and took the change for each query and resampled them 2,000 times to see how much the average varies depending on which queries you run. 95% of the resampled averages landed between -0.079 and +0.073.&lt;/p&gt;

&lt;p&gt;And if we use that range, +0.073 would take hybrid from 0.832 to 0.905, which is a meaningful improvement. But conversely, the other end of the spectrum would drop it to 0.753, which is where plain vector search started.&lt;/p&gt;

&lt;p&gt;Given the range, the results are inconclusive, at least with my sample size of 16 queries. The real effect could be a solid improvement, nothing at all, or even a loss, and 16 queries can't tell those apart.&lt;/p&gt;

&lt;p&gt;This ended up being a big realization for me. Relevance is measurable, but how &lt;em&gt;confident&lt;/em&gt; you can be depends on how many judged queries you have. A couple thousand iterations of 16 queries is still only 16 queries, and that's why the range is so wide.&lt;/p&gt;

&lt;p&gt;Meanwhile, the latency hit is pretty obvious. It was nearly 30 times slower for every request. Now I had to ask myself when that latency is worth it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where rerankers make sense
&lt;/h2&gt;

&lt;p&gt;Stay with me. Rerankers have lots of value despite what we've discussed so far. The underwhelming results are because I improved retrieval quality in a cheaper way. Let's look again at the baseline compared to the reranked version:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;First stage&lt;/th&gt;
&lt;th&gt;No reranker&lt;/th&gt;
&lt;th&gt;10 candidates&lt;/th&gt;
&lt;th&gt;20 candidates&lt;/th&gt;
&lt;th&gt;40 candidates&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lexical&lt;/td&gt;
&lt;td&gt;0.727&lt;/td&gt;
&lt;td&gt;0.800&lt;/td&gt;
&lt;td&gt;0.801&lt;/td&gt;
&lt;td&gt;0.809&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector&lt;/td&gt;
&lt;td&gt;0.753&lt;/td&gt;
&lt;td&gt;0.821&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.830&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.814&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hybrid RRF&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.832&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.832&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.829&lt;/td&gt;
&lt;td&gt;0.817&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Intuitively, the weaker retrieval methods got the most out of reranking. Lexical and vector each gained about 0.075, while hybrid gained nothing.&lt;/p&gt;

&lt;p&gt;Which makes sense because a reranker doesn't retrieve anything. It reorders (or re-ranks 😄) what it's given. If your first stage is already pulling the correct memories into the pool in a sensible order, there's nothing left to fix.&lt;/p&gt;

&lt;p&gt;Reranking vector retrieval with 20 candidates left me a little curious because it made the top five &lt;em&gt;less&lt;/em&gt; alike. I measured it a couple of different ways (with embeddings and with plain word overlap) and verified the numbers. Thinking about it though, that is desired behavior. Remember that memory lookalike issue I talked about? The cross-encoder is separating the right memory from its lookalikes and pushing the lookalikes down. Which is exactly what I said I wanted.&lt;/p&gt;

&lt;p&gt;The takeaway I want you to leave with is that you should &lt;em&gt;rerank when you have one retrieval signal, and fuse when you have two.&lt;/em&gt; Doing both leaves you paying for additional latency you don't need.&lt;/p&gt;

&lt;h2&gt;
  
  
  My new approach
&lt;/h2&gt;

&lt;p&gt;I started this wanting to know whether a reranker was worth the extra inference. After running through several rounds of benchmarks and tweaking queries, I'm not sure that was the right question. My approach to the problem is much more informed and now walks a different path than I expected.&lt;/p&gt;

&lt;p&gt;When building a RAG pipeline for memory, I'll always filter first. Owner, expiration, whatever makes a memory eligible for you. Apply these in the query before anything gets ranked. Reducing the number of candidates means fewer memories to rank (plus, you know, security and quality and all that 😜).&lt;/p&gt;

&lt;p&gt;Next, I'd add a second retrieval signal before adding a reranking model. If you have vector search, add lexical. If you have lexical, add vector. Fuse the rankings with RRF. Doing that with my tests took nDCG@10 from 0.75 with vector retrieval to 0.83 and only slowed my query down by half a millisecond.&lt;/p&gt;

&lt;p&gt;After adding the second retrieval signal, I'd measure everything. Write down 15 to 20 real requests you'd give your agent, making sure to include some difficult ones and edge cases. Things like asking for exact keyword lookups, or for advice on something you've changed your mind on a couple of times. Keep those handy. The &lt;a href="https://fandf.co/4ymVZKc" rel="noopener noreferrer"&gt;guide I was following&lt;/a&gt; says to rerun them every time you change chunking, the embedding model, or the reranker, and it's right.&lt;/p&gt;

&lt;p&gt;Then you can try a reranker against your baseline. Try a few different candidate counts (&lt;code&gt;:n&lt;/code&gt; in the query above). Check whether the quality difference is consistently higher across your queries and decide if the additional latency is worth the quality difference.&lt;/p&gt;

&lt;p&gt;Luckily for me, what I built in &lt;a href="https://blogs.oracle.com/developers/how-i-taught-an-ai-to-sound-like-me-agent-memory-with-oracle-database-26ai" rel="noopener noreferrer"&gt;my original post&lt;/a&gt; doesn't need to change much. I still have the same tables and the same reflection loop. I'm really just updating the database query that decides which memories the agent sees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;The benchmark and instructions to try it all yourself are &lt;a href="https://github.com/allenheltondev/rag-reranker-bench" rel="noopener noreferrer"&gt;available in GitHub&lt;/a&gt;. It spins up &lt;a href="https://www.oracle.com/database/free/" rel="noopener noreferrer"&gt;Oracle AI Database Free&lt;/a&gt; in Docker, generates a synthetic corpus of 480 memories and 16 judged queries, loads the ONNX models, and runs the whole thing.&lt;/p&gt;

&lt;p&gt;Everything we've discussed today ran on Free in Docker, capped at two CPUs and 2 GB. It's an intentionally small box, because we aren't running a capacity benchmark and none of these numbers should be used for sizing anything.&lt;/p&gt;

&lt;p&gt;If you want to run this kind of evaluation on your own retrieval, start with Oracle's &lt;a href="https://fandf.co/4ymVZKc" rel="noopener noreferrer"&gt;production RAG evaluation guide&lt;/a&gt;. It covers more than what we talked about here, like grading the generated answer and testing freshness, tenant isolation, and when the agent should refuse to answer. It's pretty solid.&lt;/p&gt;

&lt;p&gt;Agent memory needs more than vector search. Turns out, just using vector search means you're asking a single similarity score to decide what's relevant, current, and yours. Fortunately for all of us, "more" is a lot cheaper than I assumed.&lt;/p&gt;

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

</description>
      <category>ai</category>
      <category>agents</category>
      <category>memory</category>
    </item>
    <item>
      <title>Orchestrating workflows with Amazon S3 Files and AWS Lambda durable functions using Java SDK - Part 7 AI Agents with Spring AI and AgentCore on AWS Lambda</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 28 Sep 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-376f</link>
      <guid>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-376f</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-483b"&gt;part 6&lt;/a&gt; of the series, we moved the logic associated with each step into a separate Lambda function. We explored how to invoke another Lambda function within the durable step. Also, we showed how to invoke multiple Lambda functions in parallel.&lt;/p&gt;

&lt;p&gt;In this article, we'll transform our application into an agentic one. Currently, the search for YouTube and upcoming talks returns static content. We'll replace it with the &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 Gateway Web Search Tool&lt;/a&gt;. As it's exposed via MCP, we need an MCP client to connect to it. For that, we'll use the &lt;a href="https://spring.io/projects/spring-ai" rel="noopener noreferrer"&gt;Spring AI framework&lt;/a&gt;. First, we still use AWS Lambda to host the AI agents. In later parts, we'll show how to host those AI agents on &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; instead.&lt;/p&gt;

&lt;p&gt;The question is whether it'd be appropriate to host our AI agents on AWS Lambda. Our agents are really simple and return a constant amount of data. They also have a nearly constant context size. We don't need the flexibility of Firecracker, including the ability to grow and shrink the CPU and memory usage of VMs in place. Also, our agents are stateless, so we don't need the AgentCore support of stateful streamable-HTTP MCP servers. For such agents, it's feasible to host them on AWS Lambda.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction to Spring AI framework
&lt;/h2&gt;

&lt;p&gt;I've written an entire article series on the &lt;a href="https://dev.to/vkazulkin/series/34771"&gt;Spring AI framework&lt;/a&gt; and have also heavily used this framework in my article series &lt;a href="https://vkazulkin.com/series/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore/" rel="noopener noreferrer"&gt;Building AI Agents with Spring AI and Amazon Bedrock AgentCore&lt;/a&gt;. I refer to them to understand the basics. Spring AI is based on &lt;a href="https://spring.io/projects/spring-boot" rel="noopener noreferrer"&gt;Spring Boot&lt;/a&gt;, but we have pure Lambda functions in our application instead. So, we need to have a Spring context to use Spring AI. How can we achieve it?&lt;/p&gt;

&lt;h2&gt;
  
  
  How to convert AWS Lambda to a Spring (Boot) Application
&lt;/h2&gt;

&lt;p&gt;I've written another article series about &lt;a href="https://dev.to/vkazulkin/series/34774"&gt;Spring Boot 3.4 applications on AWS Lambda&lt;/a&gt; where I introduced 3 approaches to convert AWS Lambda to a Spring (Boot) Application :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/aws/serverless-java-container/wiki/Quick-start---Spring-Boot4" rel="noopener noreferrer"&gt;AWS Serverless Java Container for Spring Boot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/aws/aws-lambda-web-adapter" rel="noopener noreferrer"&gt;AWS Lambda Web Adapter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.spring.io/spring-cloud-function/reference/adapters/aws-intro.html" rel="noopener noreferrer"&gt;Spring Cloud Function for AWS Lambda&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, I'll go for AWS Serverless Java Container for Spring Boot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sample application using AgentCore Gateway Web Search Tool and Spring AI hosted on AWS Lambda
&lt;/h2&gt;

&lt;p&gt;The target architecture of our sample application looks 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%2Fnjlpqnavrgye3dqnrnw7.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%2Fnjlpqnavrgye3dqnrnw7.png" alt=" " width="800" height="344"&gt;&lt;/a&gt;&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-483b"&gt;part 6&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-spring-ai-agentcore-gateway-web-search-tool" rel="noopener noreferrer"&gt;aws-s3-files-lambda-durable-functions-spring-ai-agentcore-gateway-web-search-tool&lt;/a&gt; repository. We explain step by step how to adjust our application. Please go through the content of part 6 to understand the sample application and the basic concepts of Lambda durable functions. There will be some changes in the Infrastructure as Code (IaC) part as well.&lt;/p&gt;

&lt;p&gt;First, let's import some important dependencies in the &lt;a&gt;pom.xml&lt;/a&gt;. Those include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;aws-serverless-java-container-springboot4 (to use AWS Serverless Java Spring Boot 4 Container)&lt;/li&gt;
&lt;li&gt;spring-ai-starter-mcp-client (to use Spring AI MCP Client)&lt;/li&gt;
&lt;li&gt;spring-ai-starter-model-bedrock-converse (to use Spring AI Bedrock Converse API)&lt;/li&gt;
&lt;li&gt;others&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next, let's create the Spring Boot &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-spring-ai-agentcore-gateway-web-search-tool/src/main/java/dev/vkazulkin/controller/AuthorContentWebSearchExtractorController.java" rel="noopener noreferrer"&gt;AuthorContentWebSearchExtractorController&lt;/a&gt; controller:&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;AuthorContentWebSearchExtractorController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; 
&lt;span class="o"&gt;...&lt;/span&gt;

    &lt;span class="nd"&gt;@RequestMapping&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="s"&gt;"/author/content/youtubeVideos"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RequestMethod&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;consumes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MediaType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;APPLICATION_JSON_VALUE&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;produces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MediaType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;APPLICATION_JSON_VALUE&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;YouTubeVideos&lt;/span&gt; &lt;span class="nf"&gt;searchForYouTubeVideos&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestBody&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="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;webSearch&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;"YouTube videos"&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="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="o"&gt;}&lt;/span&gt;


   &lt;span class="nd"&gt;@RequestMapping&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="s"&gt;"/author/content/upcomingTalks"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RequestMethod&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;consumes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MediaType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;APPLICATION_JSON_VALUE&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;produces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MediaType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;APPLICATION_JSON_VALUE&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;UpcomingTalks&lt;/span&gt; &lt;span class="nf"&gt;searchForUpcomingTalks&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestBody&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="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;webSearch&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;"upcoming talks"&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="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="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 define 2 (web search) GET methods, &lt;em&gt;searchForYouTubeVideos&lt;/em&gt; and &lt;em&gt;searchForUpcomingTalks&lt;/em&gt;, with the specified path and incoming and outgoing media types. They both receive the JSON representation 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-spring-ai-agentcore-gateway-web-search-tool/src/main/java/dev/vkazulkin/entity/Author.java" rel="noopener noreferrer"&gt;Author&lt;/a&gt; class in their HTTP body. We can also modify our application to receive first and last name as request parameters. For this, we also need to adjust both paths, for example, to /author/content/upcomingTalks/{firstname}/{lastname}. We'll take care of the implementation of the &lt;em&gt;websearch&lt;/em&gt; method later.&lt;/p&gt;

&lt;p&gt;The controller is a standard Spring Boot controller where we have access to the whole Spring Boot (web) functionality. &lt;/p&gt;

&lt;p&gt;Our Lambda durable function &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-spring-ai-agentcore-gateway-web-search-tool/src/main/java/dev/vkazulkin/handler/AuthorContentExtractor.java" rel="noopener noreferrer"&gt;AuthorContentExtractor&lt;/a&gt; looks very similar to the function we defined in &lt;a href="https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-483b"&gt;part 6&lt;/a&gt;.  Here we invoke in parallel 2 Lambda functions: &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-spring-ai-agentcore-gateway-web-search-tool/src/main/java/dev/vkazulkin/handler/UpcomingTalksWebSearchExtractor.java" rel="noopener noreferrer"&gt;UpcomingTalksWebSearchExtractor&lt;/a&gt; and &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-spring-ai-agentcore-gateway-web-search-tool/src/main/java/dev/vkazulkin/handler/YouTubeVideosWebSearchExtractor.java" rel="noopener noreferrer"&gt;YouTubeVideosWebSearchExtractor&lt;/a&gt;. We'll transform exactly those 2 functions, so they perform the agentic search for the content instead of returning the static one. When we invoke them, we pass an object of type &lt;em&gt;Author&lt;/em&gt; as a payload. Our Lambda durable function &lt;em&gt;AuthorContentExtractor&lt;/em&gt; receives this payload as its input. &lt;/p&gt;

&lt;p&gt;The question is how to transform this payload into the HTTP requests which 2 methods of the &lt;em&gt;AuthorContentWebSearchExtractorController&lt;/em&gt; receive. This is exactly the functionality that AWS Serverless Java Spring Boot 4 Container provides to us. This framework does this transformation automatically in case the input payload comes from Amazon API Gateway. The payload should be of either type &lt;em&gt;APIGatewayProxyRequestEvent&lt;/em&gt; (REST API) or type &lt;em&gt;APIGatewayV2HTTPEvent&lt;/em&gt; (HTTP API). For more information, please read the &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/services-apigateway.html" rel="noopener noreferrer"&gt;Invoking a Lambda function using an Amazon API Gateway endpoint&lt;/a&gt; article. In such a case, we can use the generic Lambda handler &lt;em&gt;com.amazonaws.serverless.proxy.spring.SpringDelegatingLambdaContainerHandler&lt;/em&gt; to do the job. Nevertheless, AWS Serverless Java Spring Boot 4 Container offers the functionality to convert and proxy any request to one described above. In our sample application, we'll use the &lt;em&gt;APIGatewayProxyRequestEvent&lt;/em&gt; request event. Let's explain how to do it.&lt;/p&gt;

&lt;p&gt;Both Lambda functions, &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-spring-ai-agentcore-gateway-web-search-tool/src/main/java/dev/vkazulkin/handler/UpcomingTalksWebSearchExtractor.java" rel="noopener noreferrer"&gt;UpcomingTalksWebSearchExtractor&lt;/a&gt; and &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-spring-ai-agentcore-gateway-web-search-tool/src/main/java/dev/vkazulkin/handler/YouTubeVideosWebSearchExtractor.java" rel="noopener noreferrer"&gt;YouTubeVideosWebSearchExtractor.java&lt;/a&gt;, recieve the input request of type &lt;em&gt;Author&lt;/em&gt;. The only difference is that they should convert this request into an &lt;em&gt;APIGatewayProxyRequestEvent&lt;/em&gt; request event and proxy it to different paths. We define those paths in the Lambda function itself. &lt;/p&gt;

&lt;p&gt;For the UpcomingTalksWebSearchExtractor Lambda function, it looks 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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UpcomingTalksWebSearchExtractor&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AuthorContentWebSearchExtractor&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;protected&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getPath&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="s"&gt;"/author/content/upcomingTalks"&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 path &lt;em&gt;/author/content/upcomingTalks&lt;/em&gt; exactly matches the path of the request mapping in the controller:&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;@RequestMapping&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="s"&gt;"/author/content/upcomingTalks"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RequestMethod&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;consumes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MediaType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;APPLICATION_JSON_VALUE&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;produces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MediaType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;APPLICATION_JSON_VALUE&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;UpcomingTalks&lt;/span&gt; &lt;span class="nf"&gt;searchForUpcomingTalks&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestBody&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="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;webSearch&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;"upcoming talks"&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="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="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So this Lambda function will proxy the incoming request to the controller's &lt;em&gt;searchForUpcomingTalks&lt;/em&gt; method.&lt;/p&gt;

&lt;p&gt;For the YouTubeVideosWebSearchExtractor Lambda function, it looks 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="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;YouTubeVideosWebSearchExtractor&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;AuthorContentWebSearchExtractor&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;protected&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getPath&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="s"&gt;"/author/content/youtubeVideos"&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 path &lt;em&gt;/author/content/youtubeVideos&lt;/em&gt; exactly matches the path of the request mapping in the controller:&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;@RequestMapping&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="s"&gt;"/author/content/youtubeVideos"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RequestMethod&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;consumes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MediaType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;APPLICATION_JSON_VALUE&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;produces&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MediaType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;APPLICATION_JSON_VALUE&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;YouTubeVideos&lt;/span&gt; &lt;span class="nf"&gt;searchForYouTubeVideos&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestBody&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="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;webSearch&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;"YouTube videos"&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="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="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So this Lambda function will proxy the incoming request to the controller's &lt;em&gt;searchForYouTubeVideos&lt;/em&gt; method.&lt;/p&gt;

&lt;p&gt;The generic business logic that performs the conversion is 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-spring-ai-agentcore-gateway-web-search-tool/src/main/java/dev/vkazulkin/handler/AuthorContentWebSearchExtractor.java" rel="noopener noreferrer"&gt;AuthorContentWebSearchExtractor&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;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AuthorContentWebSearchExtractor&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;RequestStreamHandler&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;static&lt;/span&gt; &lt;span class="nc"&gt;SpringBootLambdaContainerHandler&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;AwsProxyRequest&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;AwsProxyResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
&lt;span class="nc"&gt;SpringBootLambdaContainerHandler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAwsProxyHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Application&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="nd"&gt;@Override&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;handleRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;InputStream&lt;/span&gt; &lt;span class="n"&gt;inputStream&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; 
  &lt;span class="nc"&gt;OutputStream&lt;/span&gt; &lt;span class="n"&gt;outputStream&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Context&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;IOException&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;author&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;String&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputStream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readAllBytes&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;StandardCharsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;UTF_8&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;proxyRequestEvent&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;getAwsProxyRequest&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="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="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proxyRequestEvent&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&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;body&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;getBody&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;try&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;printStream&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;PrintStream&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outputStream&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;StandardCharsets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;UTF_8&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;printStream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;print&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&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="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;AwsProxyRequest&lt;/span&gt; &lt;span class="nf"&gt;getAwsProxyRequest&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;author&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;awsProxyRequest&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;AwsProxyRequest&lt;/span&gt; &lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;awsProxyRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setHttpMethod&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GET"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;awsProxyRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setPath&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;getPath&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="n"&gt;awsProxyRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setBody&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;header&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;SingleValueHeaders&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;put&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;awsProxyRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setHeaders&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&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;headers&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;Headers&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;awsProxyRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setMultiValueHeaders&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;headers&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;awsProxyRequestContext&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;AwsProxyRequestContext&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;apiGatewayRequestIdentity&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;ApiGatewayRequestIdentity&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;apiGatewayRequestIdentity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setApiKey&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"blabla"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;awsProxyRequestContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setIdentity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;apiGatewayRequestIdentity&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;awsProxyRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setRequestContext&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;awsProxyRequestContext&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;awsProxyRequest&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;        
&lt;span class="o"&gt;}&lt;/span&gt;

 &lt;span class="kd"&gt;protected&lt;/span&gt; &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;getPath&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 the code. As we see, our class implements the &lt;em&gt;RequestStreamHandler&lt;/em&gt; class. First, we instantiate the  &lt;em&gt;SpringBootLambdaContainerHandler&lt;/em&gt; handler with the proxy request and response type and the main entry point of the Spring Boot &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-spring-ai-agentcore-gateway-web-search-tool/src/main/java/dev/vkazulkin/Application.java" rel="noopener noreferrer"&gt;Application&lt;/a&gt; class. In the &lt;em&gt;handleRequest&lt;/em&gt; method, we convert the input stream into a JSON representation of the &lt;em&gt;Author&lt;/em&gt; object. We use it in the &lt;em&gt;getAwsProxyRequest&lt;/em&gt; method to create the &lt;em&gt;AwsProxyRequest&lt;/em&gt; object, which represents the &lt;em&gt;APIGatewayProxyRequestEvent&lt;/em&gt; request event. To do so, we set several properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP Method as GET (should be the same as the HTTP method of both controllers' methods).&lt;/li&gt;
&lt;li&gt;path (each of the 2 Lambda functions sets its own path as described above).&lt;/li&gt;
&lt;li&gt;HTTP body (we set the &lt;em&gt;Author&lt;/em&gt; object) as required by the 2 Lambda functions.&lt;/li&gt;
&lt;li&gt;header and multi-value headers with the content type of &lt;em&gt;application/json&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;some dummy settings for request content and API Gateway request identity. It leads to an error if we don't set them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After having created the &lt;em&gt;AwsProxyRequest&lt;/em&gt; object, we use the handler to proxy it. By doing so, the appropriate 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-spring-ai-agentcore-gateway-web-search-tool/src/main/java/dev/vkazulkin/controller/AuthorContentWebSearchExtractorController.java" rel="noopener noreferrer"&gt;AuthorContentWebSearchExtractorController&lt;/a&gt; will be invoked. As a result, we get an object of the type &lt;em&gt;AwsProxyResponse&lt;/em&gt;, which contains the response of the controller's method. This response is either of type &lt;em&gt;UpcomingTalks&lt;/em&gt; or of type &lt;em&gt;YouTubeVideos&lt;/em&gt; depending on the method invoked. We get this response by invoking the &lt;em&gt;body&lt;/em&gt; method, and then we inject it into the output stream. With that, our Lambda durable function receives the response of the Lambda invocation.&lt;/p&gt;

&lt;p&gt;Now let's implement the &lt;em&gt;webSearch&lt;/em&gt; method of the controller. This method implements the web search for the given author content (either upcoming talks or YouTube videos) using the &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 Gateway Web Search Tool&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction to the Amazon Bedrock AgentCore Gateway Web Search Tool
&lt;/h2&gt;

&lt;p&gt;AI agents are transforming how organizations interact with information, but they face a fundamental limitation: their knowledge is frozen at training time. When a user asks about today’s stock prices or a software release that shipped an hour ago, an agent relying solely on its training data cannot provide accurate answers.&lt;/p&gt;

&lt;p&gt;Building custom web search integrations to solve this is costly — procuring third-party search APIs, managing keys and quotas and rate limits, parsing inconsistent result formats across providers, building snippet extraction logic so models get relevant passages instead of raw HTML, reasoning about where customer queries travel and how data might be retained, and maintaining freshness and coverage over time. Each of these is a project in itself.&lt;/p&gt;

&lt;p&gt;Web Search Tool on Amazon Bedrock AgentCore eliminates that complexity. It is a fully managed, MCP-compliant web search capability that lets your agents retrieve information from the web without any infrastructure overhead. It is available as a managed connector that you connect to your AgentCore Gateway. Agents discover it with a standard tools/list call and invoke it like any other Model Context Protocol tool. There are no search APIs to provision, no outbound credentials to manage, and no result-parsing glue to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to set up the Amazon Bedrock AgentCore Gateway Web Search Tool
&lt;/h2&gt;

&lt;p&gt;I refer to the article &lt;a href="https://aws.amazon.com/de/blogs/machine-learning/introducing-web-search-on-amazon-bedrock-agentcore/" rel="noopener noreferrer"&gt;Introducing Web Search on Amazon Bedrock AgentCore&lt;/a&gt;, which describes everything you need to set it up. Currently, there is no IaC CDK support for the Web Search Tool, but setting it up manually takes minutes.&lt;/p&gt;

&lt;p&gt;As a small help, I provided the &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/tree/main/aws-s3-files-lambda-durable-functions-as-ai-agents-orchestrator-java-25/spring-ai-2.0-ai-agents-on-agentcore-runtime/cdk" rel="noopener noreferrer"&gt;spring-ai-2.0-ai-agents-on-agentcore-runtime/cdk&lt;/a&gt; repository that uses AWS CDK in Java. Please execute the Cognio and Gateway stacks. Then, manually add a Gateway MCP Target with the Web Search tool connector as shown in the pictures above.&lt;/p&gt;

&lt;p&gt;The created &lt;a href="https://docs.aws.amazon.com/de_de/bedrock-agentcore/latest/devguide/gateway.html" rel="noopener noreferrer"&gt;Bedrock AgentCore Gateway&lt;/a&gt; should look similar to this one:&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%2Fxdvoq8yptgc2kfioh96m.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%2Fxdvoq8yptgc2kfioh96m.png" alt=" " width="795" height="728"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We'll need the Gateway Resource ARN and Gateway Resource URL later. Then we need to create the AgentCore Target by selecting the MCP Target and &lt;em&gt;Web Search tool&lt;/em&gt; in the &lt;em&gt;Connectors&lt;/em&gt; section:&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%2Fr4tbf1gomxha9p087xf9.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%2Fr4tbf1gomxha9p087xf9.png" alt=" " width="800" height="470"&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%2F5wsyi3zi4pr7w87mqbqe.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%2F5wsyi3zi4pr7w87mqbqe.png" alt=" " width="800" height="691"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We'll need to define an IAM role with the following policy, as described in the article above:&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;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="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;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"InvokeWebSearchTool"&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"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&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="s2"&gt;"bedrock-agentcore:InvokeWebSearch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:bedrock-agentcore:us-east-1:aws:tool/web-search.v1"&lt;/span&gt;&lt;span class="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;"Sid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"InvokeGateway"&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"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&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="s2"&gt;"bedrock-agentcore:InvokeGateway"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{YOUR_AGENTCORE_GATEWAY_ARN}"&lt;/span&gt;&lt;span class="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;Please replace the &lt;em&gt;{YOUR_AGENTCORE_GATEWAY_ARN}&lt;/em&gt; with the ARN of your created AgentCore Gateway.&lt;/p&gt;

&lt;p&gt;Another question is how to set up the &lt;a href="https://docs.aws.amazon.com/de_de/bedrock-agentcore/latest/devguide/gateway-inbound-auth.html" rel="noopener noreferrer"&gt;inbound authorization&lt;/a&gt; of our AgentCore Gateway. We can put it completely public, which is not recommended. Another alternative is to use &lt;a href="https://docs.aws.amazon.com/de_de/bedrock-agentcore/latest/devguide/gateway-inbound-auth.html" rel="noopener noreferrer"&gt;IAM-based inbound authorization&lt;/a&gt;. In this article, we'll use &lt;a href="https://docs.aws.amazon.com/de_de/bedrock-agentcore/latest/devguide/gateway-inbound-auth.html#gateway-inbound-auth-jwt" rel="noopener noreferrer"&gt;JSON Web Token (JWT)-based inbound authorization&lt;/a&gt; with Amazon Cognito as an identity provider as we see in the picture above. I won't go into the details of how to create and configure the Cognito User (Client) Pool, but refer to my following articles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://vkazulkin.com/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-2-deploy-conference-search-application-on-the-agentcore-runtime/" rel="noopener noreferrer"&gt;Deploy Conference Search application on AgentCore Runtime&lt;/a&gt;. Here I provided IaC and an explanation of how to create the Cognito User (Client) Pool.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://vkazulkin.com/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-4-provide-mcp-tools-for-conference-application-via-agentcore-gateway/" rel="noopener noreferrer"&gt;Provide MCP tools for Conference application via AgentCore Gateway&lt;/a&gt;. Here I provided IaC and an explanation of how to configure the inbound authorization for the AgentCore Gateway with Amazon Cognito as an identity provider.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Alternatively, please look into the Cognito and Gateway stack of the &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/tree/main/aws-s3-files-lambda-durable-functions-as-ai-agents-orchestrator-java-25/spring-ai-2.0-ai-agents-on-agentcore-runtime/cdk" rel="noopener noreferrer"&gt;spring-ai-2.0-ai-agents-on-agentcore-runtime/cdk&lt;/a&gt;&amp;nbsp;repository for an example of how to set it up.&amp;nbsp;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to implement the Amazon Bedrock AgentCore Gateway Web Search Tool using Spring AI
&lt;/h2&gt;

&lt;p&gt;I've already covered how to talk to an LLM and use the MCP Client to connect to tools with Spring AI in my following articles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://vkazulkin.com/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-3-develop-local-mcp-client-for-conference-application/" rel="noopener noreferrer"&gt;Develop local MCP client for Conference application&lt;/a&gt;. &lt;/li&gt;
&lt;li&gt;
&lt;a href="https://vkazulkin.com/building-ai-agents-with-spring-ai-and-amazon-bedrock-agentcore-part-4-provide-mcp-tools-for-conference-application-via-agentcore-gateway/" rel="noopener noreferrer"&gt;Provide MCP tools for Conference application via AgentCore Gateway&lt;/a&gt;. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Please go through those articles first to understand the basics, as we'll reuse much of the business logic. Let's first configure the AgentCore Gateway resource URL and Cognito settings 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-spring-ai-agentcore-gateway-web-search-tool/src/main/resources/application.properties" rel="noopener noreferrer"&gt;application.properties&lt;/a&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;amazon.bedrock.agentcore.gateway.base.url&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://web-search-gateway-XXX.gateway.bedrock-agentcore.us-east-1.amazonaws.com&lt;/span&gt;
&lt;span class="py"&gt;amazon.bedrock.agentcore.gateway.endpoint&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/mcp&lt;/span&gt;

&lt;span class="py"&gt;cognito.user.pool.name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;UserPoolForAuthorContentAgenticSearch&lt;/span&gt;
&lt;span class="py"&gt;cognito.user.pool.client.name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;UserClientPoolForAuthorContentAgenticSearch&lt;/span&gt;
&lt;span class="py"&gt;cognito.auth.token.resource.server.id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;ResourceServerIdAuthorContentAgenticSearch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please split this URL into 2 parts: the base URL and the endpoint, which is always &lt;em&gt;/mcp&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The implementation of the &lt;em&gt;webSearch&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-spring-ai-agentcore-gateway-web-search-tool/src/main/java/dev/vkazulkin/controller/AuthorContentWebSearchExtractorController.java" rel="noopener noreferrer"&gt;AuthorContentWebSearchExtractorController&lt;/a&gt; controller looks 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="kd"&gt;private&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;T&lt;/span&gt; &lt;span class="nf"&gt;webSearch&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;String&lt;/span&gt; &lt;span class="n"&gt;searchTopic&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;maxNumberOfResults&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Class&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;clazz&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getAuthTokenViaHttpClient&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;try&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;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;McpClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getMcpClientTransport&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&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="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;initialize&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
         &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;listTools&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;forEach&lt;/span&gt;
             &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="o"&gt;-&amp;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;"tool found: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;tool&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;mcpToolCallbackProvider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SyncMcpToolCallbackProvider&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;mcpClients&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""
            Search for the %s given by %s %s. Provide maximum %d results.
            Your response should be in JSON format.
            Do not include any explanations, only provide a RFC8259 compliant JSON response 
            following this format without deviation.
            """&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;formatted&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;searchTopic&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="na"&gt;firstName&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="na"&gt;lastName&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;maxNumberOfResults&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;chatClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;user&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="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mcpToolCallbackProvider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getToolCallbacks&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;call&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;entity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clazz&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;This is a generic method, which takes the following parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;object of type &lt;em&gt;Author&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;search topic ("search for upcoming talks" or "search for YouTube videos" in our case)&lt;/li&gt;
&lt;li&gt;maximum number of results returned by the Web Search tool. Valid range: 1-25. Defaults to 10.&lt;/li&gt;
&lt;li&gt;result type (&lt;em&gt;UpcomingTalks&lt;/em&gt; or &lt;em&gt;YouTubeVideos&lt;/em&gt; in our case)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;First, get the authentication token from Amazon Cognito. We can remove this part, including setting this token in the header of the HTTP Streamable Client, in case our AgentCore isn't secured by the JWT (either public or we use IAM authorization). Next, we create the MCP client and initialize it. After that, we list the MCP tools available; in our case, only the &lt;em&gt;WebSearch&lt;/em&gt; tool. Then, we create &lt;em&gt;MCPToolCallbackProvider&lt;/em&gt; with the MCP client. Next, we construct the parametrized prompt by passing the author, the search topic, and the number of results. We also advise an LLM to return the response in JSON format, which our Lambda functions require.&lt;/p&gt;

&lt;p&gt;Finally, we use the &lt;em&gt;ChatClient&lt;/em&gt; from Spring AI to pass the prompt, the tools, and the result type. We implement the last one by using the &lt;em&gt;entity&lt;/em&gt; method, which tries to provide &lt;a href="https://spring.io/blog/2024/05/09/spring-ai-structured-output" rel="noopener noreferrer"&gt;the structured LLM output&lt;/a&gt;. Once again, for more details about this code, I refer to my articles mentioned above. We invoke the &lt;em&gt;websearch&lt;/em&gt; method from the controller's methods &lt;em&gt;searchForUpcomingTalks&lt;/em&gt; and &lt;em&gt;searchForYouTubeVideos&lt;/em&gt; with different parameters.&lt;/p&gt;

&lt;h2&gt;
  
  
  IaC for the sample application using AgentCore Gateway Web Search Tool and Spring AI hosted on AWS Lambda
&lt;/h2&gt;

&lt;p&gt;The IaC part 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-spring-ai-agentcore-gateway-web-search-tool/template.yaml" rel="noopener noreferrer"&gt;AWS SAM Template&lt;/a&gt; matches what we described in &lt;a href="https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-483b"&gt;part 6&lt;/a&gt;. There are, of course, some differences, as we need to give our 2 Lambda functions, which implement the AI agents, more permissions. We need these permissions to grant those Lambda functions access to the Cognito, AgentCore, and Bedrock services. Here is an example of the &lt;em&gt;UpcomingTalksWebSearchExtractorFunction&lt;/em&gt; Lambda function declaration:&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;UpcomingTalksWebSearchExtractorFunction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless::Function&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;FunctionName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;UpcomingTalksWebSearchExtractor&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.UpcomingTalksWebSearchExtractor::handleRequest&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;AllowedCognitoActions&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;cognito-idp:ListUserPools&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cognito-idp:ListUserPoolClients&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cognito-idp:DescribeUserPoolClient&lt;/span&gt;
              &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&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;InvokeGateway&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;bedrock-agentcore:InvokeGateway&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bedrock-agentcore:*WorkloadIdentity&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bedrock-agentcore:*CredentialProvider&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bedrock-agentcore:*Token*&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bedrock-agentcore:*Access*&lt;/span&gt;
              &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;arn:aws:bedrock-agentcore:*:*:*gateway*'&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;InvokeRuntime&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;bedrock-agentcore:*Runtime*&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bedrock-agentcore:*WorkloadIdentity&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bedrock-agentcore:*CredentialProvider&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bedrock-agentcore:*Token*&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bedrock-agentcore:*Access*&lt;/span&gt;
              &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;arn:aws:bedrock-agentcore:*:*:*runtime*'&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;InvokeBedrock&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;bedrock:InvokeModel&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bedrock:InvokeModelWithResponseStream&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bedrock:Converse&lt;/span&gt;
                &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bedrock:ConverseStream&lt;/span&gt;
              &lt;span class="na"&gt;Resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;
      &lt;span class="na"&gt;Events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;GetRequest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Api&lt;/span&gt;
          &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;RestApiId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!Ref&lt;/span&gt; &lt;span class="s"&gt;MyApi&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;/author/content/upcomingTalks&lt;/span&gt;
            &lt;span class="na"&gt;Method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;get&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The declaration of the &lt;em&gt;YouTubeVideosWebSearchExtractorFunction&lt;/em&gt; Lambda function looks very similar. &lt;/p&gt;

&lt;h2&gt;
  
  
  Building, packaging, deploying, and testing our sample application
&lt;/h2&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;AuthorContentWebSearchExtractor&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%2Fjkxvs5zu1ksgmkgllhdz.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%2Fjkxvs5zu1ksgmkgllhdz.png" alt=" " width="800" height="561"&gt;&lt;/a&gt;&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%2Fbiwlq3owjpink5og6thi.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%2Fbiwlq3owjpink5og6thi.png" alt=" " width="800" height="479"&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%2Ftv55gkizmklket5koqgk.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%2Ftv55gkizmklket5koqgk.png" alt=" " width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We see that this matches what we implemented. Our Lambda durable function invokes 2 Lambda functions in parallel to search for YouTube videos and upcoming talks (each in an individual branch). Then, it awaits the results of both invocations because we defined in the parallel configuration that we wait until all invocations are completed. Then the last Lambda function to write the author content into the file gets invoked. This process is the same as we described in &lt;a href="https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-483b"&gt;part 6&lt;/a&gt;, as the workflows remained the same. We only changed the implementation of 2 Lambda functions responsible for the content search. Now we use the AgentCore Gateway Web Search Tool for it.&lt;/p&gt;

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

&lt;p&gt;In this article, we transformed our application into an agentic one. We replaced the search for YouTube and upcoming talks with the AgentCore Gateway Web Search Tool. Also, we used the Spring AI framework. We used AWS Lambda to host the AI agents. &lt;/p&gt;

&lt;p&gt;As we saw, Lambda durable functions provide a lot of building blocks like synchronous and asynchronous steps, invocation of another Lambda function, invoke async, waits, and callbacks (and combinations of both, like wait for the callback), and parallel execution to orchestrate the AI agents and multi-step workflows. We only showed one simple example of how to orchestrate the AI agent by using another Lambda function invocation and parallel execution.&lt;/p&gt;

&lt;p&gt;In the next part, we'll show how to host those AI agents on the AgentCore Runtime instead.&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>serverless</category>
      <category>lambdadurablefunctions</category>
      <category>bedrockagentcore</category>
    </item>
    <item>
      <title>Monthly Amazon Location Service Updates - 2026.08</title>
      <dc:creator>Yasunori Kirimoto</dc:creator>
      <pubDate>Sun, 27 Sep 2026 13:58:07 +0000</pubDate>
      <link>https://dev.to/aws-heroes/monthly-amazon-location-service-updates-202608-gml</link>
      <guid>https://dev.to/aws-heroes/monthly-amazon-location-service-updates-202608-gml</guid>
      <description>&lt;h3&gt;
  
  
  Monthly Amazon Location Service Updates - 2026.08
&lt;/h3&gt;



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



&lt;h2&gt;
  
  
  2026.08 Updates
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2026/08/amazon-location-service/amazon-location-poi-categorization-density-map-styles/" rel="noopener noreferrer"&gt;Amazon Location Service now supports POI category and density filtering in map styles&lt;/a&gt;&lt;br&gt;
This release adds the poi-categories and poi-density query parameters to the GetStyleDescriptor API, letting developers specify an allowlist of POI categories to display and tune how many POIs appear on the map. Filtering is configured server-side, so maps render only the requested categories, such as Food &amp;amp; Drink, Transit, or Accommodations, at the chosen density with no additional code required on the client side.&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-202607-45a3" class="crayons-story__hidden-navigation-link"&gt;Monthly Amazon Location Service Updates - 2026.07&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-4284415" 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-202607-45a3" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Aug 1&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-202607-45a3" id="article-link-4284415"&gt;
          Monthly Amazon Location Service Updates - 2026.07
        &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-202607-45a3#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;/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>
    <item>
      <title>Which Part Was Me?</title>
      <dc:creator>Brian Tarbox</dc:creator>
      <pubDate>Thu, 24 Sep 2026 13:08:15 +0000</pubDate>
      <link>https://dev.to/aws-heroes/which-part-was-me-90</link>
      <guid>https://dev.to/aws-heroes/which-part-was-me-90</guid>
      <description>&lt;p&gt;Last week I said I was still working on an answer. I have a partial one. It's smaller than I hoped and bigger than I feared.&lt;/p&gt;

&lt;p&gt;Here's the question again, for anyone who skipped the last post. (I saw the analytics. I'm fine. I said it a little too fast.) When a tool does in seconds what took me a decade to learn, which part was me?&lt;/p&gt;

&lt;p&gt;I started by making a list. Engineers make lists when they're anxious. &lt;/p&gt;

&lt;p&gt;Column One&lt;/p&gt;

&lt;p&gt;Column one was the stuff I spent years learning that a model now does as well as I do, or better. It was longer than I wanted. Syntax for a dozen languages, most now niche (Assembler, Fortran, Prolog). The exact incantation that makes a linker stop complaining. API surfaces I memorized like phone numbers. Boilerplate. So much boilerplate. I was proud of all of it. Some of it paid for a house.&lt;/p&gt;

&lt;p&gt;A lot of it turns out to have been lookup. Very fast lookup, with good taste in what to look up, but lookup. That's the "smaller than I hoped" part. It stung more than I expected.&lt;/p&gt;

&lt;p&gt;Column Two&lt;/p&gt;

&lt;p&gt;Column two took longer, because the things in it were harder to name. Knowing which problem is worth solving before anyone writes a line. Looking at a clean, confident architecture diagram and feeling that something is wrong before I can say what. Asking the question nobody in the meeting asked. Knowing when the right answer is "don't build it." None of it survives being written down as a rule.&lt;/p&gt;

&lt;p&gt;That's the "bigger than I feared" part.&lt;/p&gt;

&lt;p&gt;In college I studied Wittgenstein, who spent a lot of time on a question that sounds dumb until it doesn't. How do you know how to follow a rule? Not the rule itself. The applying of it. No rule tells you when it applies. You'd need another rule for that, and another for that one, and eventually you hit bottom on something that isn't a rule at all. It's practice. It's judgment. It's what's left when the lookup is free.&lt;/p&gt;

&lt;p&gt;I think that's the part that was me. It's also, inconveniently, the part I can't put on a slide.&lt;/p&gt;

&lt;p&gt;So how did I sort the columns? Mostly by remembering an old bug.&lt;/p&gt;

&lt;p&gt;The 2 a.m. Lesson&lt;/p&gt;

&lt;p&gt;Years ago I worked on Video On Demand at Motorola. We had a system that would run beautifully, then fall over. Not one server. The whole thing, in a cascade. We'd bring it back up, it would be fine, and then it wasn't. We looked for a pattern in time of day, load, content, the phase of the moon. Nothing.&lt;/p&gt;

&lt;p&gt;Eventually I noticed something small. The failure always started on the ninth request of a burst. Not the eighth. Not the tenth. The ninth.&lt;/p&gt;

&lt;p&gt;I had no idea why that mattered. I stared at it for longer than I'd like to admit. Then I remembered our servers had eight cores. The ninth request was the first one that had to wait. The first one to block.&lt;/p&gt;

&lt;p&gt;That pointed us at locking, specifically fair versus unfair locks in Java. A fair lock serves waiting threads in the order they arrived. An unfair one lets newcomers jump the queue. That meant that the 9th request stayed blocked until the burst of traffic cleared, and by that point it had timed out.&lt;/p&gt;

&lt;p&gt;Here's the thing. A model today could explain fair and unfair locking better than I could. Point it at the lock and it would probably fix it before lunch. What I'm less sure it could do is notice the nine. The number wasn't in any single log. It was a pattern across dozens of crashes, and its meaning lived in a hardware spec that had never been written down. Maybe a future model spots it. Even then, someone has to decide the ninth request is worth staring at.&lt;/p&gt;

&lt;p&gt;That staring is column two. It's also the part I'd been underpricing for 45 years, because it never showed up in a line count.&lt;/p&gt;

&lt;p&gt;The Test&lt;/p&gt;

&lt;p&gt;Here's the working distinction I landed on. If I can write it down as a rule, it's column one, or it will be soon. If I can only show it by example, it's column two.&lt;/p&gt;

&lt;p&gt;This is not a comfortable test. It means the columns aren't fixed. Things migrate. Every time someone writes down a piece of judgment clearly enough, it moves left. Code review heuristics. Architecture patterns. Security checklists. That's the grief I wrote about last time, and it isn't going away.&lt;/p&gt;

&lt;p&gt;But the migration has a floor. Every rule you write needs someone to decide when it applies. Wittgenstein's regress doesn't end with a better rule. It ends in &lt;em&gt;practice&lt;/em&gt;, not another rule.&lt;/p&gt;

&lt;p&gt;What I'm Doing About It&lt;/p&gt;

&lt;p&gt;If column two is the part that was me, I should probably invest in it on purpose. Four things so far.&lt;/p&gt;

&lt;p&gt;I review agent output like a teaching case, not a chore. When it's wrong, I ask how I knew. Half the time the answer is context it didn't have. The other half is interesting.&lt;/p&gt;

&lt;p&gt;I write down the near misses. Not the rules. The stories. Stories carry judgment in a way checklists don't. That's also how I learned most of it in the first place, usually from someone older telling me what went wrong in 1987.&lt;/p&gt;

&lt;p&gt;I pair juniors with failures instead of tutorials. A tutorial teaches column one, and a model will beat them at it by lunch. &lt;/p&gt;

&lt;p&gt;I stopped apologizing for the staring. Looking at something until you know what's wrong with it is not slowness. It's the job now. Possibly it always was.&lt;/p&gt;

&lt;p&gt;Back to the Buoy&lt;/p&gt;

&lt;p&gt;Last time I said nobody catches a drifting buoy, and the rower's job is to hold a steady stroke (I wrote the timing program for a regatta and the finish line buoy got cut and started floating downstream). I'd add one thing. Someone also has to notice the buoy moved. Someone on the bank has to say, "Wait. That's not where the finish was."&lt;/p&gt;

&lt;p&gt;That's column two. The model rows beautifully. It still doesn't know the buoy was cut.&lt;/p&gt;

&lt;p&gt;So which part was me? Less than I thought. The part that remains is the part that was always doing the real work, hiding behind all that syntax. I'm a little embarrassed it took a machine to show me. I'm also relieved it's there.&lt;/p&gt;

&lt;p&gt;Anyway. I'm going to go knead some dough. That's where the column two stuff tends to show up.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>You're still the bottleneck</title>
      <dc:creator>Allen Helton</dc:creator>
      <pubDate>Tue, 22 Sep 2026 14:33:41 +0000</pubDate>
      <link>https://dev.to/aws-heroes/youre-still-the-bottleneck-570e</link>
      <guid>https://dev.to/aws-heroes/youre-still-the-bottleneck-570e</guid>
      <description>&lt;p&gt;I went to a conference a couple of weeks ago. During the keynote, the speaker asked the audience "&lt;em&gt;how many of you have 40 or more agents running at any given time?&lt;/em&gt;" About a dozen hands went up. He then asked "&lt;em&gt;How many of you have 30 or more agents running?&lt;/em&gt;" Another dozen. This continued down to 20, 15, 10, 5, and eventually 1. Each time he asked, another 15-25 hands went up in the air.&lt;/p&gt;

&lt;p&gt;It wasn't until he got to 1 that I sheepishly put my hand in the air. Luckily I wasn't alone, but it immediately got me thinking. I think we substituted one bad practice for another.&lt;/p&gt;

&lt;p&gt;Remember a few months ago when &lt;a href="https://www.ibm.com/think/topics/tokenmaxxing" rel="noopener noreferrer"&gt;tokenmaxxing&lt;/a&gt; was a thing? If you don't, good. You missed a terrible practice. People were taking pride in the amount of tokens they were spending regardless of the value they were getting. I still don't really understand why that was a desirable thing.&lt;/p&gt;

&lt;p&gt;Tokenmaxxing has settled down significantly, but I fear that it's been replaced by something equally as bad: agentmaxxing.&lt;/p&gt;

&lt;p&gt;A quick Google search will show you there's no consensus on what agentmaxxing means (honestly I thought I made it up until I Googled it). But what I mean when I say it is that developers are taking pride in the number of agents they have running at any given time. Jumping back to that question in the conference keynote, people are running upwards of 40 agents &lt;em&gt;at any given time&lt;/em&gt;. 40?&lt;/p&gt;

&lt;p&gt;So I asked the person who delivered the keynote how many work streams 40 agents represent. And he told me about 4.&lt;/p&gt;

&lt;p&gt;Ok that makes so much more sense. He has four projects he's working in parallel, each one with a single orchestrator and about 9 subagents each actually doing the coding. That's better, but still a problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  You can't multitask like you think you can
&lt;/h2&gt;

&lt;p&gt;I have bad news. &lt;a href="https://pmc.ncbi.nlm.nih.gov/articles/PMC7075496/" rel="noopener noreferrer"&gt;You can't multitask&lt;/a&gt;. When you think you're multitasking, you're actually context switching really fast.&lt;/p&gt;

&lt;p&gt;The more inputs that demand your attention, the worse it gets. I've had times where I'm talking to my wife, one of my kids starts pulling on my shirt to show me something, the other kid starts crying in the other room, while the dog starts scratching at the door to go outside. At best, I'm going to somehow defer three of them to later and work through them sequentially. At worst, I'm going to walk into the other room and yell into a pillow because I'm overstimulated. 😅&lt;/p&gt;

&lt;p&gt;Now, imagine that was four agents reporting back at the same time asking for approval or a design decision or reporting a CI failure on four different projects. With each task that lands back on your plate, you have to reconstruct the mental model of what that app is, what you're trying to do, where the agent left off, &lt;em&gt;then&lt;/em&gt; you can act.&lt;/p&gt;

&lt;p&gt;Would you be able to handle each one appropriately? Probably not.&lt;/p&gt;

&lt;p&gt;Realistically, you'll probably hit the accept button without reading the context of the ask and tell your agents to just "fix it". That's what anybody would do when they make themselves the approval bottleneck across multiple unrelated contexts.&lt;/p&gt;

&lt;p&gt;When you start blindly approving tasks, you're inadvertently delegating the decision-making to the model. At that point, what are you there for?&lt;/p&gt;

&lt;h2&gt;
  
  
  You are the decision maker
&lt;/h2&gt;

&lt;p&gt;Code is cheaper than it ever has been. As a developer, the amount of your value that comes from typing code is dwindling. And if that was the only thing you had to do as a dev, the answer to "&lt;em&gt;will AI take our jobs&lt;/em&gt;" would be a whole heck of a lot scarier.&lt;/p&gt;

&lt;p&gt;Our value (among other things) comes from decision making. Weighing the risks, evaluating tradeoffs, and a nose for what looks right, to name a few. Some decisions are bigger than others, and the important ones need your undivided attention.&lt;/p&gt;

&lt;p&gt;I love AI, but it makes it way too easy to make bad decisions. And you often won't know about it until you're fighting a production issue when it's significantly more expensive to change. If an agent spits out a 1,000 word response after a long-running task, your task is to make an informed decision using the provided context, ask questions, and think two steps ahead.&lt;/p&gt;

&lt;p&gt;If you glance, hit accept, tab, glance again, and hit accept again, then you aren't a decision maker, you're a rubber stamp with a keyboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attention is your greatest resource
&lt;/h2&gt;

&lt;p&gt;I don't want you reading this and your takeaway being "&lt;em&gt;Allen says 40 agents is too many&lt;/em&gt;." That's not it at all. I'm not advocating for anything like that.&lt;/p&gt;

&lt;p&gt;What I am advocating for is not spreading yourself too thin. People have been saying for years that &lt;a href="https://www.iese.edu/stories/attention-is-one-of-the-most-scarce-and-important-resources-of-today/" rel="noopener noreferrer"&gt;attention is one of the scarcest and most important resources&lt;/a&gt; we have.&lt;/p&gt;

&lt;p&gt;In 2026, everything wants your attention. Humans have gotten really good at creating things that steal your attention away from something else. With coding agents, it takes almost 0 effort to bring in another work stream so you can "force multiply yourself." Just open another terminal, type in &lt;code&gt;claude&lt;/code&gt; and you're (basically) done.&lt;/p&gt;

&lt;p&gt;Protect it. Treat your attention like the scarce resource it is. Keep the decision-heavy workstreams to a minimum, just 1 or 2. Then run as many low-judgment streams as you want next to them. But keep your attention on the tasks that need it. Don't split your attention across decisions, because when everything is a priority, nothing is.&lt;/p&gt;

&lt;p&gt;Next time somebody asks you how many agents you have running, tell them that's the wrong thing to be counting. The better question is how many decisions you're actually making.&lt;/p&gt;

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

</description>
      <category>ai</category>
      <category>agents</category>
    </item>
    <item>
      <title>Orchestrating workflows with Amazon S3 Files and AWS Lambda durable functions using Java SDK - Part 6 Invoke a Lambda function and parallel execution</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 21 Sep 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-483b</link>
      <guid>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-483b</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. After that, in parts &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;3&lt;/a&gt;, &lt;a href="https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1f1n"&gt;4&lt;/a&gt;, and &lt;a href="https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-3kmg"&gt;5&lt;/a&gt; of the series, we explored how to use the AWS Lambda Durable Execution SDK for Java to implement wait for callback, wait for condition, and map operations.&lt;/p&gt;

&lt;p&gt;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 this part, we'll move the logic associated with each step into a separate Lambda function. An additional advantage of this approach is that it allows us to apply individual settings to those functions, like setting the right memory and granting the exact permissions they need. We'll explore how to invoke another Lambda function within the durable step. Also, we'll show how to invoke multiple Lambda functions in parallel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sample application with invoking another Lambda function and performing parallel execution
&lt;/h2&gt;

&lt;p&gt;Let's explore invoking another Lambda function and parallel execution. We get access to them through &lt;em&gt;DurableContext&lt;/em&gt;. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invoke - the invoke operation calls another Lambda function and waits for its result. It supports both durable functions and standard on-demand Lambda functions. When you call &lt;em&gt;ctx.invoke()&lt;/em&gt; or &lt;em&gt;ctx.invokeAsync()&lt;/em&gt;, the SDK checkpoints the start of the operation, and the durable functions backend invokes the target Lambda function. On replay, the SDK returns the checkpointed result without invoking the target again.&lt;/li&gt;
&lt;li&gt;parallel - parallel runs multiple independent branches concurrently, each in its own child context. Branches are registered via &lt;em&gt;branch()&lt;/em&gt; and execute immediately (respecting maxConcurrency). The operation completes when all branches finish or completion criteria are met.&lt;/li&gt;
&lt;/ul&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-as-orchestrator-java-25" rel="noopener noreferrer"&gt;aws-s3-files-lambda-durable-functions-as-orchestrator-java-25&lt;/a&gt; repository. We explain step by step how to adjust our application. Please go through the content of part 2 to understand the sample application and the basic concepts of Lambda durable functions. There will be some changes in the Infrastructure as Code (IaC) part as well. I'll leave out the wait for callbacks that we implemented in part 2. You can copy this logic into the Lambda durable function orchestrator, if you will.&lt;/p&gt;

&lt;p&gt;We'll move the business logic for 3 individual steps out of our Lambda durable function &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-as-orchestrator-java-25/src/main/java/dev/vkazulkin/handler/AuthorContentExtractor.java" rel="noopener noreferrer"&gt;AuthorContentExtractor&lt;/a&gt; to 3 different Lambda functions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-as-orchestrator-java-25/src/main/java/dev/vkazulkin/handler/UpcomingTalksExtractor.java" rel="noopener noreferrer"&gt;Search for upcoming talks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-as-orchestrator-java-25/src/main/java/dev/vkazulkin/handler/YouTubeVideosExtractor.java" rel="noopener noreferrer"&gt;Search for YouTube videos&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-as-orchestrator-java-25/src/main/java/dev/vkazulkin/handler/WriteAuthorContentToFile.java" rel="noopener noreferrer"&gt;Write author content to file&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4p2ieihwx5rlmxwnco6j.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%2F4p2ieihwx5rlmxwnco6j.png" alt=" " width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The implementation of all 3 Lambda functions is trivial. We'll focus on the change in the Lambda durable function &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-as-orchestrator-java-25/src/main/java/dev/vkazulkin/handler/AuthorContentExtractor.java" rel="noopener noreferrer"&gt;AuthorContentExtractor&lt;/a&gt; and in the IaC located 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-as-orchestrator-java-25/template.yaml" rel="noopener noreferrer"&gt;AWS SAM template&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We already showed in part 2 how to execute the first 2 Lambda functions asynchronously. This time, we'll show how to execute them in parallel. Here is how the complete code of the &lt;em&gt;AuthorContentExtractor&lt;/em&gt; Lambda durable function 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="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="o"&gt;{&lt;/span&gt;

&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;UPCOMING_TALKS_EXTRACTOR_FUNCTION_ARN&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; 
&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"UpcomingTalksExtractorFunctionArn"&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;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;YOUTUBE_VIDEOS_EXTRACTOR_FUNCTION_ARN&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"YouTubeVideosExtractorFunctionArn"&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;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="no"&gt;WRITE_AUTHOR_CONTENT_TO_FILE_FUNCTION_ARN&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getenv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WriteAuthorContentToFileFunctionArn"&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="nc"&gt;ParallelConfig&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;maxConcurrency&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="na"&gt;nestingType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NestingType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;NESTED&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;completionConfig&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CompletionConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;allCompleted&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;invokeConfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;InvokeConfig&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;build&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;parallel&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;parallel&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"parallel-search"&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;upcomingTalksFuture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parallel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"searchForUpcomingTalks-parallel-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;branchCtx&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;return&lt;/span&gt; &lt;span class="n"&gt;branchCtx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invoke&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="no"&gt;UPCOMING_TALKS_EXTRACTOR_FUNCTION_ARN&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="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;invokeConfig&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;youtubeVideosFuture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parallel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"searchForYouTubeVideos-parallel-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;branchCtx&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;return&lt;/span&gt; &lt;span class="n"&gt;branchCtx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invoke&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="no"&gt;YOUTUBE_VIDEOS_EXTRACTOR_FUNCTION_ARN&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="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;invokeConfig&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parallel&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;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;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;invoke&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="no"&gt;WRITE_AUTHOR_CONTENT_TO_FILE_FUNCTION_ARN&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="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;invokeConfig&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 the code. First, we pass the ARNs of all 3 Lambda functions via environment variables. We set those for the Lambda durable function &lt;em&gt;AuthorContentExtractorFunction&lt;/em&gt; 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-as-orchestrator-java-25/template.yaml" rel="noopener noreferrer"&gt;AWS SAM template&lt;/a&gt;. Also, we give our Lambda durable function the required permissions to invoke the other 3 Lambda functions:&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;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;AuthorContentExtractorOrchestrator&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;Environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;UpcomingTalksExtractorFunctionArn&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;UpcomingTalksExtractorFunction.Arn&lt;/span&gt;
          &lt;span class="na"&gt;YouTubeVideosExtractorFunctionArn&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;YouTubeVideosExtractorFunction.Arn&lt;/span&gt;
          &lt;span class="na"&gt;WriteAuthorContentToFileFunctionArn&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;WriteAuthorContentToFileFunction.Arn&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;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;InvokeLambda&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="s"&gt;lambda:InvokeFunction&lt;/span&gt;
              &lt;span class="na"&gt;Resource&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;YouTubeVideosExtractorFunction.Arn&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${YouTubeVideosExtractorFunction.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;UpcomingTalksExtractorFunction.Arn&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${UpcomingTalksExtractorFunction.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;WriteAuthorContentToFileFunction.Arn&lt;/span&gt;
                  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="kt"&gt;!Sub&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;${WriteAuthorContentToFileFunction.Arn}:*'&lt;/span&gt;
&lt;span class="s"&gt;....&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As we invoke &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-as-orchestrator-java-25/src/main/java/dev/vkazulkin/handler/UpcomingTalksExtractor.java" rel="noopener noreferrer"&gt;UpcomingTalksExtractor&lt;/a&gt; and &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-as-orchestrator-java-25/src/main/java/dev/vkazulkin/handler/YouTubeVideosExtractor.java" rel="noopener noreferrer"&gt;YouTubeVideosExtractor&lt;/a&gt; Lambda functions in parallel, we create &lt;em&gt;ParallelConfig&lt;/em&gt;. Here we set the maximum concurrency value and nesting type. There are 2  nesting types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nested&lt;/strong&gt; (the default one). Create CONTEXT operations for each branch/iteration with full checkpointing. Operations within each branch/iteration are wrapped in their own context. &lt;strong&gt;Observability&lt;/strong&gt;: High - each branch/iteration appears as a separate operation in execution history. &lt;strong&gt;Cost&lt;/strong&gt;: Higher - consumes more operations due to CONTEXT creation overhead. &lt;strong&gt;Scale&lt;/strong&gt;: Lower maximum iterations due to operation limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flat&lt;/strong&gt;.  Skip CONTEXT operations for branches/iterations using virtual contexts. Operations execute directly without individual context wrapping. &lt;strong&gt;Observability&lt;/strong&gt;: Lower - branches/iterations don't appear as separate operations. &lt;strong&gt;Cost&lt;/strong&gt;: ~30% lower - reduces operation consumption by skipping CONTEXT overhead. &lt;strong&gt;Scale&lt;/strong&gt;: Higher maximum iterations possible within operation limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also set the &lt;em&gt;CompletionConfig&lt;/em&gt;. Here, there are many possibilities from which we can choose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;allCompleted&lt;/strong&gt; (we use it). All items run regardless of failures. Failures are captured per-item.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;allSuccessful&lt;/strong&gt;. All items must succeed. Zero failures tolerated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;firstSuccessful&lt;/strong&gt;. Complete as soon as the first item succeeds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;minSuccessful&lt;/strong&gt;. Complete when the specified number of items have succeeded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;toleratedFailureCount&lt;/strong&gt;. Complete when more than the specified number of failures have occurred.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;toleratedFailurePercentage&lt;/strong&gt;. Complete when the failure percentage exceeds the specified threshold (0.0 to 1.0).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next, we create an &lt;em&gt;InvocationConfig&lt;/em&gt; for each individual Lambda function invocation that we can optionally set. Here we go with the default configuration. But we additionally set the tenant ID, which is used to isolate execution state for different tenants. It's required when invoking &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/tenant-isolation.html" rel="noopener noreferrer"&gt;multi-tenant Lambda functions&lt;/a&gt;. So far, we covered the following parts:&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;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ParallelConfig&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;maxConcurrency&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="na"&gt;nestingType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NestingType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;NESTED&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;completionConfig&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CompletionConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;allCompleted&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;invokeConfig&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;InvokeConfig&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;build&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 implement the Lambda function invocation in parallel:&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;parallel&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;parallel&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"parallel-search"&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;upcomingTalksFuture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parallel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"searchForUpcomingTalks-parallel-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;branchCtx&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;return&lt;/span&gt; &lt;span class="n"&gt;branchCtx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invoke&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="no"&gt;UPCOMING_TALKS_EXTRACTOR_FUNCTION_ARN&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="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;invokeConfig&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;youtubeVideosFuture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parallel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;branch&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"searchForYouTubeVideos-parallel-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;branchCtx&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;return&lt;/span&gt; &lt;span class="n"&gt;branchCtx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invoke&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="no"&gt;YOUTUBE_VIDEOS_EXTRACTOR_FUNCTION_ARN&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="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;invokeConfig&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parallel&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;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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, we invoke the &lt;em&gt;parallel&lt;/em&gt; method on the &lt;em&gt;DurableContext&lt;/em&gt; and pass the &lt;em&gt;ParallelConfig&lt;/em&gt;. We get back the &lt;em&gt;ParallelDurableFuture&lt;/em&gt;, with which we can start a branch. To do this, we invoke the &lt;em&gt;branch&lt;/em&gt; method for each individual Lambda invocation and specify the result type. In each branch, we call the &lt;em&gt;invoke&lt;/em&gt; method on the &lt;em&gt;BranchContext&lt;/em&gt;. Here we pass the following parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;name&lt;/strong&gt; - the unique operation name within this context. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;functionName&lt;/strong&gt; - the ARN or name of the Lambda function to invoke.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;payload&lt;/strong&gt; - the input payload to send to the target function.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;resultType&lt;/strong&gt; - the result class for deserialization.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We get back the &lt;em&gt;DurableFuture&lt;/em&gt; of the &lt;em&gt;resultType&lt;/em&gt; (in our case &lt;em&gt;UpcomingTalks&lt;/em&gt; or &lt;em&gt;YouTubeVideos&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;When we invoke the blocking &lt;em&gt;get&lt;/em&gt; method on the &lt;em&gt;ParallelDurableFuture&lt;/em&gt; to get the results with respect to the &lt;em&gt;ParallelConfig&lt;/em&gt;. Then, we invoke the &lt;em&gt;get&lt;/em&gt; method on &lt;em&gt;DurableFuture&lt;/em&gt; to get the individual results of both Lambda invocations. Finally, we create the _ AuthorContent _ with these results.&lt;/p&gt;

&lt;p&gt;The last step is to invoke the Lambda function, which is responsible for writing the author content into 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="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;invoke&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="no"&gt;WRITE_AUTHOR_CONTENT_TO_FILE_FUNCTION_ARN&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="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;invokeConfig&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As we explained 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;, we use S3 Files here. The whole part on how to configure S3 Files and give the Lambda function access to it has now moved from the &lt;em&gt;AuthorContentExtractorOrchestrator&lt;/em&gt; durable Lambda function to the &lt;em&gt;WriteAuthorContentToFileFunction&lt;/em&gt; Lambda function. See the &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-as-orchestrator-java-25/template.yaml" rel="noopener noreferrer"&gt;SAM template&lt;/a&gt; for more detail.&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;AuthorContentExtractorOrchestrator&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%2Fftjhvn72z4q0dpgstpfi.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%2Fftjhvn72z4q0dpgstpfi.png" alt=" " width="798" height="199"&gt;&lt;/a&gt;&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%2Ffrlfzv8c4kg5devzuw5m.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%2Ffrlfzv8c4kg5devzuw5m.png" alt=" " width="800" height="562"&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%2Ftv55gkizmklket5koqgk.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%2Ftv55gkizmklket5koqgk.png" alt=" " width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We see that this matches what we implemented. Our Lambda durable function invokes in parallel 2 Lambda functions to search for YouTube videos and upcoming talks (each in an individual branch). Then, it awaits the results of both invocations because we defined in the parallel configuration that we wait until all invocations are completed. Then the last Lambda function to write the author content into the file gets invoked.&lt;/p&gt;

&lt;p&gt;One general question at the end: is the Lambda function invocation from another Lambda function considered an anti-pattern? Or should we put an API Gateway in between instead? My answer is that with built-in fault-tolerance for AWS Lambda durable functions, it's now safe to invoke a Lambda function directly.&lt;/p&gt;

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

&lt;p&gt;In this part of the series, we moved the logic associated with each step into a separate Lambda function. We explored how to invoke another Lambda function within the durable step. Also, we showed how to invoke multiple Lambda functions in parallel.&lt;/p&gt;

&lt;p&gt;In the next part of the series, we'll transform our application into an agentic one. Currently, the search for YouTube and upcoming talks returns static content. We'll replace it with the &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 Gateway Web Search Tool&lt;/a&gt;. As it's exposed via MCP, we need an MCP client to connect to it. For that, we'll use the &lt;a href="https://spring.io/projects/spring-ai" rel="noopener noreferrer"&gt;Spring AI framework&lt;/a&gt;. First, we still use AWS Lambda to host the AI agents. In later parts, we'll show how to host those AI agents on the &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; instead.&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>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 5 Map and child context</title>
      <dc:creator>Vadym Kazulkin</dc:creator>
      <pubDate>Mon, 14 Sep 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-3kmg</link>
      <guid>https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-3kmg</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.  Later, in parts &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;3&lt;/a&gt; and &lt;a href="https://dev.to/aws-heroes/orchestrating-workflows-with-amazon-s3-files-and-aws-lambda-durable-functions-using-java-sdk-part-1f1n"&gt;4&lt;/a&gt; of the series, we explored how to use the AWS Lambda Durable Execution SDK for Java to implement wait for callback and wait for condition. &lt;/p&gt;

&lt;p&gt;In this part, we'll explore map and child context operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sample application with map and child context
&lt;/h2&gt;

&lt;p&gt;Let's explore the &lt;a href="https://github.com/aws/aws-durable-execution-sdk-java/blob/main/docs/core/child-contexts.md" rel="noopener noreferrer"&gt;child context&lt;/a&gt; operation first.  Child contexts run an isolated stream of work with their own operation counter and checkpoint log. They support the full range of durable operations — step, wait, invoke, createCallback, and nested child contexts.  In our sample application, we'll add a &lt;a href="https://github.com/aws/aws-durable-execution-sdk-java/blob/main/docs/core/map.md" rel="noopener noreferrer"&gt;map&lt;/a&gt; operation, which uses a child context underneath. We get access to it through &lt;em&gt;DurableContext&lt;/em&gt;. A &lt;em&gt;Map&lt;/em&gt; operation applies a function to each item in a collection concurrently, with each item running in its own child context.&lt;/p&gt;

&lt;p&gt;Now let's add an artificial use case: for each YouTube video found, we add it to the author's YouTube playlist. 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-java-25" rel="noopener noreferrer"&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-map-and-child-context-java-25" rel="noopener noreferrer"&gt;aws-s3-files-lambda-durable-functions-with-map-and-child-context-java-25&lt;/a&gt; repository.&lt;/p&gt;

&lt;p&gt;The main business logic we have to add is in the &lt;em&gt;saveYouTubeVideosToPlayList&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-map-and-child-context-java-25/src/main/java/dev/vkazulkin/handler/AbstractAuthorContentExtractor.java" rel="noopener noreferrer"&gt;AbstractAuthorContentExtractor&lt;/a&gt;:&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="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;saveYouTubeVideosToPlayList&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;YouTubeVideos&lt;/span&gt; &lt;span class="n"&gt;youtubeVideos&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;youtubePlayList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;findOrCreateYouTubePlayListForTheAuthor&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mapConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MapConfig&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;nestingType&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;NestingType&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;NESTED&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
       &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;maxConcurrency&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="na"&gt;completionConfig&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CompletionConfig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;allCompleted&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mapResult&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;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"saveYouTubeVideoToPlayList-step"&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="na"&gt;youtubeVideos&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; 
      &lt;span class="nc"&gt;Boolean&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;youtubeVideo&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;childCtx&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;return&lt;/span&gt; &lt;span class="n"&gt;childCtx&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;"saveYouTubeVideoToPlayList-step-"&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Boolean&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="n"&gt;saveYouTubeVideoToPlayList&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;youtubeVideo&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;youtubePlayList&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;getStepConfig&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;},&lt;/span&gt; &lt;span class="n"&gt;mapConfig&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="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;findOrCreateYouTubePlayListForTheAuthor&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="o"&gt;{&lt;/span&gt;
     &lt;span class="c1"&gt;// Use YouTube API to log in to the YouTube Account and search for the author's playlist&lt;/span&gt;
     &lt;span class="c1"&gt;// and return it if it already exists or create a new one&lt;/span&gt;
     &lt;span class="c1"&gt;// move this logic into a separate Lambda function and invoke it as a durable step&lt;/span&gt;
     &lt;span class="k"&gt;return&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="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt; &lt;span class="nf"&gt;saveYouTubeVideoToPlayList&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;YouTubeVideo&lt;/span&gt; &lt;span class="n"&gt;youtubeVideo&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;playListId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;//Use YouTube API to save the video URL to the playlist. &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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We invoke this &lt;em&gt;saveYouTubeVideosToPlayList&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-map-and-child-context-java-25/src/main/java/dev/vkazulkin/handler/AuthorContentExtractor.java" rel="noopener noreferrer"&gt;AuthorContentExtractor&lt;/a&gt; or &lt;a href="https://github.com/Vadym79/aws-s3-files-lambda-durable-functions-java-sdk/blob/main/aws-s3-files-lambda-durable-functions-with-map-and-child-context-java-25/src/main/java/dev/vkazulkin/handler/AsyncAuthorContentExtractor.java" rel="noopener noreferrer"&gt;AsyncAuthorContentExtractor&lt;/a&gt; classes.&lt;/p&gt;

&lt;p&gt;First of all, I won't use the YouTube API to create or retrieve the YouTube playlist for the given author and store each found video there. I use some static output, as the goal of this article is to demonstrate the functionality of the &lt;em&gt;map&lt;/em&gt; operation. &lt;/p&gt;

&lt;p&gt;Let's go step by step through this code. &lt;em&gt;Map&lt;/em&gt; applies a function to each item in a collection concurrently, with each item running in its own child context. Results are collected into a &lt;em&gt;MapResult&lt;/em&gt; that maintains input order. Each item's function receives its own DurableContext, so you can use any durable operation (step(), wait(), invoke(), etc.) inside the map function.  The function passed to map() is a &lt;em&gt;MapFunction&lt;/em&gt;:&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;@FunctionalInterface&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;MapFunction&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;I&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="no"&gt;O&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="no"&gt;O&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="no"&gt;I&lt;/span&gt; &lt;span class="n"&gt;item&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;index&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;context&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 index parameter is the zero-based position of the item in the input collection, useful for naming operations or correlating results. For each found YouTube video, we invoke the &lt;em&gt;saveYouTubeVideoToPlayList&lt;/em&gt; method, which returns the result of type &lt;em&gt;Boolean&lt;/em&gt;. In our case, it is always true, indicating that the video was stored in the playlist. &lt;/p&gt;

&lt;p&gt;The input collection must have deterministic iteration order. List, LinkedList, and TreeSet are accepted. HashSet and unordered map views are not, leading to the IllegalArgumentException. That's why I changed the implementation 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-map-and-child-context-java-25/src/main/java/dev/vkazulkin/entity/YouTubeVideos.java" rel="noopener noreferrer"&gt;YouTubeVideos&lt;/a&gt; class to use List instead of Set there.&lt;/p&gt;

&lt;p&gt;Next, let's explore &lt;em&gt;MapConfig&lt;/em&gt; in more detail. There are several main settings there:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;maxConcurrency&lt;/strong&gt; controls how many items execute concurrently. When set, items beyond the limit are queued and started as earlier items complete. The default is null (unlimited).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;nestingType&lt;/strong&gt;, with &lt;strong&gt;Nested&lt;/strong&gt; as the default. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Nested&lt;/strong&gt;. Create CONTEXT operations for each branch/iteration with full checkpointing. Operations within each branch/iteration are wrapped in their own context. &lt;strong&gt;Observability&lt;/strong&gt;: High - each branch/iteration appears as a separate operation in execution history. &lt;strong&gt;Cost&lt;/strong&gt;: Higher - consumes more operations due to CONTEXT creation overhead. &lt;strong&gt;Scale&lt;/strong&gt;: Lower maximum iterations due to operation limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flat&lt;/strong&gt;.  Skip CONTEXT operations for branches/iterations using virtual contexts. Operations execute directly without individual context wrapping. &lt;strong&gt;Observability&lt;/strong&gt;: Lower - branches/iterations don't appear as separate operations. &lt;strong&gt;Cost&lt;/strong&gt;: ~30% lower - reduces operation consumption by skipping CONTEXT overhead. &lt;strong&gt;Scale&lt;/strong&gt;: Higher maximum iterations possible within operation limits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;CompletionConfig&lt;/strong&gt; controls when the map operation stops starting new items. Here, there are many possibilities from which we can choose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;allCompleted&lt;/strong&gt; (we use it). All items run regardless of failures. Failures are captured per-item.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;allSuccessful&lt;/strong&gt;. All items must succeed. Zero failures tolerated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;firstSuccessful&lt;/strong&gt;. Complete as soon as the first item succeeds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;minSuccessful&lt;/strong&gt;. Complete when the specified number of items have succeeded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;toleratedFailureCount&lt;/strong&gt;. Complete when more than the specified number of failures have occurred.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;toleratedFailurePercentage&lt;/strong&gt;. Complete when the failure percentage exceeds the specified threshold (0.0 to 1.0).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When early termination is triggered, items that were never started have SKIPPED status with null for both result and error in the MapResult.&lt;/p&gt;

&lt;p&gt;Each &lt;em&gt;MapResultItem&lt;/em&gt; contains the following methods:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;status&lt;/strong&gt;: SUCCEEDED, FAILED, or SKIPPED.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;result&lt;/strong&gt;:  the result value, or null if failed/skipped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;error&lt;/strong&gt;: the error details as MapError, or null if succeeded/skipped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Failed items store error details as &lt;em&gt;MapError&lt;/em&gt;, a serializable record that survives checkpoint-and-replay cycles with the following methods that can be invoked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;errorType&lt;/strong&gt;:  Fully qualified exception class name (e.g., java.lang.RuntimeException).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;errorMessage&lt;/strong&gt;: The exception message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;stackTrace&lt;/strong&gt;:  Stack trace frames as a list of strings, or null.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One item's failure does not prevent other items from completing. &lt;em&gt;MapResult&lt;/em&gt; captures the failed items at their corresponding index.&lt;/p&gt;

&lt;p&gt;Map operations are fully durable. On replay after interruption:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Completed items return cached results without re-execution.&lt;/li&gt;
&lt;li&gt;Incomplete items resume from their last checkpoint.&lt;/li&gt;
&lt;li&gt;Items that never started execute fresh.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Small results (&amp;lt; 256KB) are checkpointed directly, whereas large results are reconstructed from individual child context checkpoints on replay.&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%2Fsj1489ohgljy9lud4gsq.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%2Fsj1489ohgljy9lud4gsq.png" alt=" " width="799" height="286"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here we see that the operation with the name &lt;em&gt;saveYouTubeVideoToPlayList-step&lt;/em&gt; and the subtype &lt;em&gt;Map&lt;/em&gt;. We also see 2 iterations (for each of the 2 YouTube videos found) with the subtype &lt;em&gt;MapIteration&lt;/em&gt;. Each iteration is executed as an individual durable step.&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%2Fjtqhquzb7wlhtd03mazx.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%2Fjtqhquzb7wlhtd03mazx.png" alt=" " width="800" height="579"&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%2Fnpzsq3fnnwjw04xhj8jk.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%2Fnpzsq3fnnwjw04xhj8jk.png" alt=" " width="800" height="548"&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;mapAsync&lt;/em&gt; operation. It starts the map operation without blocking, returning a &lt;em&gt;DurableFuture&lt;/em&gt;.&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 map and child context operations.  &lt;/p&gt;

&lt;p&gt;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 next part, 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>serverless</category>
      <category>s3files</category>
      <category>lambdadurablefunctions</category>
      <category>java</category>
    </item>
  </channel>
</rss>
