<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Lucas Blog]]></title><description><![CDATA[I write about Software Engineering, Cloud Architecture, AWS and Serverless]]></description><link>https://blog.lucasdev.info</link><image><url>https://cdn.hashnode.com/uploads/logos/5e95b544a12c7e6a1a6ab882/a6691bf1-8a8c-43a6-bbc7-e292d1c45e27.png</url><title>Lucas Blog</title><link>https://blog.lucasdev.info</link></image><generator>RSS for Node</generator><lastBuildDate>Mon, 18 May 2026 00:04:48 GMT</lastBuildDate><atom:link href="https://blog.lucasdev.info/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[AI Agents vs Workflows: what's the difference?]]></title><description><![CDATA[Introduction
My work as a software engineer and technical architect is centered around building and delivering software doing things like designing systems, collaborating with dev teams and helping mo]]></description><link>https://blog.lucasdev.info/ai-agents-vs-workflows</link><guid isPermaLink="true">https://blog.lucasdev.info/ai-agents-vs-workflows</guid><category><![CDATA[AI]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[llm]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[architecture]]></category><category><![CDATA[System Design]]></category><dc:creator><![CDATA[Lucas Vera Toro]]></dc:creator><pubDate>Thu, 23 Apr 2026 13:15:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5e95b544a12c7e6a1a6ab882/202981f3-748e-4019-9e2a-75bd7a0627b8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>Introduction</h1>
<p>My work as a software engineer and technical architect is centered around building and delivering software doing things like designing systems, collaborating with dev teams and helping move solutions from idea to implementation to operation. It's a highly technical role, but not necessarily at the frontier or state of the art AI</p>
<p>Still, over the past months I've been paying attention to what's happening in the AI space. AI agents have caught my attention and I've been studying and experimenting the topic on my own</p>
<p>To me, software engineering has always been about solving problems with tech, which means understanding the what, why and how. While learning about AI agents, I kept running into one question: what actually is an agent and how is it different than an AI workflow? In this blog post I go over this distinction in a practical way</p>
<h1>What is an AI workflow?</h1>
<p>A workflow can be defined as a series of steps needed to complete a task. Adding "AI" to it generally just means an LLM is involved in at least one of those steps. A good and easy to understand example would be a support system ticket where tickets can be created and an AI workflow can summarize the ticket, check relevant documentation and draft a response.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5e95b544a12c7e6a1a6ab882/045652ea-70c7-4a31-bce9-fcec45dcf158.png" alt="Flowchart of an AI workflow for support handling. The diagram shows a top-to-bottom linear sequence: “Support Ticket Arrives,” “LLM Summarizes Ticket,” “Search Docs / Knowledge base,” “LLM Drafts Response,” “(Optional) Human Review,” and “Final Output.” Arrows connect each step, emphasizing a fixed, predefined workflow." style="display:block;margin:0 auto" />

<p><em>High-level use case diagram for "support ticket" AI workflow sample. Own creation</em></p>
<p>The important part here is that the sequence is fixed. The model adds intelligence/reasoning inside the steps, but it's not free to decide on the whole process</p>
<h1>What is an AI Agent?</h1>
<p>An AI agent goes one step further than a workflow. Instead of following a fixed sequence of steps, it is given a goal and some ability (tools) to decide what actions to take.</p>
<p>In our previous example of the support ticket workflow, with an AI agent, the agent gets into a feedback loop where the LLM can use tools, search docs or ask user for more info. Each internal loop step inside the agent also checks if current "reasoning" is enough to resolve or if need to continue with the agentic loop</p>
<img src="https://cdn.hashnode.com/uploads/covers/5e95b544a12c7e6a1a6ab882/f9c85679-9bbc-4ba0-a359-3548025eb394.png" alt="Flowchart of an AI agent handling a support request. The process starts with “Support Ticket Arrives,” then “Agent receives goal,” followed by “Decide next action.” From there, the agent can choose among three actions: “Ask human,” “Call internal tool,” or “Search docs / knowledge base.” The results are then evaluated in “Evaluate findings,” followed by a decision point labeled “Enough to resolve?” If no, the flow loops back to “Decide next action.” If yes, it proceeds to “Final Output.” The diagram emphasizes dynamic decision-making and iteration rather than a fixed linear path." style="display:block;margin:0 auto" />

<p><em>High-level use case diagram for "support ticket" using an AI agent. Own creation</em></p>
<p>The important difference here is that the agent can choose actions dynamically, like using a tool, inspecting results, and loop to refine the output and complete the task. That flexibility is what makes the agents powerful and interesting, but also significantly more complex than workflows</p>
<h1>Why people confuse them?</h1>
<p>It's easy to confuse workflows and agents because both look similar on the surface. Both use LLMs, tools, APIs and so on.</p>
<p>A workflow may already look smart enough to feel like an agent. Also, most agents are not fully autonomous either as they require a human input to start.</p>
<p>So the distinction is not really about whether AI is involved, It is more about how much freedom an AI system has to decide next step</p>
<ul>
<li><p><strong>Workflows</strong>: Has a predefined path. The system is mostly developer-controlled</p>
</li>
<li><p><strong>Agent</strong>: Has more of a dynamic path towards a goal. The system is More system-controlled</p>
</li>
</ul>
<h1>Conclusion</h1>
<p>In software engineering, we usually try to use the right tool for the right job. I think the same concept applies well here. Not every AI-powered system needs to be an agent.</p>
<p>An AI workflow is a good choice when the path is known in advance and you want something easier to understand, test and control.</p>
<p>An AI Agent makes more sense when the system needs freedom to decide what to do in order to reach a goal or objective</p>
]]></content:encoded></item><item><title><![CDATA[Lambda Managed Instances for Steady Workloads]]></title><description><![CDATA[[Summary Generated with AI] This article explores AWS Lambda Managed Instances as an alternative compute option for steady-state, high-volume serverless workloads. Using a telemetry collection pipelin]]></description><link>https://blog.lucasdev.info/lambda-managed-instances-for-steady-workloads</link><guid isPermaLink="true">https://blog.lucasdev.info/lambda-managed-instances-for-steady-workloads</guid><category><![CDATA[AWS]]></category><category><![CDATA[serverless]]></category><category><![CDATA[lambda]]></category><category><![CDATA[telemetry]]></category><dc:creator><![CDATA[Lucas Vera Toro]]></dc:creator><pubDate>Sun, 12 Apr 2026 15:30:00 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/5e95b544a12c7e6a1a6ab882/4ad0fbc7-6eba-4638-8981-eb80173bec95.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>[Summary Generated with AI] This article explores AWS Lambda Managed Instances as an alternative compute option for steady-state, high-volume serverless workloads. Using a telemetry collection pipeline as the example, it proposes an architecture where API Gateway directly enqueues events into SQS, and a Managed Instances Lambda processes messages in batches. The post walks through a small CDK-based PoC and highlights practical gotchas—VPC networking, memory-to-vCPU ratios, capacity provider association behavior, and NAT vs VPC endpoints—so you can evaluate Managed Instances with realistic constraints. It closes with a simple recommendation: keep traditional Lambda as the default for bursty/unpredictable workloads, and consider Managed Instances when your workers are effectively “always on” and you can size capacity around a steady baseline.</p>
</blockquote>
<h1>Introduction</h1>
<p>When architecting a serverless solution, one common question/concern is “Lambda is very costly at scale”. And that can be true depending on the traffic and flow of the service, and what the function is doing</p>
<p>One thing I love about lambda is that it gives you the ability to move fast and keep operational overhead at a minimum. A simple execution model, clean and easy integrations with other aws services and a powerful scaling model</p>
<p>But there are also tradeoffs that become evident when traffic grows or becomes "steady state". Resulting in a lambda function that is "always on"</p>
<p>Recently, AWS announced "Lambda Managed Instances" (<a href="https://aws.amazon.com/blogs/aws/introducing-aws-lambda-managed-instances-serverless-simplicity-with-ec2-flexibility/">Announcement Post</a>). It caught my attention because it seems to fill the gap: "steady-state" workflows with lambda's dev experience</p>
<p>It's not a replacement for traditional lambda, I see it more like "stay inside lambda flow with a capacity style compute and scaling model", which is great for high volume and steady traffic</p>
<p>In this post I analyze a concrete use case where I think lambda managed instances fits as a compute choice. I also go over a small POC where I explore this new offering using CDK to deploy it</p>
<h1>Use Case</h1>
<p>For the use case, let's dive into a "collect telemetry" scenario, where multiple client devices using one or different apps send telemetry data that can result in a lot of volume. Things like clicks, screen views, custom events, metrics and so on may be collected and sent over to a "Telemetry Ingestion" service which in turn can send the data in batches to a "Telemetry Processing" service</p>
<p>A simplified diagram for the use case is shown as follows:</p>
<img src="https://cdn.hashnode.com/uploads/covers/5e95b544a12c7e6a1a6ab882/0982a43c-8a7a-4fc6-89c3-d8b618358287.png" alt="Simple telemetry pipeline diagram: client devices (phone, tablet, laptop) collect and send data to a ‘Telemetry Ingestion’ component, which then batches the data and forwards it to a ‘Telemetry Processing’ component" style="display:block;margin:0 auto" />

<p><em>Diagram for "Telemetry Data" collection and processing use case. Own Creation</em></p>
<p>Since a single user that's navigating an app in a device can generate a lot of events, if we think at scale, the data can be quite a lot. A good candidate for using Lambda Managed Instance for this scenario could be the "Telemetry Processing":</p>
<ul>
<li><p>Would be an "always-on" service. Even if usage varies during a day, there's usually steady traffic 24/7</p>
</li>
<li><p>Spikes in traffic like marketing campaigns can be handled gracefully by lambda's capacity provider</p>
</li>
<li><p>Ingestion needs to stay thin. So an approach that accepts telemetry data fast works here. SQS can help in batching the data so that the lambda managed instance can process multiple telemetry data events at a time</p>
</li>
</ul>
<p>The combination of "steady traffic" + "high volume" + "traffic spikes" makes this a good scenario to try out lambda managed instances</p>
<h1>Architecture</h1>
<p>So as described in the previous section, for the "Telemetry Ingestion" service a thin layer would be needed in order to accept the telemetry data and batch it. For that, using API Gateway as entry point and a SQS queue to help batch the messages works like a charm. Using a direct integration between API Gateway and SQS it's possible to avoid another compute in between those two</p>
<p>For the "Telemetry Processing", since we're using an SQS queue for the ingestion, the batching is controlled nicely. A DLQ can be introduced to handle failed messages that the lambda can't process, so that those messages are not automatically lost. For the compute, it's possible to use lambda with the "managed instance" variant, as it would fit nicely in the use case and would be more cost effective at scale because the queue is frequently non-empty</p>
<img src="https://cdn.hashnode.com/uploads/covers/5e95b544a12c7e6a1a6ab882/9db56e3b-13fa-44cc-a4f8-9af0be70d78e.png" alt="Architecture flow diagram: a web/mobile client sends telemetry to an Ingestion API (API Gateway), which queues the data in an Ingestion Queue (SQS). A Process Lambda running on Managed Instances consumes the queue and processes events in batches" style="display:block;margin:0 auto" />

<p><em>Architecture Component View for the "Collect Telemetry" use case. Own Creation</em></p>
<p>Let's now see how to implement a sample POC of this architecture, focusing on the "lambda managed instance" part, which is the telemetry processing piece</p>
<h1>POC</h1>
<p>Implementation using a IaC framework like AWS CDK is straight forward. The stack is composed of one HTTP endpoint for the API Gateway, one SQS queue + DLQ and one lambda function running on Managed Instances (ec2) with a capacity provider defined</p>
<p>Some things are different when defining traditional Lambda from Managed Instances. Some gotchas I learnt during the implementation:</p>
<ul>
<li><p>Since the EC2 instances that are spun up will be inside vpc in private subnets. So all the networking configuration needs to be defined in CDK as well in order for the lambda to work properly</p>
</li>
<li><p>The memory-to-CPU ratio must match your function memory. When attaching a function to a capacity provider, a supported memory-to-CPU ratio must be chosen, which as of the writing of this article it's 2GB, 4GB or 8GB per vCPU. If the function's memory is smaller, deployment will fail</p>
</li>
<li><p>Attaching a function to a capacity provider is one-way. As of the writing of the article, it's not possible to "go back to traditional lambda" by removing the association. To go back, a new function needs to be created</p>
</li>
<li><p>For this POC, adding a NAT gateway works. However, because of pricing and cost, in a real scenario it might be better to consider VPC endpoints for SQS or cloudwatch instead.</p>
</li>
</ul>
<p>For the complete code implementation of the POC, please see the following <a href="https://github.com/LucasVera/lambda-managed-instances-poc">GitHub Repo</a></p>
<p>Some screenshots of the deployed stack:</p>
<img src="https://cdn.hashnode.com/uploads/covers/5e95b544a12c7e6a1a6ab882/78a19ece-cc35-4fc8-afc0-2b18e4726ad0.png" alt="" style="display:block;margin:0 auto" />

<p><em>Lambda with managed instance configuration. Own creation</em></p>
<img src="https://cdn.hashnode.com/uploads/covers/5e95b544a12c7e6a1a6ab882/ad1f1a43-150e-4fc2-a6cb-f4525f72e3f1.png" alt="" style="display:block;margin:0 auto" />

<p><em>EC2 instances managed by lambda and capacity provider. Own creation</em></p>
<img src="https://cdn.hashnode.com/uploads/covers/5e95b544a12c7e6a1a6ab882/8782a024-5b49-4e42-942a-6fdc1c80ba07.png" alt="" style="display:block;margin:0 auto" />

<p><em>Lambda managed instance Capacity Provider defined. Own creation</em></p>
<h1>Conclusion</h1>
<p>I think Lambda Managed Instances won't change much how people are using lambda. Traditional lambda is still the best default for unpredictable workload. But for steady-state systems where functions end up "always-on", Lambda managed instances are a great option to keep the lambda ecosystem while moving to a capacity-oriented model</p>
<p>The telemetry use case it's a good example of when lambda managed instances fits in nicely. using API gateway + SQS queue as a direct integration, we keep the telemetry ingestion simple and avoid unnecessary compute. The queue provides buffering and batching which a Lambda using "managed instances" can consume from.</p>
<p>Some gotchas I found when developing the POC for this use case are things like VPC configuration, capacity provider and lambda configuration and the fact that a function configured with a capacity provider can't "remove the capacity provider association" to go back to traditional lambda</p>
<p>If considering to implement lambda managed instances in a real production scenario, I would measure traffic to be able to take the decision based on that. some measures could be queue depth/arrival rate, processing throughput and cost under traditional lambda (optionally considering provisioned concurrency). Then do a comparision exercise using Managed Instances with a configuration sized for a steady traffic.</p>
]]></content:encoded></item><item><title><![CDATA[Never lose an event: Outbox Pattern with DynamoDb + Eventbridge Pipes]]></title><description><![CDATA[[Summary Generated with AI] This article addresses the issue of data inconsistency in distributed, event-driven systems due to dual writes, where a database update and event publication might not succeed simultaneously. It presents a fictional car wo...]]></description><link>https://blog.lucasdev.info/never-lose-an-event-outbox-pattern-with-dynamodb-eventbridge-pipes</link><guid isPermaLink="true">https://blog.lucasdev.info/never-lose-an-event-outbox-pattern-with-dynamodb-eventbridge-pipes</guid><category><![CDATA[AWS]]></category><category><![CDATA[software architecture]]></category><category><![CDATA[Cloud Computing]]></category><category><![CDATA[serverless]]></category><dc:creator><![CDATA[Lucas Vera Toro]]></dc:creator><pubDate>Fri, 14 Nov 2025 11:58:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/7hA2wqBcSF8/upload/9dd19ed39e638b1fdf7ba053c0597370.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>[Summary Generated with AI] This article addresses the issue of data inconsistency in distributed, event-driven systems due to dual writes, where a database update and event publication might not succeed simultaneously. It presents a fictional car workshop scenario to highlight this problem and introduces the Outbox Pattern as a solution. By using AWS services like DynamoDB Transactions and EventBridge Pipes, the Outbox Pattern ensures reliable messaging and data consistency. The system's resilience and maintainability are improved by atomically handling database and outbox writes and reliably delivering events using serverless components. For complete code samples, refer to the linked GitHub repository.</p>
</blockquote>
<h1 id="heading-introduction">Introduction</h1>
<p>When dealing with distributed systems, failures may happen in the communication gaps between systems. One common issue in event-driven systems is the “dual write”: update a database and then publish an event. A disruption (timeouts, retries, crashes, etc) in a part of the system will make operation results diverge, resulting in incorrect state and issues with data consistency and integrity</p>
<p>This post showcases a fictional use case for a Car Workshop which may run into this problem by using a naive approach. It shows how to introduce more confidence in the system by implementing the “Outbox Pattern” architecture pattern and shows how to implement such pattern using AWS Services such as DynamoDb Transactions and EventBridge pipes</p>
<p>For full code implementation, please <a target="_blank" href="https://github.com/LucasVera/outbox-pattern">Check the GitHub Repository</a></p>
<h1 id="heading-use-case">Use Case</h1>
<p>As a fictional example, a car workshop does repair orders for cars that come to the building. Once a repair is finished, the system must update a database with the status as <em>COMPLETED</em> and trigger an asynchronous process to send an Invoice to the customer and a notification to another section of the building</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763116486676/ea4f6b28-1cd9-4922-8765-b2042756c88a.png" alt class="image--center mx-auto" /></p>
<p><em>Component diagram of the car workshop use case “Repair Completed”. Own Creation</em></p>
<p>While the above architecture for the use case may work most times, we can see the system relies on a fragile two-part writes: One to DynamoDb to update the repair status, and the other write to publish the event to EventBridge bus. The following is an example code snippet inside the Lambda Function that showcases this:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">await</span> ddb.update({ <span class="hljs-attr">pk</span>: <span class="hljs-string">`REPAIR#<span class="hljs-subst">${id}</span>`</span>, <span class="hljs-attr">sk</span>: <span class="hljs-string">'META'</span>, <span class="hljs-attr">set</span>: { <span class="hljs-attr">status</span>: <span class="hljs-string">'COMPLETED'</span> }});
<span class="hljs-keyword">await</span> eventBridge.putEvents({ <span class="hljs-attr">detail</span>: { <span class="hljs-attr">eventType</span>: <span class="hljs-string">'RepairCompleted'</span>, <span class="hljs-attr">repairId</span>: id }});
<span class="hljs-keyword">return</span> <span class="hljs-number">200</span>;
</code></pre>
<p>Some potential problems from this code:</p>
<ul>
<li><p>Db updates successfully but the event bridge “putEvents” times out.</p>
<ul>
<li>This results in database with the Repair as completed but no event was sent for invoicing and notification</li>
</ul>
</li>
<li><p>DB updates successfully and event bridge “putEvents” also works, but Lambda crashes before responding.</p>
<ul>
<li>This will trigger a lambda retry with the same input, which will send a duplicated event for the invoicing and notification</li>
</ul>
</li>
</ul>
<h1 id="heading-outbox-pattern">Outbox Pattern</h1>
<p>In System Design, Reliable messaging is an essential part. Specially in distributed systems where multiple services are connecting and integrating with each other in a choreography-style.</p>
<p>The Outbox Pattern aims to ensure a reliable messaging between different services and components. It is specially important in scenarios where data consistency and data integrity are critical. It does so by defining a table or entity as an “<em>Outbox</em>”, which will be separated from the main data entity. It temporarily stores messaged that need to be sent to other systems.</p>
<p>By leveraging databases ACID Transactions, we can join together two or more writes in a single atomic operation, ensuring that the data is consistent and having the “Outbox” entity be in charge of the delivery of the events</p>
<p>By using this pattern, we are effectively updating the needed business entities and setting the message to be sent in one go. A separate process can then send the messages to the message broker.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763117430278/922e7956-05e6-4c1d-8648-6507529478cf.png" alt class="image--center mx-auto" /></p>
<p><em>Outbox Pattern Diagram. Taken from</em> <a target="_blank" href="https://microservices.io/patterns/data/transactional-outbox.html"><em>https://microservices.io/patterns/data/transactional-outbox.html</em></a></p>
<h1 id="heading-serverless-outbox-pattern">Serverless Outbox Pattern</h1>
<p>By leveraging Serverless services in AWS, we can propose a better architecture that gives more reliability on event delivery and data integrity. Using DynamoDb Transactions, DynamoDb Streams and EventBridge Pipes we can give more confidence to the system:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763117686803/0e948d78-6ae5-4455-b277-bf95fd5b3d52.png" alt class="image--center mx-auto" /></p>
<p><em>Component diagram of the car workshop use case “Repair Completed” implementing the “Outbox Pattern”. Own Creation</em></p>
<p>As we’re now using the “Outbox” pattern, the lambda updates the status AND writes a new entity called “Outbox” in one go, using a DynamoDb Transaction. This ensures that both writes to the database are encapsulated atomically, resulting in either both writes succeeding or none. The following is a code snippet of a sample “DynamoDb Transaction” that implements the Outbox Pattern:</p>
<pre><code class="lang-javascript">    <span class="hljs-keyword">const</span> params = {
      <span class="hljs-attr">TransactItems</span>: [
        {
          <span class="hljs-attr">Update</span>: {
            <span class="hljs-attr">TableName</span>: table,
            <span class="hljs-attr">Key</span>: {
              <span class="hljs-attr">pk</span>: marshall(<span class="hljs-string">`REPAIR#<span class="hljs-subst">${repairId}</span>`</span>),
              <span class="hljs-attr">sk</span>: marshall(<span class="hljs-string">'META'</span>),
            },
            <span class="hljs-attr">ConditionExpression</span>: <span class="hljs-string">'attribute_not_exists(#status) OR #status &lt;&gt; :done'</span>,
            <span class="hljs-attr">UpdateExpression</span>: <span class="hljs-string">'SET #status = :done, completeAt = :now'</span>,
            <span class="hljs-attr">ExpressionAttributeNames</span>: { <span class="hljs-string">'#status'</span>: <span class="hljs-string">'status'</span> },
            <span class="hljs-attr">ExpressionAttributeValues</span>: marshall({ <span class="hljs-string">':done'</span>: <span class="hljs-string">'COMPLETED'</span>, <span class="hljs-string">':now'</span>: now }),
          }
        },
        {
          <span class="hljs-attr">Put</span>: {
            <span class="hljs-attr">TableName</span>: table,
            <span class="hljs-attr">Item</span>: marshall({
              <span class="hljs-attr">pk</span>: <span class="hljs-string">`OUTBOX#<span class="hljs-subst">${eventId}</span>`</span>,
              <span class="hljs-attr">sk</span>: <span class="hljs-string">`REPAIR#<span class="hljs-subst">${repairId}</span>`</span>,
              <span class="hljs-attr">entityType</span>: <span class="hljs-string">'OUTBOX'</span>,
              <span class="hljs-attr">eventType</span>: <span class="hljs-string">'RepairCompleted'</span>,
              <span class="hljs-attr">occurredAt</span>: now,
              eventId,
              repairId,
            }),
            <span class="hljs-attr">ConditionExpression</span>: <span class="hljs-string">'attribute_not_exists(pk)'</span>
          }
        }
      ]
    };

    <span class="hljs-keyword">const</span> command = <span class="hljs-keyword">new</span> DynamoDb.TransactWriteItemsCommand(params);
    <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> dynamoDbClient.send(command);
</code></pre>
<p>By setting DynamoDb Streams, we can setup a stream on the dynamodb table that triggers on specific database writes. Using EventBridge Pipes, we can connect the DynamoDb events with an EventBridge Bus. EventBridge pipes allows us to set up filtering, which we can use to filter by “entity type” being “OUTBOX”</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1763118844750/424fcbe7-1213-49c5-9f63-8326b20cfcee.png" alt class="image--center mx-auto" /></p>
<p><em>EventBridge Pipe showing the event filtering by “OUTBOX” events. Own Creation</em></p>
<p>The event delivered to the EventBridge Bus can have the following structure, which is filtered and mapped using EventBridge Pipes:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"detail-type"</span>: <span class="hljs-string">"OutboxEvent"</span>,
  <span class="hljs-attr">"source"</span>: <span class="hljs-string">"carworkshop.outbox"</span>,
  <span class="hljs-attr">"detail"</span>: {
    <span class="hljs-attr">"eventId"</span>: <span class="hljs-string">"c8f2d7e6-1e2f-4b7b-8c41-0b2a2f1c9f92"</span>,
    <span class="hljs-attr">"eventType"</span>: <span class="hljs-string">"RepairCompleted"</span>,
    <span class="hljs-attr">"repairId"</span>: <span class="hljs-string">"R-123"</span>,
    <span class="hljs-attr">"occurredAt"</span>: <span class="hljs-string">"2025-11-12T14:00:00Z"</span>,
    <span class="hljs-attr">"source"</span>: <span class="hljs-string">"fleetfix.outbox"</span>
  }
}
</code></pre>
<p>For a complete code sample implementation, please <a target="_blank" href="https://github.com/LucasVera/outbox-pattern">Check the GitHub Repository</a></p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>One of the challenges when dealing with distributed systems is ensuring data consistency and integrity across asynchronous processes. One common issue is the “dual-write” approach where two separate writes to two separate systems may result in data discrepancies, events not sent or duplicated events</p>
<p>By implementing the “Outbox Pattern”, this article shows how to handle event-delivery that is consistent with the business data updates. By leveraging DynamoDb Transactions and EventBridge pipes, the pattern was implemented for a fictional “car workshop” use case where a repair is done and asynchronous process needs to be sent to handle invoicing and notifications</p>
<p>Incorporating the “Outbox pattern” in our architecture allows for systems that more resilient, handle failures gracefully, which ensures a more predictable and maintainable system overall</p>
<h1 id="heading-references">References</h1>
<ul>
<li><p><a target="_blank" href="https://github.com/LucasVera/outbox-pattern">Full code sample implementation in GitHub</a></p>
</li>
<li><p><a target="_blank" href="https://www.geeksforgeeks.org/system-design/outbox-pattern-for-reliable-messaging-system-design/">Outbox Pattern for Reliable Messaging - System Design</a></p>
</li>
<li><p><a target="_blank" href="https://microservices.io/patterns/data/transactional-outbox.html">Pattern: Transactional outbox</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/transaction-apis.html#transaction-apis-txwriteitems">Amazon DynamoDB Transactions: How it works</a></p>
</li>
<li><p><a target="_blank" href="https://serverlessland.com/serverless/visuals/eventbridge/understanding-eventbridge-pipes">Understanding EventBridge Pipes</a></p>
</li>
<li><p><a target="_blank" href="https://blog.murawsky.net/eventbridge-pipes-in-practice">EventBridge Pipes in Practice</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[AWS Community Day Colombia 2025: lecciones de mi charla "Lambdaless: Serverless sin Lambda"]]></title><description><![CDATA[Introducción
El pasado 28 de Junio de 2025 fue el AWS Community Day Colombia. Aceptaron mi charla “Lambdaless: Serverless sin Lambda” con la que pretendo dar un par de tips sobre arquitecturas serverless en aws sin lambdas
El evento: energía, comunid...]]></description><link>https://blog.lucasdev.info/aws-community-day-colombia-2025-lecciones-de-mi-charla-lambdaless-serverless-sin-lambda</link><guid isPermaLink="true">https://blog.lucasdev.info/aws-community-day-colombia-2025-lecciones-de-mi-charla-lambdaless-serverless-sin-lambda</guid><category><![CDATA[AWS]]></category><category><![CDATA[AWS Community Day]]></category><category><![CDATA[serverless]]></category><dc:creator><![CDATA[Lucas Vera Toro]]></dc:creator><pubDate>Fri, 22 Aug 2025 20:31:28 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1755893577702/75b6ef0f-e9d1-493c-87db-0ded65016f72.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduccion">Introducción</h1>
<p>El pasado 28 de Junio de 2025 fue el AWS Community Day Colombia. Aceptaron mi charla “Lambdaless: Serverless sin Lambda” con la que pretendo dar un par de tips sobre arquitecturas serverless en aws sin lambdas</p>
<h1 id="heading-el-evento-energia-comunidad-y-muy-buena-organizacion">El evento: energía, comunidad y muy buena organización</h1>
<p>Aunque había participado de varios AWS Summits en años pasados, este fue mi primer AWS Community day! No estaba muy seguro si sería diferente o similar a los summits, dado que es organizado por las comunidades y voluntarios.</p>
<p>El evento estuvo muy top: La logística fue fluida, los speakers e invitados estuvieron geniales y dieron swag y regalos muy geniales!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755878228822/07ba9243-ec66-40bb-b7ee-dd06c2929b1a.jpeg" alt class="image--center mx-auto" /></p>
<h1 id="heading-la-charla-patrones-lambdaless-para-aplicar-en-aws">La charla: patrones “Lambdaless” para aplicar en AWS</h1>
<p>En mi charla tuve sala llena, un público muy participativo y haciendo preguntas muy interesantes. Mostré tres patrones de arquitectura:</p>
<ul>
<li><p><strong>API Gateway —&gt; DynamoDb</strong>: CRUD rápido y barato con todas las de la ley: Autenticación, Observabilidad, Validaciones, Mappeo, entre otras</p>
</li>
<li><p><strong>Step Functions + Integraciones nativas</strong>: Orquestación de diferentes servicios en AWS para evitar el anti-patrón de “Lambdalith” o “monolitos lambda”</p>
</li>
<li><p><strong>AppSync API</strong>: Creación de GraphQL APIs que permiten crear pipeline functions reusables y con conexión directa a servicios como dynamodb, step functions, entre otros</p>
</li>
</ul>
<p>Comparé costos, tiempos/latencia y carga operativa. Al final dí unos tips para decidir “Lambda o Lambdaless”</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755894149450/418c04bd-53c2-4a55-9dc7-ac55f4cbe4a6.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755892755461/6f788374-4e3d-42a4-82cf-7f3206c89be4.jpeg" alt class="image--center mx-auto" /></p>
<h1 id="heading-lo-que-mas-resono">Lo que más resonó</h1>
<p>Creo que el mayor impacto que tuvo la charla sobre los participantes fue que vieran las diferentes decisiones o “tradeoffs” que se deben considerar a la hora de proponer una arquitectura. En especial en AWS donde hay tantas opciones diferentes y con tantas implicaciones de costos, mantenibilidad, escalabilidad, latencia, entre otros</p>
<p>Otro tema del que sentí bastante curiosidad del público fue el de definición de Infraestructura cómo código (IaC) usando AWS CDK. Durante mi charla presenté varios ejemplos de 3 casos de uso, todos mostrando código en typescript y usando AWS CDK para los despliegues. A lo mejor mi próxima charla estará enfocada en este tema en específico</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755893008832/df720f24-3652-4e25-8a86-78821bd30bae.png" alt class="image--center mx-auto" /></p>
<h1 id="heading-repositorio-de-la-charla">Repositorio de la charla</h1>
<p>El código y ejemplos mostrados durante la charla se pueden encontrar en el siguiente <a target="_blank" href="https://github.com/LucasVera/Lambdaless-APIs">Link de Github</a>. Si pruebas los patrones de mi charla, o discrepas de ellos, me encantaría leerte!</p>
<h1 id="heading-conclusiones">Conclusiones</h1>
<p>El AWS Community Day estuvo genial, pude conectar con algunos entusiastas del desarrollo, devops, cloud y aws. Fue una experiencia muy enriquecedora y la cual espero con ansias para el próximo año!</p>
<p>Mil gracias a las comunidades organizadores y voluntarios, a los sponsors, a los otros speakers y en general a los asistentes.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755893498957/14b1e82e-bf02-49aa-914d-0f2c6feed4ca.jpeg" alt class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[Automate Brand Visibility Tracking With Amazon Rekognition]]></title><description><![CDATA[[Summary Generated With AI] This article outlines the process of creating a custom logo detector for the "SG Mascot" using Amazon Rekognition's Custom Labels. By preparing a dataset of positive and negative examples, we trained a model to identify th...]]></description><link>https://blog.lucasdev.info/automate-brand-visibility-tracking-with-amazon-rekognition</link><guid isPermaLink="true">https://blog.lucasdev.info/automate-brand-visibility-tracking-with-amazon-rekognition</guid><category><![CDATA[AWS]]></category><category><![CDATA[AWS rekognition]]></category><category><![CDATA[serverless]]></category><category><![CDATA[Machine Learning]]></category><dc:creator><![CDATA[Lucas Vera Toro]]></dc:creator><pubDate>Tue, 11 Mar 2025 05:00:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1737477569519/affc66c0-e472-4efa-a907-09bf42c2b2ad.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>[Summary Generated With AI] This article outlines the process of creating a custom logo detector for the "SG Mascot" using Amazon Rekognition's Custom Labels. By preparing a dataset of positive and negative examples, we trained a model to identify the mascot in uploaded images efficiently. The solution leverages AWS services like Lambda, S3, and DynamoDB, creating a serverless architecture for scalable image analysis. Despite the tool's ease of use, the article highlights the potential cost implications and emphasizes the importance of stopping the model when not in use to minimize charges.</p>
</blockquote>
<h1 id="heading-introduction">Introduction</h1>
<p>Creating an identity for a brand is an important part of a business strategy. You want your brand to be easily recognized by others. At <a target="_blank" href="https://sls.guru">Serverless Guru</a> we have an awesome and good-looking brand identity. One key component of that brand identity is the “SG Mascot”, which is easily recognizable and is visually appealing. It has a unique style in its drawing and colors</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737467323879/1725ae86-0a7c-4b12-b71a-11a941470f1e.png" alt="Illustration of a cloud-shaped character using a computer keyboard and mouse. The character is surrounded by abstract shapes, a floating lock connected to the cloud, and other colorful elements on a blue and white background." class="image--center mx-auto" /></p>
<p><em>Image of the “SG Mascot”. Taken from https://sls.guru</em></p>
<p>The purpose of this blog post is to showcase a “custom logo detector”, that is capable of analyzing photos uploaded by people, reviewing those images and detecting if the “SG Mascot” is present in the image or not.</p>
<p>Imagine hosting a global event where hundreds of attendees send pictures of the event through a web application. The system will identify the ones that match the “SG Mascot” and offer, for example, some sort of ticket to a raffle or something similar. While it can be done manually and review each image one by one, it wouldn’t be such a scalable process. And since we have access to so many amazing and easy-to-use AI tools, why not automate the process?</p>
<p>After a bit of research, I found <a target="_blank" href="https://docs.aws.amazon.com/rekognition/latest/customlabels-dg/what-is.html">Rekognition Custom Labels</a>. Since I have used Amazon Rekognition a few times in the past, I’m more or less familiar with the interface. This API allows you to identify different custom objects based on your own specific needs. All you need is a dataset to train the custom labels you want and then do inference on the resulting model!</p>
<h1 id="heading-model-training">Model Training</h1>
<h3 id="heading-preparing-the-dataset">Preparing the Dataset</h3>
<p>First, to train a good model that can detect the mascot accurately, we need a good dataset of images. Since this is a classification model, it’s enough to know if the SG Mascot is detected in a picture or not. So we need images that contain the mascot but also images that don’t contain the mascot to teach the model what not to detect</p>
<ul>
<li><p><strong>Positive Examples</strong>: Images with the mascot</p>
</li>
<li><p><strong>Negative Examples</strong>: Images without the mascot</p>
</li>
</ul>
<p>For the “Negative Examples”, I recurred to the <a target="_blank" href="https://cocodataset.org">COCO Dataset</a>, which is a large, open dataset of images for machine-learning purposes. “<em>COCO is a large-scale object detection, segmentation, and captioning dataset.</em>“</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737469871742/466d5686-f788-4590-8087-7f309f2dd1f0.png" alt="A grid of thumbnail images displayed on a screen, each with a file name underneath, showing various subjects like landscapes, animals, and people." class="image--center mx-auto" /></p>
<p><em>Sample images from the COCO dataset. Images taken from</em> <a target="_blank" href="https://cocodataset.org"><em>https://cocodataset.org</em></a></p>
<p>For the “Positive Examples”, I manually gathered some images that I found for the SG Mascot. With this approach, I could only gather 13 different images of the “SG Mascot”. Since this is not enough to build a good model, I recurred to augment the dataset by generating new images from the exiting images. I did so by doing the following</p>
<ul>
<li><p>Rotating images</p>
</li>
<li><p>Adjusting brightness and contrast</p>
</li>
<li><p>Scaling the mascot to smaller or larger sizes</p>
</li>
</ul>
<p>This will give a larger set of images for the “Positive” scenarios, resulting in a model that’s more robust and able to identify the SG mascot under different conditions.</p>
<p>After a bit of help from chatGPT, I executed the following script which was suggested by it:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> sharp = <span class="hljs-built_in">require</span>(<span class="hljs-string">'sharp'</span>);
<span class="hljs-keyword">const</span> fs = <span class="hljs-built_in">require</span>(<span class="hljs-string">'fs'</span>);
<span class="hljs-keyword">const</span> path = <span class="hljs-built_in">require</span>(<span class="hljs-string">'path'</span>);

<span class="hljs-keyword">const</span> inputDir = <span class="hljs-string">'./positive_images'</span>;
<span class="hljs-keyword">const</span> outputDir = <span class="hljs-string">'./augmented_images'</span>;

<span class="hljs-comment">// Ensure output directory exists</span>
<span class="hljs-keyword">if</span> (!fs.existsSync(outputDir)) {
    fs.mkdirSync(outputDir, { <span class="hljs-attr">recursive</span>: <span class="hljs-literal">true</span> });
}

<span class="hljs-comment">// Augmentation settings</span>
<span class="hljs-keyword">const</span> rotations = [<span class="hljs-number">15</span>, <span class="hljs-number">45</span>, <span class="hljs-number">90</span>, <span class="hljs-number">180</span>]; <span class="hljs-comment">// Degrees to rotate</span>
<span class="hljs-keyword">const</span> scales = [<span class="hljs-number">0.5</span>, <span class="hljs-number">0.8</span>, <span class="hljs-number">1.2</span>, <span class="hljs-number">1.5</span>]; <span class="hljs-comment">// Scale factors</span>
<span class="hljs-keyword">const</span> brightnessFactors = [<span class="hljs-number">0.5</span>, <span class="hljs-number">1.5</span>]; <span class="hljs-comment">// Brightness adjustments</span>

<span class="hljs-comment">// Augment a single image</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">augmentImage</span>(<span class="hljs-params">filePath, fileName</span>) </span>{
    <span class="hljs-keyword">const</span> img = sharp(filePath);

    <span class="hljs-comment">// Apply rotations</span>
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> angle <span class="hljs-keyword">of</span> rotations) {
        <span class="hljs-keyword">const</span> outputPath = path.join(outputDir, <span class="hljs-string">`rotated_<span class="hljs-subst">${angle}</span>_<span class="hljs-subst">${fileName}</span>`</span>);
        <span class="hljs-keyword">await</span> img.rotate(angle).toFile(outputPath);
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Generated: <span class="hljs-subst">${outputPath}</span>`</span>);
    }

    <span class="hljs-comment">// Apply scaling</span>
    <span class="hljs-keyword">const</span> metadata = <span class="hljs-keyword">await</span> img.metadata();
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> scale <span class="hljs-keyword">of</span> scales) {
        <span class="hljs-keyword">const</span> width = <span class="hljs-built_in">Math</span>.round(metadata.width * scale);
        <span class="hljs-keyword">const</span> height = <span class="hljs-built_in">Math</span>.round(metadata.height * scale);
        <span class="hljs-keyword">const</span> outputPath = path.join(outputDir, <span class="hljs-string">`scaled_<span class="hljs-subst">${scale}</span>_<span class="hljs-subst">${fileName}</span>`</span>);
        <span class="hljs-keyword">await</span> img.resize(width, height).toFile(outputPath);
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Generated: <span class="hljs-subst">${outputPath}</span>`</span>);
    }

    <span class="hljs-comment">// Adjust brightness</span>
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> factor <span class="hljs-keyword">of</span> brightnessFactors) {
        <span class="hljs-keyword">const</span> outputPath = path.join(outputDir, <span class="hljs-string">`brightness_<span class="hljs-subst">${factor}</span>_<span class="hljs-subst">${fileName}</span>`</span>);
        <span class="hljs-keyword">await</span> img.modulate({ <span class="hljs-attr">brightness</span>: factor }).toFile(outputPath);
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Generated: <span class="hljs-subst">${outputPath}</span>`</span>);
    }
}

<span class="hljs-comment">// Process all images in the input directory</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">processImages</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> files = fs.readdirSync(inputDir);
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> file <span class="hljs-keyword">of</span> files) {
        <span class="hljs-keyword">const</span> filePath = path.join(inputDir, file);
        <span class="hljs-keyword">const</span> fileName = path.basename(file);
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Processing: <span class="hljs-subst">${fileName}</span>`</span>);
        <span class="hljs-keyword">await</span> augmentImage(filePath, fileName);
    }
}

processImages()
    .then(<span class="hljs-function">() =&gt;</span> <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Dataset augmentation completed.'</span>))
    .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error during augmentation:'</span>, err));
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737470118624/292decea-a482-40eb-aef2-222c0b465593.png" alt="A grid of cartoon mascots, each displayed with varying brightness and rotations. The images are labeled with different filenames indicating adjustments like &quot;brightness_0.5&quot; and &quot;brightness_1.5&quot; alongside the word &quot;mascot&quot; and different numbers. The mascots are colorful and situated on blue backgrounds, some appear on white backgrounds with playful, abstract designs." class="image--center mx-auto" /></p>
<p><em>Results of executing the “augment images” script. Original Images taken from</em> <a target="_blank" href="https://sls.guru"><em>https://sls.guru</em></a></p>
<p>Now the “Positive” dataset contains 130 images - way better than the previous 13 images. With this plus 130 “Negative” images taken from the COCO dataset, it’s time to train the model!</p>
<h3 id="heading-training-the-model-in-rekognition">Training the Model in Rekognition</h3>
<p>First thing we need to do is upload the dataset images to an S3 bucket. We can create a new bucket and add two folders: “<em>Positive”</em> and “<em>Negative”.</em> After uploading the respective images to each folder in the bucket, we go into the Rekognition console, “Use Custom Labels” and create a project. Then, after generating the project, we create a dataset and select “<em>Import images from S3 bucket</em>”. Also we need to make sure the “<em>Automatically assign image-level labels to images</em>” is checked. This will make the labeling automated</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737471000770/f8e1cb45-e7a6-436c-b5ae-d8b0483aeaba.png" alt="A grid of six images with text labels. Top three images: a group of people on a baseball field, a kitchen interior, and a man looking at a game console screen. Bottom three images: cartoon cloud mascots with a parachute, reading books, and blowing bubbles. Each image has a label marked &quot;Negative&quot; or &quot;Positive.&quot;" class="image--center mx-auto" /></p>
<p><em>Dataset of images created and uploaded to the Rekognition Custom Labels. Original SG Mascot Images taken from</em> <a target="_blank" href="https://sls.guru"><em>https://sls.guru</em></a></p>
<p>Notice that the images were automatically labeled based on the folder name that we specified in the S3 bucket. That’s great as doing this process manually it’s quite tedious and time-consuming!</p>
<p>After having the dataset labeled, the project it’s ready to train the model! all we need to do is click on “Train model” and let it train for some time. In my case, it took approximately 45 minutes</p>
<p>After the training is done, we need to “start” the model. To do this, in the console, we go to the “Use model” tab in the results of the training, then click on “Start”. And that’s it! we’re ready to start using the model to detect the SG mascot!</p>
<h1 id="heading-using-the-model">Using the Model</h1>
<h3 id="heading-architecture">Architecture</h3>
<p>For our use case, we can design a simple service that leverages Event-Driven architecture and Serverless services to use the model we just trained. For this, the process starts with a user uploading a picture to an S3 bucket (it can be through a web application). The S3 bucket triggers a lambda function whenever an image gets uploaded. The Lambda function can then call the Amazon Rekognition “Detect Custom Labels” using the model we just trained, then store the results in a database like a DynamoDb Table:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737471419207/632a5f3a-c35b-450c-8475-895693ce23d0.png" alt="Diagram illustrating a process flow for image recognition. A user takes a photo of a mascot and uploads it to an &quot;Images Bucket.&quot; This triggers a &quot;Recognize Image Lambda&quot; function, which makes an API call to &quot;Rekognition&quot; to detect custom labels. The results are stored in an &quot;Analysis Results Table.&quot;" class="image--center mx-auto" /></p>
<p><em>Image of the backend architecture for detecting the SG Mascot in images. Own Creation</em></p>
<p>For implementing this architecture we can leverage the AWS CDK, which allows us to define these resources as typescript code. Check out the full code in my <a target="_blank" href="https://github.com/LucasVera/mascot-rekognition">GitHub Repository</a></p>
<h3 id="heading-testing">Testing</h3>
<p>Since the project is created with the AWS CDK, we can easily create and connect these resources in the cloud using the commands “<em>cdk bootstrap</em>“ and “<em>cdk deploy</em>“.</p>
<p>After deploying the resources, we can upload images to the bucket manually and check the results. It seems it’s working as expected:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737473160493/1b9dec2c-d78b-457d-aa26-8b8995b5cc0e.png" alt="A JSON response containing metadata and custom labels. The metadata includes an HTTP status code of 200 and a request ID. Under &quot;CustomLabels,&quot; there's a label named &quot;Positive&quot; with a confidence value of 98.09." class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737473180466/c672cb97-ebd4-4f14-8191-99a2f60b4689.png" alt="JSON output with metadata indicating an HTTP status code of 200, a request ID, and one custom label named &quot;Negative&quot; with a confidence score of 98.27." class="image--center mx-auto" /></p>
<p><em>Cloudwatch logs for a “Positive” and a “Negative” image. Own creation</em></p>
<h3 id="heading-cost">Cost</h3>
<p>If you’re following this blog post, it’s important to note the cost of the Amazon Rekognition for detecting custom labels. It’s amazingly easy and simple to use, however the cost it’s significant even for a short POC like the one presented in this blog post.</p>
<p>For this blog post POC, I tested the model for a little while. Once the model is turned-on (hitting that “Start” button on the project), it starts to charge per minute, so even using it for only a small amount of time the bill resulted in 8.91 USD</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1737473497248/0fc52f95-8f1c-47ad-8110-5fb14dad7c7e.png" alt="A screenshot of Amazon Web Services billing details for Rekognition in the US East (N. Virginia) region, totaling USD 8.91. It includes USD 8.74 for inference minutes and USD 0.17 for training minutes, with specific rates per minute displayed for each." class="image--center mx-auto" /></p>
<p><em>Snippet of the billing for Rekognition in the AWS Console for the creation of the POC presented in this blog post. Own creation</em></p>
<p>So if you’re using it to follow this tutorial, make sure to <strong><em>STOP THE MODEL</em></strong> by hitting the “Stop” button after you’re done to avoid extra unnecessary charges</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Automating the detection of a brand’s unique identity, like the “SG Mascot”, is a great example of how AI and machine learning can simplify and speed-up what can be a very tedious and manual process. Using Amazon Rekognition “Custom Labels”, this blog post covered the creation of a POC that analyses imaged uploaded by users and detects if the unique brand’s identity is found in the image, in this case, the “SG Mascot”</p>
<p>By leveraging the use of scalable and serverless services like Lambda, S3, dynamodb, we can build an cost-effective solution that integrates the components in a seamless way using the AWS CDK. While the “Custom Labels” cost can add-up with time, it’s an excellent tool for prototyping and for production systems.</p>
<h1 id="heading-references">References</h1>
<ul>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/rekognition/latest/customlabels-dg/what-is.html">Rekognition Custom Labels</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html">Getting started with AWS CDK</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/rekognition/command/DetectCustomLabelsCommand/">AWS SDK for Javascript - Detect Custom Labels</a></p>
</li>
<li><p><a target="_blank" href="https://cocodataset.org">COCO Image Datasets</a></p>
</li>
<li><p><a target="_blank" href="https://sls.guru">Serverless Guru Webpage</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/LucasVera/mascot-rekognition">Full POC Code Repository</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Offline-First Recipe Manager with AWS AppSync and Amplify]]></title><description><![CDATA[[Summary Generated with AI] This article discusses the importance of offline capabilities in mobile applications to prevent data loss due to connectivity issues. It highlights how AWS AppSync and Amplify simplify offline data synchronization, allowin...]]></description><link>https://blog.lucasdev.info/offline-first-recipe-manager-with-aws-appsync-and-amplify</link><guid isPermaLink="true">https://blog.lucasdev.info/offline-first-recipe-manager-with-aws-appsync-and-amplify</guid><category><![CDATA[AWS]]></category><category><![CDATA[aws-cdk]]></category><category><![CDATA[AppSync]]></category><category><![CDATA[DynamoDB]]></category><category><![CDATA[AWS Amplify]]></category><dc:creator><![CDATA[Lucas Vera Toro]]></dc:creator><pubDate>Mon, 03 Feb 2025 16:38:30 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/RMCHhAxXbWE/upload/8feea3ba584a17cfe8a6bf8e7ebbc21d.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>[Summary Generated with AI] This article discusses the importance of offline capabilities in mobile applications to prevent data loss due to connectivity issues. It highlights how AWS AppSync and Amplify simplify offline data synchronization, allowing developers to focus on business logic. The article provides a proof of concept for a recipe-managing app, featuring a backend using AWS AppSync, DynamoDB, and AWS CDK for Infrastructure as Code. It explains how to connect the backend to a mobile frontend using AWS Amplify's DataStore, ensuring seamless data handling when offline. The article concludes by emphasizing the benefits of an offline-first strategy for mobile apps to enhance user experience.</p>
</blockquote>
<h1 id="heading-introduction">Introduction</h1>
<p>In today’s world standards, when planning to build a mobile application, it’s essential that offline capabilities are in place. This is important in order to avoid data loss, as mobile devices often suffer from bad connectivity and internet access may be limited</p>
<p>To offer the best user experience to users, an application that supports offline capabilities seamlessly is important. As a developer, manually managing updates that are in the device vs in the database can be very difficult to handle, as local device cache vs cloud database synchronizations may result in data being overwritten, deleted or ignored if not done properly</p>
<p>This is where AppSync and Amplify come to the rescue! The offline capabilities and data synchronization is already taken care by these two technologies, allowing the developers to focus on the business logic rather than spending too much time in carefully handling offline vs online synchronizations</p>
<p>Let’s take a look at how it works with a small POC for a simple recipe-managing app:</p>
<h1 id="heading-recipe-managing-poc">Recipe Managing POC</h1>
<h3 id="heading-use-cases">Use Cases</h3>
<p>For illustrating the offline capabilities of AWS AppSync and Amplify, let’s build a simple poc app that lets a user manage recipes in a mobile device. Specifically, It allows for the two main use cases:</p>
<ul>
<li><p>Create recipes (Title, Description, List of Ingredients and Cooking Instructions)</p>
</li>
<li><p>Get Recipe</p>
</li>
</ul>
<p>A real recipe-app would have more use cases and be more robust, but for the purposes of illustrating the topic of this blog post, those should be enough</p>
<p>In order to fulfill these use cases, we can have a backend composed by a graphql AppSync API and a dynamodb table that holds the information. In the UI, we can use Amplify’s capabilities with DataStore for offline sync</p>
<h3 id="heading-architecture">Architecture</h3>
<p>The architecture of the POC developed in this blog post involves a mobile app client that is using AWS Amplify to interact with AWS services. Amplify leverages the “DataStore”, which is connected to the Recipes graphql api, which uses a DynamoDb table “Recipes Database” to store recipes data</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1738597002264/d4b7b832-9118-4b61-ba29-6676adc1b3f2.png" alt class="image--center mx-auto" /></p>
<p><em>High-level overview of the architecture for the “Recipes App” POC. Own Creation</em></p>
<p>The Data Flow for this architecture starts from the mobile app client, which can be any modern framework to build mobile apps. Using an offline-first approach, the mobile framework can leverage AWS Amplify’s “DataStore” and integrate with the “Recipes API” to have the data synchronized between the device’s offline cache and the dynamodb “Recipes Database” table</p>
<h3 id="heading-backend-implementation">Backend Implementation</h3>
<p>Implementing this small poc is easy to do with the help of AWS CDK to define our resources as Infrastructure as Code. First, we can set-up a new cdk project with the following commands (make sure cdk’s latest version is installed in your system first)</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Install CDK globally (only if needed. run "cdk --version" to check)</span>
npm install -g aws-cdk

<span class="hljs-comment"># Create a new project</span>
mkdir offline-first-recipe-manager
<span class="hljs-built_in">cd</span> offline-first-recipe-manager
cdk init app --language typescript
</code></pre>
<p>This commands will create a new CDK stack called “offline-first-recipe-manager”.</p>
<p>After the project is bootstrapped, install the libraries for our aws resources:</p>
<pre><code class="lang-bash">npm install @aws-cdk/aws-appsync @aws-cdk/aws-dynamodb
</code></pre>
<p>Also make sure you have an aws account ready to use and you have a profile configured with programmatic access in your local machine. To find out how to do it, check <a target="_blank" href="https://docs.aws.amazon.com/cdk/v2/guide/configure-access.html">this link</a></p>
<p>To check the implementation and code, please see the <a target="_blank" href="https://github.com/LucasVera/offline-first-appsync-poc">GitHub Repository</a></p>
<h3 id="heading-connect-the-backend-to-our-mobile-frontend">Connect the Backend to our mobile Frontend</h3>
<p>Now that we have a backend poc ready, we can easily connect it to the mobile Frontend by using AWS Amplify’s configuration:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// If using React Native and Typescript</span>
<span class="hljs-keyword">import</span> Amplify <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-amplify'</span>;

Amplify.configure({
  aws_appsync_graphqlEndpoint: <span class="hljs-string">''</span>, <span class="hljs-comment">// replace from the CDK output</span>
  aws_appsync_region: <span class="hljs-string">'us-east-1'</span>,
  aws_appsync_authenticationType: <span class="hljs-string">'API_KEY'</span>,
  aws_appsync_apiKey: <span class="hljs-string">''</span>, <span class="hljs-comment">// replace from the CDK output</span>
});
</code></pre>
<pre><code class="lang-dart"><span class="hljs-comment">// If using Flutter and Dart, we can use the "initState" to initialize amplify:</span>

<span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> initState() {
    <span class="hljs-keyword">super</span>.initState();
    _configureAmplify();
  }

  <span class="hljs-comment">// Configure Amplify with the DataStore plugin</span>
  Future&lt;<span class="hljs-keyword">void</span>&gt; _configureAmplify() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> dataStorePlugin =
        AmplifyDataStore(modelProvider: ModelProvider.instance);
    <span class="hljs-keyword">await</span> Amplify.addPlugin(dataStorePlugin);
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> Amplify.configure(amplifyconfig);
      setState(() {
        _isAmplifyConfigured = <span class="hljs-keyword">true</span>;
      });
    } <span class="hljs-keyword">catch</span> (e) {
      <span class="hljs-built_in">print</span>(<span class="hljs-string">'Amplify configuration failed: <span class="hljs-subst">$e</span>'</span>);
    }
  }
</code></pre>
<p>Using the AWS Amplify’s DataStore is also easy to do. Using the SDK for the appropriate framework, we can use it to have an offline-first application:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// If using React Native and Typescript</span>
<span class="hljs-keyword">import</span> { DataStore } <span class="hljs-keyword">from</span> <span class="hljs-string">'aws-amplify'</span>

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fetchRecipes</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> recipes = <span class="hljs-keyword">await</span> DataStore.query(Recipe)
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Fetched recipes:'</span>, recipes)
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error fetching recipes:'</span>, error)
  }
}
</code></pre>
<pre><code class="lang-dart"><span class="hljs-comment">// If using Flutter and Dart</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:amplify_flutter/amplify_flutter.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:amplify_datastore/amplify_datastore.dart'</span>;

<span class="hljs-comment">// ...</span>
  <span class="hljs-keyword">void</span> _queryRecipes() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> recipes = <span class="hljs-keyword">await</span> Amplify.DataStore.query(Recipe.classType);
      recipes.forEach((recipe) {
        <span class="hljs-built_in">print</span>(<span class="hljs-string">"Recipe: <span class="hljs-subst">${recipe.title}</span>"</span>);
      });
    } <span class="hljs-keyword">catch</span> (e) {
      <span class="hljs-built_in">print</span>(<span class="hljs-string">"Error querying recipes: <span class="hljs-subst">$e</span>"</span>);
    }
  }
</code></pre>
<p>By using AWS Amplify’s DataStore for fetching and saving the data instead of calling the API directly, we leave the data sync to Amplify. Even if the device is offline, the cache will store and sync later when connectivity is back</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Building applications that handle data is a common day-to-day requirement. Thinking about mobile devices, where the connectivity can fail at times, we need to think about offline-first approach to data handling, which involves having data stored in the mobile device’s internal cache, and then synchronizing with the database (system of record)</p>
<p>Manually building this synchronization can be a very complex task. Luckily, AWS Amplify’s DataStore and AWS AppSync offer a seamless way to build these kind of offline-first apps. In this blog post I went over a simple POC for handling cooking recipes from a mobile device using this offline-first approach</p>
<p>With technologies such as AWS CDK, AppSync and DynamoDb, building the backend was simple and fast, while integrating with a mobile app was also simple by using Amplify’s configuration and SDK capabilities.</p>
<p>By using an offline-first strategy in your app you’re future-proofing your app and providing real value to users who may have spotty WiFi or internet connection, which may be common. Check out the GitHub Repository for the implementation and experiment with these technologies for you next project. Happy Coding!</p>
<h1 id="heading-references">References</h1>
<ul>
<li><p><a target="_blank" href="https://github.com/LucasVera/offline-first-appsync-poc">Recipes POC Github Repository</a></p>
</li>
<li><p><a target="_blank" href="https://docs.amplify.aws/gen1/javascript/build-a-backend/more-features/datastore/set-up-datastore/">Amplify Docs - Set Up Amplify DataStore</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/cdk/v2/guide/configure-access.html">Configure security credentials for the AWS CDK CLI</a></p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/blogs/aws/amplify-datastore-simplify-development-of-offline-apps-with-graphql/">AWS Blog - Amplify DataStore + GraphQL</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Short story generator with AWS Bedrock and Amplify]]></title><description><![CDATA[Introduction
Recently, I got curious about how easy it is nowadays to build an AI-powered app. So, I decided to give it a shot and was blown away by how easy and fast it was.
Given the some recent AWSome pre:invent announcements, I saw that the ampli...]]></description><link>https://blog.lucasdev.info/short-story-generator-with-aws-bedrock-and-amplify</link><guid isPermaLink="true">https://blog.lucasdev.info/short-story-generator-with-aws-bedrock-and-amplify</guid><category><![CDATA[AWS]]></category><category><![CDATA[AI]]></category><category><![CDATA[AWS Amplify]]></category><category><![CDATA[Amazon Bedrock]]></category><category><![CDATA[serverless]]></category><dc:creator><![CDATA[Lucas Vera Toro]]></dc:creator><pubDate>Mon, 30 Dec 2024 05:00:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/6jYoil2GhVk/upload/f22b787d2fcfe9fc93d3bc354666245b.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>Recently, I got curious about how easy it is nowadays to build an <strong>AI-powered app</strong>. So, I decided to give it a shot and was blown away by how easy and fast it was.</p>
<p>Given the some recent <strong>AWSome pre:invent</strong> announcements, I saw that the amplify team released an <a target="_blank" href="https://aws.amazon.com/blogs/mobile/build-fullstack-ai-apps-in-minutes-with-the-new-amplify-ai-kit/">Amplify AI Kit</a> for developers to build full-stack, ai-powered apps.</p>
<p>So, I follo<a target="_blank" href="https://aws.amazon.com/blogs/mobile/build-fullstack-ai-apps-in-minutes-with-the-new-amplify-ai-kit/">wed the tutori</a>al in the aws documentation for a recipes assistant. I just wanted to see how easy <a target="_blank" href="https://aws.amazon.com/blogs/mobile/build-fullstack-ai-apps-in-minutes-with-the-new-amplify-ai-kit/">and fast it wa</a>s to have something up and running. My <strong>aim was to create a story-telling app</strong> that could take different inputs for the story, like genre, tone and styl<a target="_blank" href="https://aws.amazon.com/blogs/mobile/build-fullstack-ai-apps-in-minutes-with-the-new-amplify-ai-kit/">e.</a></p>
<h1 id="heading-implementahttpsawsamazoncomblogsmobilebuild-fullstack-ai-apps-in-minutes-with-the-new-amplify-ai-kittion"><a target="_blank" href="https://aws.amazon.com/blogs/mobile/build-fullstack-ai-apps-in-minutes-with-the-new-amplify-ai-kit/">Implementa</a>tion</h1>
<p>After approximately 40 minutes of implementing the tutorial (I like to read first and type the code instead of copy/pasting), I already <strong>had a simple poc ready</strong>, with authentication, somewhat nice views and inference for creating cooking recipes!</p>
<p>After that it was a matter of playing around with what the app would do. I added a couple of selects with options for the user to generate different kinds of stories:</p>
<ul>
<li><p><strong>Genre</strong>: Sci-fi, Mystery, Fantasy, Romance, Horror</p>
</li>
<li><p><strong>Tone</strong>: Light, Humorous, Dark, Adventorous</p>
</li>
<li><p><strong>Style</strong>: First Person, Third Person, Fairy Tale, Poetic</p>
</li>
</ul>
<p>After that, changed the definition in the amplify <em>data’s resources</em>, which defines cloud resources that will be used. So three things mainly changed: the arguments, returns and the system prompt:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> schema = a.schema({
  generateShortStory: a.generation({
    aiModel: a.ai.model(<span class="hljs-string">'Claude 3.5 Sonnet'</span>),
    systemPrompt: <span class="hljs-string">"You are a story-teller that generates short, engaging stories based on user-provided themes or genres. Each story should have a clear structure: an introduction to set the stage, a middle to develop the plot, a twist to surprise the reader, and a satisfying ending. The characters should be fictional, with at least one relatable protagonist. Adapt the tone and style based on user input. Ensure the story is concise and self-contained."</span>,
  })
    .arguments({
      genre: a.string(),
      tone: a.string(),
      style: a.string(),
    })
    .returns(
      a.customType({
        numberOfParagraphs: a.integer(),
        story: a.string(),
      })
    )
    .authorization(<span class="hljs-function">(<span class="hljs-params">allow</span>) =&gt;</span> allow.authenticated()),
})
</code></pre>
<p>After making this change and saving, while still having active the amplify sandbox environment (<em>npx ampx sandbox</em>), the cloud resources would <strong>compile and resources would be updated</strong> to reflect these changes! Just need to wait for a minute or two and it’s done!</p>
<p>After making a couple of minor tweaks to the UI, so the page looks a bit nicer, the end result was the following</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732743361700/fa0e9822-8298-4aa0-a9e6-d7386d4a76d2.png" alt="Screenshot of a &quot;Short Story Generator POC&quot; with options for genre, tone, and style. The selected options are Sci-fi, Dark, and Third Person. A generated story is displayed about a child named Echo discovering a sound-emitting artifact in a silent village surrounded by misty mountains. The story unfolds with the village experiencing sound for the first time, leading to chaos and awe." class="image--center mx-auto" /></p>
<p><em>Image that shows the view of the completed story-telling POC app. Own creation</em></p>
<p>Pretty good considering it took me only like a <strong>couple of hours</strong> of experimentation!</p>
<p>Changing the AI model is very easy as well, as I was experimenting with different Claude models. Ultimately went with the <em>Claude Sonnet 3.5</em></p>
<h1 id="heading-architecture">Architecture</h1>
<p>So, what’s under the hood? When inspecting in the AWS console about what resources are spinned up, we realize it’s some awesome <strong>AWS Serverless infrastructure</strong> that’s powering this AI-kit. So apps created with this AI-kit are ready to power your web apps and scale as easily as a serverless stack allows.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732743676910/5620e77b-c891-4495-8dd4-638a0deacfe3.png" alt="A diagram illustrating a system architecture where a user interacts with AWS AppSync, which communicates with Amazon Cognito for authentication, Amazon DynamoDB for data storage, AWS Lambda for processing, and Amazon Bedrock for machine learning tasks." class="image--center mx-auto" /></p>
<p><em>Image that shows the general architecture of apps created using the aws amplify’s AI kit. Taken from</em> <a target="_blank" href="https://aws.amazon.com/blogs/mobile/build-fullstack-ai-apps-in-minutes-with-the-new-amplify-ai-kit/"><em>Build fullstack AI apps in minutes with the new Amplify AI Kit</em></a></p>
<p>The im<a target="_blank" href="https://aws.amazon.com/blogs/mobile/build-fullstack-ai-apps-in-minutes-with-the-new-amplify-ai-kit/">age above is the one that powers the full AI-kit from Amplify.</a> For my simple POC app, which only uses the “generate” variant of the kit (no conversation), I d<a target="_blank" href="https://aws.amazon.com/blogs/mobile/build-fullstack-ai-apps-in-minutes-with-the-new-amplify-ai-kit/">iscovered that the stack is a bit different. It doesn’t use dy</a>namodb or lambda, instead aws <strong>appsync makes a direct HTTP request to bedrock</strong>, and the mapping template from appsync does the logic of formatting the response properly:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1732744490304/2ff9d338-9d7f-447d-a75b-47b6d0bce760.png" alt class="image--center mx-auto" /></p>
<p><em>Image that shows the architecture of the short-story teller app created in this blog post using aws amplify’s generation ai kit. Own creation</em></p>
<p><strong>Full code repository of this POC can be found here:</strong> <a target="_blank" href="https://github.com/LucasVera/story-telling-app**"><strong>https://github.com/LucasVera/story-telling-app</strong></a></p>
<h1 id="heading-conclusion"><strong>Conclusion</strong></h1>
<p>This experiment showed me just how far tools like AWS Amplify and Bedrock have come. In just a few hours I could get my app from idea to a working proof-of-concept. The best part is that it’s backed by a scalable and efficient serverless infrastructure!</p>
<p>It’s exciting to see how easy it is nowadays to experiment with AI-powered projects, and I’m looking forward to exploring and enhancing the POC even further. Even launching it may also be a good idea!</p>
<h1 id="heading-references"><strong>References</strong></h1>
<ul>
<li><p><a target="_blank" href="https://docs.amplify.aws/react/ai/set-up-ai/">https://docs.amplify.aws/react/ai/set-up-ai/</a></p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/blogs/mobile/build-fullstack-ai-apps-in-minutes-with-the-new-amplify-ai-kit/">https://aws.amazon.com/blogs/mobile/build-fullstack-ai-apps-in-minutes-with-the-new-amplify-ai-kit/</a></p>
</li>
<li><p><a target="_blank" href="https://ui.docs.amplify.aws/react/getting-started/usage">https://ui.docs.amplify.aws/react/getting-started/usage</a></p>
</li>
<li><p>POC Repo: <a target="_blank" href="https://github.com/LucasVera/story-telling-app">https://github.com/LucasVera/story-telling-app</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/what-is-appsync.html">https://docs.aws.amazon.com/appsync/latest/devguide/what-is-appsync.html</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/cognito/latest/developerguide/what-is-amazon-cognito.html">https://docs.aws.amazon.com/cognito/latest/developerguide/what-is-amazon-cognito.html</a></p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/bedrock/">https://aws.amazon.com/bedrock/</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Building a Real-Time Voting System With AWS AppSync and DynamoDb]]></title><description><![CDATA[This article presents a proof of concept for a real-time voting app facilitating SCRUM grooming sessions using AWS AppSync for a serverless GraphQL API and DynamoDB for storage. It outlines the architecture, data modeling, and implementation details,...]]></description><link>https://blog.lucasdev.info/building-a-real-time-voting-system-with-aws-appsync-and-dynamodb</link><guid isPermaLink="true">https://blog.lucasdev.info/building-a-real-time-voting-system-with-aws-appsync-and-dynamodb</guid><category><![CDATA[AWS]]></category><category><![CDATA[AppSync]]></category><category><![CDATA[DynamoDB]]></category><category><![CDATA[serverless]]></category><category><![CDATA[GraphQL]]></category><category><![CDATA[agile]]></category><dc:creator><![CDATA[Lucas Vera Toro]]></dc:creator><pubDate>Fri, 25 Oct 2024 15:00:48 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/T9CXBZLUvic/upload/f673aa21a4d6d871a138f0571f629591.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>This article presents a proof of concept for a real-time voting app facilitating SCRUM grooming sessions using AWS AppSync for a serverless GraphQL API and DynamoDB for storage. It outlines the architecture, data modeling, and implementation details, leveraging AppSync's real-time updates and DynamoDB's single-table design and TTL feature for an efficient voting system. [Summary Generated with AI]</p>
</blockquote>
<h1 id="heading-introduction">Introduction</h1>
<p>In the SCRUM agile methodology, the <strong><em>grooming sessions</em></strong> are meetings where team members get together and agree on how much effort is a particular story worth. This is measured in points. Typically, each team member would give a secret vote for the effort on a particular story, then all votes get revealed and, based on the votes, the team agrees on an effort for a story</p>
<p>In this blog post we will cover a proof of concept for a real-time app that facilitates the voting session that takes place as part of a grooming session. For this, we will use AWS technologies - specifically AWS AppSync for the backend real-time API and DynamoDb for any storage needs</p>
<h1 id="heading-use-case-and-architecture">Use Case and Architecture</h1>
<p>For the use case, we identify two main actors for the system: “<strong><em>Session Creator</em></strong>” (SC) and “<strong><em>Session Participant</em></strong>” (SP). The SC is responsible for creating a voting session, then sharing the session with other people. Then, the SPs are responsible for joining the session. After enough SPs have joined the session, the SC can start the voting session. Each SP can cast a secret and private vote. After everyone has voted, the SC can reveal the votes and that will conclude the voting session.</p>
<p>For each of the actions described, it’s important to notify all participants and the creator so that updates are visible to everyone.</p>
<p>With that use case in mind, we can lay out a general high-level overview of the system as an AWS AppSync API that allows each actor to perform operations to interact with the system, and at the same time allow for subscription to real-time events for operations made by other actors:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729792096576/670d63ca-21df-48cf-87e4-04131be4fee1.png" alt="Diagram illustrating a voting session process. A Session Creator initiates and manages sessions, while a Session Participant joins and votes. Both roles receive updates. The AppSync API connects to DynamoDB for data management." class="image--center mx-auto" /></p>
<p>The good thing is that AWS AppSync allows for easily building a serverless graphql API, which supports “subscriptions”. These subscriptions are websockets implementation which allows for server-to-client communication. So a session participant can receive events when other participants joins, vote or when a voting session starts or ends.</p>
<h1 id="heading-sample-implementation">Sample Implementation</h1>
<p>For implementing this backend we can leverage the usage of javascript and AWS AppSync resolvers to perform all interactions needed. For this API, it’s needed to interact with the dynamodb table and send message to other clients. With AppSync is easy to build an API using javascript resolvers. Interacting with the dynamodb table can be done through direct integrations between AppSync and DynamoDb (no need for lambda or other computing resource).</p>
<p>For sending real-time updates in the graphql subscription, another awesome thing about AppSync is that the heavy lifting is done by AppSync. Using configuration in the schema, we are able to connect a graphql mutation to a subscription so that everytime that mutation is requested, the response will trigger a subscription response to all susbcribed clients (SC and SPs)</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729855597178/d04be177-1ecd-4178-adf8-c6537859a0ed.png" alt="Diagram showing two participants interacting with an AppSync API. Participant A joins a session using a GraphQL mutation, and Participant B receives a notification upon joining using a GraphQL subscription." class="image--center mx-auto" /></p>
<h3 id="heading-schema">Schema</h3>
<p>For the schema we need to take into account all operations for both the SC and the SPs. Along with operations, we need to define the subscriptions so that clients can receive the real-time updates. As this is a sample implementation, for auth a simple API_KEY approach will be used however in production is recommended to set-up a more robust auth method. The following shows a sample graphql schema with the operations and the subscriptions:</p>
<pre><code class="lang-graphql"><span class="hljs-keyword">schema</span> {
  <span class="hljs-symbol">query:</span> Query
  <span class="hljs-symbol">mutation:</span> Mutation
  <span class="hljs-symbol">subscription:</span> Subscription
}

<span class="hljs-keyword">type</span> Query <span class="hljs-meta">@aws_api_key</span> {
  getSession(<span class="hljs-symbol">sessionId:</span> String!): Session
}
<span class="hljs-keyword">type</span> Mutation <span class="hljs-meta">@aws_api_key</span> {
  createSession(<span class="hljs-symbol">participant:</span> ParticipantInput!): Session
  startSession(<span class="hljs-symbol">sessionId:</span> String!): Session
  endSession(<span class="hljs-symbol">sessionId:</span> String!): Session
  addParticipant(<span class="hljs-symbol">sessionId:</span> String!, <span class="hljs-symbol">participant:</span> ParticipantInput!): Participant
  removeParticipant(<span class="hljs-symbol">sessionId:</span> String!, <span class="hljs-symbol">participantEmail:</span> String!): Participant
  updateParticipantVote(<span class="hljs-symbol">sessionId:</span> String!, <span class="hljs-symbol">participantEmail:</span> String!, <span class="hljs-symbol">vote:</span> Int!): Participant
}
<span class="hljs-keyword">type</span> Subscription <span class="hljs-meta">@aws_api_key</span> {
  onParticipantAdded(<span class="hljs-symbol">sessionId:</span> String!): Participant
    <span class="hljs-meta">@aws_subscribe</span>(<span class="hljs-symbol">mutations:</span> [<span class="hljs-string">"addParticipant"</span>])
  onParticipantRemoved(<span class="hljs-symbol">sessionId:</span> String!): Participant
    <span class="hljs-meta">@aws_subscribe</span>(<span class="hljs-symbol">mutations:</span> [<span class="hljs-string">"removeParticipant"</span>])
  onParticipantVoted(<span class="hljs-symbol">sessionId:</span> String!): Participant
    <span class="hljs-meta">@aws_subscribe</span>(<span class="hljs-symbol">mutations:</span> [<span class="hljs-string">"updateParticipantVote"</span>])
  onSessionStarted(<span class="hljs-symbol">sessionId:</span> String!): Session
    <span class="hljs-meta">@aws_subscribe</span>(<span class="hljs-symbol">mutations:</span> [<span class="hljs-string">"startSession"</span>])
  onSessionEnded(<span class="hljs-symbol">sessionId:</span> String!): Session
    <span class="hljs-meta">@aws_subscribe</span>(<span class="hljs-symbol">mutations:</span> [<span class="hljs-string">"endSession"</span>])
}
</code></pre>
<p>Notice the use of “<strong><em>@aws_subscribe</em></strong>” when defining the subscription. We specify the mutation that will be connected and that’s it! AWS AppSync takes care of the rest.</p>
<h3 id="heading-data-modeling">Data Modeling</h3>
<p>For the data modeling needs, we need to keep track of sessions created, session participants that joined a particular session and votes for a session. Using DynamoDb Single-Table Design principles we can keep track of all this information in one table. Using a unique id for each session, we can tie together sessions, participants and votes, as shown in the following image</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1729865932067/7ab935d1-db4c-4814-a1cf-6e70382d51c8.png" alt="A table displaying session details with columns for primary key, sort key, and various attributes such as session ID, creator, status, and participant information. The table includes email addresses and timestamps for creation and expiration." class="image--center mx-auto" /></p>
<p>Without going too much into details of single table design, notice that we can store different entities depending on the PK+SK combination. In this case, we identify two entities: <strong><em>Session Details</em></strong> and <strong><em>Session Participants</em></strong>.</p>
<ul>
<li><p><strong>Session Details</strong></p>
<ul>
<li><p><strong>PK</strong>: <em>SESSION#&lt;session_id&gt;</em></p>
</li>
<li><p><strong>SK</strong>: <em>SESSION#&lt;session_id&gt;#DETAILS</em></p>
</li>
</ul>
</li>
<li><p><strong>Session Participant</strong></p>
<ul>
<li><p><strong>PK</strong>: <em>SESSION#&lt;session_id&gt;</em></p>
</li>
<li><p><strong>SK</strong>: <em>SESSION#&lt;session_id&gt;#PARTICIPANT#&lt;participant_email&gt;</em></p>
</li>
</ul>
</li>
</ul>
<p>This gives us flexibility when reading information for a particular session, as we can either read all entries for a session, or read some of it depending on the query pattern we specify.</p>
<p>Another thing to highlight from the data modeling is the usage of DynamoDb TTL. This feature allows for self-cleaning of the dynamodb table of old sessions that may no longer be active. The awesome thing is that it’s done automatically by DynamoDb once the “<em>expirationTime</em>” timestamp has passed.</p>
<h3 id="heading-code">Code</h3>
<p>The following repository contains a partial implementation of this API: <a target="_blank" href="https://github.com/LucasVera/letspointit">GitHub Repository</a></p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Using AWS AppSync and DynamoDb, this proof of concept shows an simple implementation for a real-time, serverless voting system that’s useful for SCRUM grooming sessions.</p>
<p>The capabilities of AWS AppSync simplify the implementation of graphql subscriptions for the real-time updates. Using DynamoDb for data storage needs allows for scalable and performant operations while leveraging the use of single-table design for cohesive data storage and DynamoDb TTL for self-cleaning of inactive sessions.</p>
<p>This architecture allows for scalable and low-latency interactions between participants in a grooming session, resulting in effective and efficient SCRUM ceremonies.</p>
<h1 id="heading-references">References</h1>
<ul>
<li><p>SCRUM Backlog Grooming: <a target="_blank" href="https://www.scrum-institute.org/scrum-grooming-meeting-the-scrum-framework.php">Scrum Grooming Meeting: Backlog Refinement Meetings Best Practices</a></p>
</li>
<li><p>AWS AppSync:</p>
<ul>
<li><p>What is AppSync: <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/what-is-appsync.html">AWS Documentation</a></p>
</li>
<li><p>Subscriptions for Real-time updates in AppSync: <a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/aws-appsync-real-time-data.html">AWS Documentation</a></p>
</li>
</ul>
</li>
<li><p>DynamoDb:</p>
<ul>
<li><p>Main Documentation Page: <a target="_blank" href="https://docs.aws.amazon.com/dynamodb/">AWS Documentation</a></p>
</li>
<li><p>Single-Table Design: <a target="_blank" href="https://www.alexdebrie.com/posts/dynamodb-single-table/">The What, Why, and When of Single-Table Design with DynamoDB</a></p>
</li>
<li><p>No SQL Workbench for Dynamodb Data Modeling: <a target="_blank" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/workbench.html">AWS Documentation</a></p>
</li>
</ul>
</li>
<li><p>Sample code implementation: <a target="_blank" href="https://github.com/LucasVera/letspointit">GitHub Repository</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Maximize Performance with AWS AppSync Direct Integrations]]></title><description><![CDATA[[generated with AI] AWS AppSync simplifies the creation and management of GraphQL APIs by connecting data sources to resolvers through functions, enhancing front-end and back-end collaboration. This article explores the advantages of using AppSync’s ...]]></description><link>https://blog.lucasdev.info/maximize-performance-with-aws-appsync-direct-integrations</link><guid isPermaLink="true">https://blog.lucasdev.info/maximize-performance-with-aws-appsync-direct-integrations</guid><category><![CDATA[GraphQL]]></category><category><![CDATA[AppSync]]></category><category><![CDATA[AWS]]></category><category><![CDATA[DynamoDB]]></category><category><![CDATA[opensearch]]></category><category><![CDATA[performance]]></category><category><![CDATA[cost-optimisation]]></category><dc:creator><![CDATA[Lucas Vera Toro]]></dc:creator><pubDate>Tue, 24 Sep 2024 23:22:26 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1727219718030/aaa4ee77-531a-4d8d-be40-17044ab4102e.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>[generated with AI] AWS AppSync simplifies the creation and management of GraphQL APIs by connecting data sources to resolvers through functions, enhancing front-end and back-end collaboration. This article explores the advantages of using AppSync’s direct integration capabilities, showcasing examples with DynamoDB, OpenSearch, and HTTP data sources to improve performance and reduce costs by eliminating unnecessary AWS Lambda usage. Leveraging direct integrations not only optimizes operations but also simplifies architecture and promotes reusability within the API.</p>
</blockquote>
<h1 id="heading-introduction">Introduction</h1>
<p>AWS AppSync is a serverless services that makes it very easy to build and maintain GraphQL APIs. It works by connecting “data sources” (such as lambda) to the “resolvers” through “functions”. Each function has one data source associated and also executes logic.</p>
<p>With AppSync, we can define an entire API schema and business logic in a single place, allowing for front-end and back-end developers to collaborate more efficiently. The following image shows the high-level of a typical AppSync API architecture.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727196552901/a3422845-54d2-45ca-9bbe-30c59de8d2fb.webp" alt="Typical architecture for an appsync API, in the left side shows different frontend clients like mobile, web and others. In the middle it shows the AppSync API and to the right it shows data sources which the api interacts with" class="image--center mx-auto" /></p>
<p><em>Image that shows the architecture of a graphql API in AWS AppSync. Source: Own Creation</em></p>
<p>Each resolver function in an appsync API can be written in javascript or VTL (Velocity Template Language). In this blog post we’ll be focusing on javascript resolvers as they are more popular. It’s not a full-feature javascript engine, rather it’s a sub-set of javascript that allows to execute some simple logic in conjunction with the resolver’s data source execution.</p>
<p>However, It’s common to see developers defaulting to use AWS Lambda as a data source for graphql operations that may not need one. AWS Lambda is a fantastic serverless compute that uses the Functions-as-a-Service model (FaaS) to execute code. While the approach works, it introduces latency due to Lambda’s ephemeral execution environment, incurring in cold starts. It also incurs additional costs - specially in high frequency operations.</p>
<p>The following image shows an example of an AppSync’s architecture that can be improved</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1727209813632/0692d554-a9e6-4ba2-b408-3bce7077e32d.webp" alt="Image that shows two aws serverless architectures. The first one is using an appsync api which invokes a lambda and then does a call to dynamodb. the second one is from appsync api directly to dynamodb, which doesn't need the lambda and increases performance and saves costs" class="image--center mx-auto" /></p>
<p><em>Image that shows an improvement on performance and cost for appsync api that only interact with a dynamodb database. Source: Own Creation</em></p>
<p>In this blog post we’ll take a look at AppSync’s direct integration capabilities. Using direct integration may remove a lambda if it’s not needed. This results in higher performance and lower costs. It can also help simplify an architecture and at the same time, having reusable resolver functions promotes re-usability within the API.</p>
<h1 id="heading-example-1-dynamodb">Example 1: DynamoDb</h1>
<p>DynamoDb Is a serverless NoSQL database service offered by AWS. It’s main advantage is that it provides fast and predictable performance regardless of scale. DynamoDb is an ideal database choice for serverless application.</p>
<p>In data-driven APIs, the most common operations are CRUD operations: Create, Read, Update and Delete operations to a database. So using the direct integration with appsync and dynamodb is a very common use case. The following is a sample snippet of a resolver function that interacts with a dynamodb table.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">request</span> (<span class="hljs-params">ctx</span>) </span>{
  <span class="hljs-keyword">const</span> { args } = ctx;
  <span class="hljs-keyword">const</span> { blogPost } = args;

  <span class="hljs-comment">// Any validation logic or transformation/mapping logic</span>

  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">operation</span>: <span class="hljs-string">'UpdateItem'</span>,
    <span class="hljs-attr">key</span>: util.dynamodb.toMapValues({ <span class="hljs-attr">BlogPostId</span>: blogPost.id }),
    <span class="hljs-attr">update</span>: {
      <span class="hljs-attr">expression</span>: <span class="hljs-string">'SET #BlogPost = :blogPost'</span>,
      <span class="hljs-attr">expressionNames</span>: { <span class="hljs-string">'#BlogPost'</span>: <span class="hljs-string">'BlogPost'</span> },
      <span class="hljs-attr">expressionValues</span>: {
        <span class="hljs-string">':blogPost'</span>: util.dynamodb.toDynamoDB(blogPost),
      }
    },
  };
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">response</span> (<span class="hljs-params">ctx</span>) </span>{
  <span class="hljs-keyword">const</span> { result } = ctx;
  <span class="hljs-comment">// ... any transformation/mapping logic, etc</span>

  <span class="hljs-keyword">return</span> result.Attributes.BlogPost;
}
</code></pre>
<p><em>Code snippet that shows an AppSync Javascript Function Resolver which directly interacts with a dynamodb table. Source: Own Creation (</em><a target="_blank" href="https://gist.github.com/LucasVera/b9bff3a5d1f5b1a1c22b426927508b32"><em>Gist Link</em></a><em>)</em></p>
<p>Notice in the code that the resolver has capabilities of mapping, validating and even doing a bit of computation. So the most common use cases around CRUD operations can be covered in pure function resolvers without needing a lambda in between.</p>
<h1 id="heading-example-2-open-search">Example 2: Open Search</h1>
<p>Amazon Open Search is a managed service that helps run open search (and elastic search) clusters at scale. It shines in analytics, monitoring and click streams.</p>
<p>With AWS AppSync, we can use integrate directly from a resolver function to perform a query in open search, as shown in the following code snippet.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { util } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-appsync/utils'</span>;

<span class="hljs-comment">// Searches for posts in the OpenSearch index</span>
<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">request</span>(<span class="hljs-params">ctx</span>) </span>{
  <span class="hljs-keyword">const</span> { args } = ctx;
  <span class="hljs-keyword">const</span> { searchTerm } = args;

  <span class="hljs-comment">// ... Any other logic to build the query</span>

  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">operation</span>: <span class="hljs-string">'GET'</span>,
    <span class="hljs-attr">path</span>: <span class="hljs-string">'/post/_search'</span>,
    <span class="hljs-attr">params</span>: {
      <span class="hljs-attr">body</span>: {
        <span class="hljs-attr">from</span>: <span class="hljs-number">0</span>,
        <span class="hljs-attr">size</span>: <span class="hljs-number">20</span>,
        <span class="hljs-attr">query</span>: {
          <span class="hljs-attr">match</span>: {
            <span class="hljs-attr">content</span>: searchTerm || <span class="hljs-string">''</span>,
          },
        },
      },
    },
  };
}


<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">response</span>(<span class="hljs-params">ctx</span>) </span>{
  <span class="hljs-keyword">const</span> { result, error } = ctx;

  <span class="hljs-comment">// Handle errors</span>
  <span class="hljs-keyword">if</span> (error) {
    util.error(error.message, error.type);
  }

  <span class="hljs-comment">// Extract and return search hits from the response</span>
  <span class="hljs-keyword">const</span> results = result.hits.hits.map(<span class="hljs-function"><span class="hljs-params">hit</span> =&gt;</span> hit._source);

  <span class="hljs-comment">// ... Any other business logic to process the results</span>

  <span class="hljs-keyword">return</span> results;
}
</code></pre>
<p><em>Code snippet that shows an AppSync Javascript Function Resolver which directly interacts with an open search cluster. Source: Own Creation</em></p>
<p>Same as with the Example 1, notice there is room for validation/mapping/error handling in the resolver function itself. Using this direct integration method without having a lambda in between to execute complex search queries will likely saves costs and increase performance.</p>
<h1 id="heading-example-3-http-call">Example 3: HTTP call</h1>
<p>There is a Data Source for AppSync called <code>HTTP</code>. With this Data Source, we specify an endpoint, then in the resolver we can include query parameters, body, path, HTTP method and headers. This is useful when we’re fetching data from third-party APIs, microservices or even other AWS resources that don’t have a direct integration.</p>
<p>The following shows an example of an external API that verifies the validity of an email from the input.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { util } <span class="hljs-keyword">from</span> <span class="hljs-string">'@aws-appsync/utils'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">request</span> (<span class="hljs-params">ctx</span>) </span>{
  <span class="hljs-keyword">const</span> { args, <span class="hljs-attr">env</span>: { VERIFY_EMAIL_API_KEY = <span class="hljs-literal">null</span> } } = ctx;

  <span class="hljs-keyword">if</span> (!VERIFY_EMAIL_API_KEY) util.error(<span class="hljs-string">'No email verification api key provided'</span>, <span class="hljs-string">'ValidateEmailError'</span>, <span class="hljs-literal">null</span>, <span class="hljs-literal">null</span>);

  <span class="hljs-comment">// ... any other additional logic for validation/mapping/preparation</span>

  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">method</span>: <span class="hljs-string">'GET'</span>,
    <span class="hljs-attr">params</span>: {
      <span class="hljs-attr">query</span>: {
        <span class="hljs-attr">email</span>: args.email,
        <span class="hljs-attr">apikey</span>: VERIFY_EMAIL_API_KEY,
      }
    },
    <span class="hljs-attr">resourcePath</span>: <span class="hljs-string">`/verify-email`</span>
  };
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">response</span> (<span class="hljs-params">ctx</span>) </span>{
  <span class="hljs-keyword">const</span> {
    result,
    error,
  } = ctx;

  <span class="hljs-keyword">if</span> (error) {
    <span class="hljs-comment">// error handling</span>
  }

  <span class="hljs-keyword">const</span> { body } = result;
  <span class="hljs-keyword">if</span> (!body) util.error(<span class="hljs-string">"Email is not valid"</span>, <span class="hljs-string">'ValidateEmailError'</span>, <span class="hljs-literal">null</span>, <span class="hljs-literal">null</span>);

  <span class="hljs-comment">// Any additional logic or checks</span>

  <span class="hljs-keyword">return</span> {
    <span class="hljs-attr">isValid</span>: <span class="hljs-literal">true</span>,
  };
}
</code></pre>
<p><em>Code snippet that shows an AppSync Javascript Function Resolver which directly interacts with a verification email third party endpoint. Source: Own Creation</em></p>
<p>Notice in the code snippet that we can specify different details like the HTTP method <code>GET</code>, query parameters and path.</p>
<p>It is also possible to interact with other AWS resources that don’t have a direct integration with AppSync using this “HTTP Data Source”. For example it’s possible to add a message to an SQS queue using the direct integration with HTTP. AppSync will sign the request of the HTTP method using the current AppSync execution role.</p>
<h1 id="heading-summary">Summary</h1>
<p>We explored how can AppSync integrate with services such as DynamoDb and OpenSearch, and even with HTTP endpoints. These direct integrations, compared with an architecture that involves using lambdas, allow to reduce costs and enhance performance. It can also simplify the architecture of the GraphQL API. This approach also promotes reusability as the resolvers may be used in other graphql operations.</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Evaluate your current serverless architecture and identify opportunities to leverage AppSync direct integrations</div>
</div>

<h1 id="heading-references">References</h1>
<ul>
<li><p>AWS API Documentation:</p>
<ul>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/supported-features.html">AppSync Javascript’s supported features</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/js-resolver-reference-dynamodb.html">DynamoDb Direct Integration</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-elasticsearch-js.html">OpenSearch Direct Integration</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/appsync/latest/devguide/resolver-reference-http-js.html">HTTP Direct Integration</a></p>
</li>
</ul>
</li>
<li><p><a target="_blank" href="https://lhidalgo.dev/getting-started-with-appsync-javascript-resolvers-basic-setup">Get Started With AppSync Javascript Resolvers</a></p>
</li>
<li><p><a target="_blank" href="https://blog.graphbolt.dev/aws-appsync-best-practices">AppSync Best Practices</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Tips For Becoming A Better Developer]]></title><description><![CDATA[Introduction
Earlier this year I discovered the book 97 Things Every Programmer Should Know. It's a book which has a bunch of amazing wisdom regarding software engineering. I didn't go one by one in order but jumped around in between the items, and c...]]></description><link>https://blog.lucasdev.info/tips-for-becoming-a-better-developer</link><guid isPermaLink="true">https://blog.lucasdev.info/tips-for-becoming-a-better-developer</guid><category><![CDATA[software development]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[refactoring]]></category><category><![CDATA[team collaboration]]></category><category><![CDATA[teamwork]]></category><category><![CDATA[user experience]]></category><category><![CDATA[tips]]></category><category><![CDATA[error handling]]></category><dc:creator><![CDATA[Lucas Vera Toro]]></dc:creator><pubDate>Mon, 09 Sep 2024 04:02:34 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1725854107930/425ca6a3-c856-489a-98d9-b6541e470c6b.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>Earlier this year I discovered the book <a target="_blank" href="https://www.oreilly.com/pub/pr/2499">97 Things Every Programmer Should Know</a>. It's a book which has a bunch of amazing wisdom regarding software engineering. I didn't go one by one in order but jumped around in between the items, and could relate through most of them.</p>
<p>It reminded me of many times in my career where I didn't follow these concepts. There was a learning in those times and some items described in this book resonated with me immediately.</p>
<p>As software developers, sometimes we think that getting better as a developer and advancing in our careers involves technical expertise. So we get obsessed with learning that new framework, new technology, etc. But in my experience, while being technically proficient is important, the non-technical aspects are what allow a developer from going to "good" to "amazing" developer.</p>
<p>I recently gave a talk to my dev team and shared three concepts from the book. While I haven't read all the items, I shared the three that resonated with me the most so far. In this blog post I wanted to share them because I think they help developers improve.</p>
<h1 id="heading-1-what-would-the-user-do">1. What Would the User Do</h1>
<blockquote>
<p><strong><em>By Giles Colborne</em></strong></p>
</blockquote>
<p>This concept highlights that we, as software developers, are not the users. Users of the software have different mental models and problem-solving approaches. Sometimes we create assumptions based on user's input or actions, or assume that the users will understand and use the software.</p>
<p>Observing users is really insightful as well. Using observability and analytics tools that help gather what users do when using the software is really helpful in understanding what are the pain points users face.</p>
<p>One example that I think it's easy to relate to is user input. Sometimes when dealing with user input we, as software developers, assume that the formatting would be as we expected and programmed. However, as mentioned in this concept, "we are not the users". That means we need to think about the different possibilities for the user input.</p>
<p>In the following example I present a function that's receiving a phone number from the user. The function assumes the format to be in a certain way. However, users may be able to send a phone number in different ways:</p>
<h3 id="heading-without-the-concept">Without the concept</h3>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">validatePhoneNumber</span> (<span class="hljs-params">phoneNumber</span>) </span>{
  <span class="hljs-comment">// Assume users will enter phone numbers in the '123-456-7890' format</span>
  <span class="hljs-keyword">const</span> regex = <span class="hljs-regexp">/^\d{3}-\d{3}-\d{4}$/</span>;
  <span class="hljs-keyword">return</span> regex.test(phoneNumber);
}

<span class="hljs-comment">// Example usage</span>
<span class="hljs-keyword">const</span> userInput = <span class="hljs-string">'123-456-7890'</span>;
<span class="hljs-keyword">const</span> isValid = validatePhoneNumber(userInput); <span class="hljs-comment">// Worksfor the expected format only</span>
</code></pre>
<p>In this example we see a simple function <code>validatePhoneNumber</code>, which validates if a phone number is valid or not. The function expects the format <code>123-456-7890</code> for phone numbers. But if we ask the question <em>¿What would the user do?</em>, the answer is that the user will probably use other types of inputs:</p>
<h3 id="heading-applying-the-concept">Applying the concept</h3>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">validatePhoneNumber</span> (<span class="hljs-params">phoneNumber</span>) </span>{
  <span class="hljs-keyword">const</span> regex = <span class="hljs-regexp">/^\d{3}-\d{3}-\d{4}$/</span>;
  <span class="hljs-keyword">if</span> (!regex.test(phoneNumber)) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Invalid phone number format. Please use the format: 123-456-7890'</span>);
  }
  <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
}

<span class="hljs-comment">// Example usage</span>
<span class="hljs-keyword">const</span> input = <span class="hljs-string">'(123) 456-7890'</span>;
<span class="hljs-keyword">try</span> {
  validatePhoneNumber(input);
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Phone number is valid'</span>);
} <span class="hljs-keyword">catch</span> (error) {
  <span class="hljs-built_in">console</span>.error(error.message);
}
</code></pre>
<p>After applying the concept to this simple function, we can refactor our code to have better error handling to gracefully handle other formats for phone numbers that the user can input. Our code will probably result in a better user experience because the user can receive feedback about the expected format.</p>
<h1 id="heading-2-no-magic-happens-here">2. No "Magic Happens Here"</h1>
<blockquote>
<p><strong><em>By</em></strong> Alan Griffiths</p>
</blockquote>
<p>This is a concept that developers (myself included) break a lot. It's very easy to assume things "just work" without knowing how they work. This applies to many things: functions or classes, API calls, Async processes and even other people or other teams!</p>
<p>Having an understanding of critical dependencies ensures our own software components are interacting correctly with other processes. "Magic" components or software can fail. When we understand what happens under the hood, we can align better with error handling and ensure a better experience. This makes our software more robust</p>
<p>When building software that interacts with unfamiliar processes/components, take the time to understand it</p>
<p>This concept also applies to other people or teams. From a developer POV, it's easy to just code and be in a "bubble" where we just assume other people/teams just work. Product Managers, Business Analyst, QA Testers, etc. Imagine just how chaotic things will be without them</p>
<p>For the following example, we have a simple function that sends an email. The function uses an unfamiliar "magical" process that sends the message. Assuming it will just work, without understanding the underlying process, makes the function susceptible to unhandled exceptions, and potentially using the process in a wrong way:</p>
<h3 id="heading-without-the-concept-1">Without the concept:</h3>
<pre><code class="lang-javascript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sendEmail</span> (<span class="hljs-params">recipient, subject, message</span>) </span>{
  <span class="hljs-keyword">const</span> queueMessage = { recipient, subject, message, <span class="hljs-attr">behavior</span>: <span class="hljs-string">"send"</span> };

  <span class="hljs-comment">// magically sends the email</span>
  <span class="hljs-keyword">await</span> sendToSqsQueue(queueMessage);
}

<span class="hljs-comment">// Example usage</span>
<span class="hljs-keyword">const</span> recipient = <span class="hljs-string">"test@test.com"</span>;
<span class="hljs-keyword">const</span> subject = <span class="hljs-string">"Hello!"</span>;
<span class="hljs-keyword">const</span> message = <span class="hljs-string">"This is a test email."</span>;
<span class="hljs-keyword">await</span> sendEmail(recipient, subject, message);
</code></pre>
<p>This function is sending an email. However It doesn't handle any potential errors that may occur during the process. Also, it sends the message with the behavior as <code>send</code>. Without understanding the purpose of the <code>behavior</code> parameter, it's hard to improve or maintain the code.</p>
<h3 id="heading-applying-the-concept-1">Applying the concept:</h3>
<pre><code class="lang-javascript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sendEmail</span> (<span class="hljs-params">recipient, subject, message</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> queueMessage = { recipient, subject, message, <span class="hljs-attr">behavior</span>: <span class="hljs-string">"sendEmail"</span> };
    <span class="hljs-keyword">await</span> sendToSqsQueue(queueMessage);
    <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Failed to send email:"</span>, error);
    <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
  }
}

<span class="hljs-comment">// Example usage</span>
<span class="hljs-keyword">const</span> isEmailSent = <span class="hljs-keyword">await</span> sendEmail(recipient, subject, message);
<span class="hljs-keyword">if</span> (isEmailSent) {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Email sent successfully!"</span>);
}
<span class="hljs-keyword">else</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Failed to send email. Please try again."</span>);
}
</code></pre>
<p>Now, after inspecting and understanding the unfamiliar <code>sendToSqsQueue</code> process, error handling can be appropriately implemented. Also, the <code>behavoir</code> parameter can be set to a better, more performant alternative</p>
<p>Notice that it's not necessary to memorize every small detail about an unfamiliar process - Sometimes just reading the documentation of a "magical" API or process can be enough!</p>
<h1 id="heading-3-the-boy-scout-rule">3. The Boy Scout Rule</h1>
<blockquote>
<p><strong><em>By</em></strong> Robert C. Martin (Uncle Bob)</p>
</blockquote>
<p>This concept is about continuous improvement and refactoring. The boy scouts have a rule: "Always leave the campground cleaner than you found it". Transposed to the software world, it could be similar to "Always leave the code a little bit cleaner than you found it".</p>
<p>This rule is about being able to refactor code as you implement changes in a codebase. Little improvements over time can make a huge difference, specially in a large repository.</p>
<p>I think this is a great rule, as long as it doesn't involve huge refactoring. Ultimately, when working in a change we don't want to break what's already working! The level of refactoring comes to the level of comfort and familiarity with the process (see concept 2). Improvements that impact readability and maintainability are greatly appreciated</p>
<p>A high-performing software team can adopt this rule, which prevents gradual deterioration of the codebase. The larger a codebase becomes, the more difficult changes become if not following a maintainability and refactoring culture. This concept aims to improve code quality every time someone touches a module</p>
<p>For the code example, a function with several bad practices is shown. When a new feature gets implemented, also a small portion of the function is refactored to improve maintainability and readability:</p>
<h3 id="heading-without-the-concept-2">Without the concept:</h3>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">calcOrder</span> (<span class="hljs-params">odr</span>) </span>{
  <span class="hljs-keyword">let</span> x = <span class="hljs-number">0</span>;

  <span class="hljs-comment">// Sum up item prices</span>
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; odr.length; i++) {
    x += odr[i].prc * odr[i].qty;
  }
  <span class="hljs-comment">// Apply discount if applicable</span>
  <span class="hljs-keyword">if</span> (odr.length &gt; <span class="hljs-number">5</span>) {
    x = x - (x * <span class="hljs-number">0.1</span>);
  }
  <span class="hljs-comment">// Special handling for specific product IDs</span>
  <span class="hljs-keyword">if</span> (odr.some(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> e.id === <span class="hljs-string">'ABC123'</span>)) {
    x += <span class="hljs-number">100</span>;
  }

  <span class="hljs-keyword">return</span> x;
}
</code></pre>
<p>This is a sample function that has some bad practices. Bad naming convention for variables, magic strings and numbers, not following the Single Responsibility Principle are among the most apparent.</p>
<h3 id="heading-applying-the-concept-2">Applying the concept:</h3>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">calcOrder</span> (<span class="hljs-params">odr</span>) </span>{
  <span class="hljs-keyword">let</span> total = <span class="hljs-number">0</span>; <span class="hljs-comment">// Improved variable name</span>

  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; odr.length; i++) {
    total += odr[i].prc * odr[i].qty;
  }
  <span class="hljs-keyword">if</span> (odr.length &gt; <span class="hljs-number">5</span>) {
    total = total - (total * <span class="hljs-number">0.1</span>);
  }
  <span class="hljs-keyword">if</span> (odr.some(<span class="hljs-function"><span class="hljs-params">e</span> =&gt;</span> e.id === <span class="hljs-string">'ABC123'</span>)) {
    total += <span class="hljs-number">100</span>;
  }

  <span class="hljs-comment">// New feature: discount for orders over $500</span>
  <span class="hljs-keyword">if</span> (total &gt; <span class="hljs-number">500</span>) {
    total -= <span class="hljs-number">20</span>;
  }

  <span class="hljs-keyword">return</span> total;
}
</code></pre>
<p>When implementing a new feature in this function, aside from implementing the required change, the developer also takes the effort to make a small readability improvement: changing the variable called <code>x</code> to something more appropriate, like <code>total</code>. Notice the function wasn't all refactored, only a small portion was. The idea is to refactor small portions of code at a time to avoid big refactors that can result in more harm than good</p>
<h1 id="heading-summary">Summary</h1>
<ol>
<li><p><strong>What Would the User Do?</strong> We’re building a digital product for the users. But we are not the users - Think how would users interact with the software and take appropriate action</p>
</li>
<li><p><strong>Don't Rely on "Magic Happens Here"</strong>:  Don’t assume a process or component “just works” - have an understanding of critical dependencies. Also applies to other people/roles</p>
</li>
<li><p><strong>The Boy Scout Rule</strong>: Continuous improvement - leave the codebase a bit better that what you found it. Make small refactors aimed towards maintainability and readability</p>
</li>
</ol>
<h1 id="heading-conclusion">Conclusion</h1>
<p>By Following the concepts explained here, you can become a better software developer. These concepts are not purely technical, but I see them more on how to improve in the "Collaboration", "Teamwork" and "Purpose".</p>
<p>In my experience, awesome software developers have great non-technical skills, and the concepts presented here can help a software developer go from "good" to "great" or "awesome"!</p>
<h1 id="heading-references">References</h1>
<ul>
<li><a target="_blank" href="https://www.oreilly.com/pub/pr/2499">97 Things Every Programmer Should Know</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Unit Tests That Speak to Developers]]></title><description><![CDATA[Introduction
At the beginning of my career, I remember my first software company introducing me to "unit tests." Back then, it was literally a checklist of tasks that I had to manually verify before checking in my code to version control. Over time, ...]]></description><link>https://blog.lucasdev.info/unit-tests-that-speak-to-developers</link><guid isPermaLink="true">https://blog.lucasdev.info/unit-tests-that-speak-to-developers</guid><category><![CDATA[unit testing]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Jest]]></category><dc:creator><![CDATA[Lucas Vera Toro]]></dc:creator><pubDate>Tue, 27 Aug 2024 00:02:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/EWLHA4T-mso/upload/5ad0b596f998e616516bcb6179eb1480.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>At the beginning of my career, I remember my first software company introducing me to "unit tests." Back then, it was literally a checklist of tasks that I had to manually verify before checking in my code to version control. Over time, these tests have evolved. Now they cover real use cases, edge cases, and more complex scenarios. There’s something fascinating about seeing a test fail because of a code change; it means the test is doing its job. It provides a sense of security and confidence that the codebase is "protected".</p>
<p>As I learned more about automated tests and integrated them into code repositories, I noticed something was off: some tests seemed to be written just for the sake of it. It was easy to create a lot of passing tests, but not all of them were necessarily useful. Over time, I realized that the best tests are the ones that focus on the business logic - the real purpose of software systems. These tests ensure the core functionality works as expected and reflect the real-world scenarios that the software will encounter.</p>
<p>To me, the most valuable tests also act as documentation for the project. They describe the logic and rules the software is following. Rather than aiming for "100% code coverage," it's often more valuable to focus on tests that cover real use cases and different scenarios within the product and business logic.</p>
<p>Earlier this year, I stumbled upon Gerard Meszaros' article, <a target="_blank" href="https://web.archive.org/web/20150114192856/http://programmer.97things.oreilly.com/wiki/index.php/Write_Tests_for_People">Write Tests for People</a>. In this article, Meszaros asks a fundamental question: "Who am I writing the test for?" The answer is that tests should be written for the person trying to understand your code. Good tests can act as clear documentation that helps developers understand how the system behaves.</p>
<p>This mindset resonated with me. It’s something we as developers can sometimes overlook, myself included. In this blog post, I want to share some examples of unit tests that I believe are truly useful, not because they contribute to some arbitrary percentage of code coverage, but because they provide clarity and real value to the project.</p>
<h1 id="heading-practical-examples">Practical Examples</h1>
<h2 id="heading-example-1-calculate-discount">Example 1: Calculate Discount</h2>
<p>Imagine we have a function that calculates a discount given a price and a discount percentage. The function also needs to validate some of its input, as shown below:</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">calculateDiscount</span> (<span class="hljs-params">price, discountPercentage</span>) </span>{
  <span class="hljs-keyword">if</span> (price &lt;= <span class="hljs-number">0</span> || discountPercentage &lt; <span class="hljs-number">0</span> || discountPercentage &gt; <span class="hljs-number">100</span>) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Invalid input"</span>);
  }
  <span class="hljs-keyword">const</span> discount = (price * discountPercentage) / <span class="hljs-number">100</span>;
  <span class="hljs-keyword">return</span> price - discount;
}
</code></pre>
<h4 id="heading-bad-test-example">Bad Test Example ❌</h4>
<pre><code class="lang-javascript">test(<span class="hljs-string">'test price calculation'</span>, <span class="hljs-function">() =&gt;</span> {
  expect(calculatePrice(<span class="hljs-number">100</span>, <span class="hljs-number">20</span>)).toBe(<span class="hljs-number">80</span>);
});
</code></pre>
<p>What’s wrong with this test?</p>
<ul>
<li><p>This test is lacking context. The test name is vague and doesn't tell us what specific scenario is being tested.</p>
</li>
<li><p>It doesn't tell us anything about the logic. There’s no indication of why this particular discount and price are being used or what the business rule behind them is.</p>
</li>
<li><p>It's not readable and doesn't give future developers any insight into the functionality, making it hard to maintain or change in the future</p>
</li>
</ul>
<h4 id="heading-improved-version">Improved Version ✅</h4>
<pre><code class="lang-javascript">describe(<span class="hljs-string">"calculateDiscount"</span>, <span class="hljs-function">() =&gt;</span> {
  test(<span class="hljs-string">'should throw an error for negative price'</span>, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> price = <span class="hljs-number">-100</span>;
    <span class="hljs-keyword">const</span> discount = <span class="hljs-number">20</span>;

    expect(<span class="hljs-function">() =&gt;</span> calculateDiscount(price, discount)).toThrow(<span class="hljs-string">'Invalid input'</span>);
  });

  test(<span class="hljs-string">'should throw an error for discount greater than 100%'</span>, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> price = <span class="hljs-number">100</span>;
    <span class="hljs-keyword">const</span> discount = <span class="hljs-number">120</span>;

    expect(<span class="hljs-function">() =&gt;</span> calculateDiscount(price, discount)).toThrow(<span class="hljs-string">'Invalid input'</span>);
  });

  test(<span class="hljs-string">'should apply a 20% discount on a $100 product'</span>, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> price = <span class="hljs-number">100</span>;
    <span class="hljs-keyword">const</span> discount = <span class="hljs-number">20</span>;

    <span class="hljs-keyword">const</span> result = calculateDiscount(price, discount);

    expect(result).toBe(<span class="hljs-number">80</span>);
  });

  test(<span class="hljs-string">'should return the full price when discount is 0%'</span>, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> price = <span class="hljs-number">100</span>;
    <span class="hljs-keyword">const</span> discount = <span class="hljs-number">0</span>;

    <span class="hljs-keyword">const</span> result = calculateDiscount(price, discount);

    expect(result).toBe(<span class="hljs-number">100</span>);
  });
});
</code></pre>
<p>Why is this a better test?</p>
<ul>
<li><p>Names are clear and descriptive. They explain the scenarios being tested</p>
</li>
<li><p>Each test describes a real-world scenario, which adds clarity in the use cases that the function covers (valid and invalid inputs)</p>
</li>
<li><p>The tests follows a structured pattern (like the <a target="_blank" href="https://medium.com/@pjbgf/title-testing-code-ocd-and-the-aaa-pattern-df453975ab80">AAA Pattern</a>). This makes it easy to understand and correlate the different scenarios together</p>
</li>
<li><p>The tests cover different scenarios like the error handling, which is an important part of the business logic</p>
</li>
</ul>
<h2 id="heading-example-2-discount-eligibility">Example 2: Discount eligibility</h2>
<p>For the second example, we have a function that verifies the eligibility for a discount based on an age. An example implementation is described below:</p>
<pre><code class="lang-javascript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isEligibleForDiscount</span> (<span class="hljs-params">age</span>) </span>{
  <span class="hljs-keyword">if</span> (age &lt; <span class="hljs-number">0</span> || age &gt; <span class="hljs-number">120</span>) {
    <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">"Invalid age"</span>);
  }
  <span class="hljs-keyword">return</span> age &gt;= <span class="hljs-number">65</span>;
}
</code></pre>
<h4 id="heading-bad-test-example-1">Bad Test Example ❌</h4>
<pre><code class="lang-javascript">test(<span class="hljs-string">'check discount eligibility'</span>, <span class="hljs-function">() =&gt;</span> {
  expect(isEligibleForDiscount(<span class="hljs-number">70</span>)).toBe(<span class="hljs-literal">true</span>);
});
</code></pre>
<p>What’s wrong with this test?</p>
<ul>
<li><p>The name is vague and generic. It doesn't give info on the scenario</p>
</li>
<li><p>Edge cases are not covered as only one value is used</p>
</li>
<li><p>the test doesn't give any context on the logic of the function and doesn't explain why is it using the provided input and output</p>
</li>
</ul>
<h4 id="heading-improved-version-1">Improved Version ✅</h4>
<pre><code class="lang-javascript">describe(<span class="hljs-string">'isEligibleForDiscount'</span>, <span class="hljs-function">() =&gt;</span> {
  test(<span class="hljs-string">'should return true for age 70 (eligible for senior discount)'</span>, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> age = <span class="hljs-number">70</span>;
    <span class="hljs-keyword">const</span> result = isEligibleForDiscount(age);
    expect(result).toBe(<span class="hljs-literal">true</span>);
  });

  test(<span class="hljs-string">'should return false for age 50 (not eligible for senior discount)'</span>, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> age = <span class="hljs-number">50</span>;
    <span class="hljs-keyword">const</span> result = isEligibleForDiscount(age);
    expect(result).toBe(<span class="hljs-literal">false</span>);
  });

  test(<span class="hljs-string">'should return true for age 65 (boundary value)'</span>, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> age = <span class="hljs-number">65</span>;
    <span class="hljs-keyword">const</span> result = isEligibleForDiscount(age);
    expect(result).toBe(<span class="hljs-literal">true</span>);
  });

  test(<span class="hljs-string">'should throw an error for negative age'</span>, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> age = <span class="hljs-number">-5</span>;
    expect(<span class="hljs-function">() =&gt;</span> isEligibleForDiscount(age)).toThrow(<span class="hljs-string">'Invalid age'</span>);
  });

  test(<span class="hljs-string">'should throw an error for age over 120'</span>, <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> age = <span class="hljs-number">130</span>;
    expect(<span class="hljs-function">() =&gt;</span> isEligibleForDiscount(age)).toThrow(<span class="hljs-string">'Invalid age'</span>);
  });
});
</code></pre>
<p>Why is this a better test?</p>
<ul>
<li><p>The tests uses descriptive names that tell the story behind the scenario</p>
</li>
<li><p>The tests covers different values for input and expected outputs, including the edge cases</p>
</li>
<li><p>The tests give business context about the underlying logic in the test</p>
</li>
<li><p>The tests follow a structured pattern, which helps in relating the different cases and understanding them</p>
</li>
</ul>
<h1 id="heading-conclusion">Conclusion</h1>
<p>Unit tests can be a powerful tool to not only safeguard our code from bugs and issues, but also to provide clarity and documentation. The key is focusing on real-world use cases and covering the business logic that our software product uses.</p>
<p>The goal isn't to "write tests until we hit 100% test coverage" - rather the objective should be creating tests that offer insights and help in understanding the underlying business logic.</p>
<p>Let's focus on writing tests that matter!</p>
]]></content:encoded></item><item><title><![CDATA[AI-Powered Learning: Enhancing Student Attention: Part 1]]></title><description><![CDATA[Introduction
When discussing challenges in the education system, we can identify that measuring student’s attention is challenging, especially in a virtual environment, where there is a lack of closeness between students and the educator. It’s known ...]]></description><link>https://blog.lucasdev.info/ai-powered-learning-enhancing-student-attention-part-1</link><guid isPermaLink="true">https://blog.lucasdev.info/ai-powered-learning-enhancing-student-attention-part-1</guid><category><![CDATA[AWS]]></category><category><![CDATA[AI]]></category><category><![CDATA[edtech]]></category><category><![CDATA[lambda]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Lucas Vera Toro]]></dc:creator><pubDate>Mon, 26 Aug 2024 21:08:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1724705778300/23999bd5-0cf7-4e4e-a9f0-315935cdd84e.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1 id="heading-introduction">Introduction</h1>
<p>When discussing challenges in the education system, we can identify that measuring student’s attention is challenging, especially in a virtual environment, where there is a lack of closeness between students and the educator. It’s known through various research efforts that students in virtual environments engage and participate less in the virtual classroom than those in a normal classroom. As a consequence, students in virtual environments find the learning experience more challenging, and as a result, their learning is negatively affected.</p>
<p>One way to mitigate this problem is when the teacher implements “active learning” strategies, which consist of different activities that stimulate and engage the students:</p>
<ul>
<li><p>Propose debates</p>
</li>
<li><p>Q&amp;A sessions</p>
</li>
<li><p>Ask student’s opinions</p>
</li>
<li><p>Group discussions</p>
</li>
<li><p>Facilitate student-to-student interactions</p>
</li>
</ul>
<p>However, to properly implement these strategies, an educator needs to balance their explanations with these active learning strategies. One important metric for balancing that is the “engagement level” of each student. In a virtual environment, this is very challenging to measure.</p>
<h2 id="heading-measuring-attention-level">Measuring Attention Level</h2>
<p>In 2020, a group of researchers from Georgia Southern University presented an approach to identify the attentiveness of students. They conducted an experiment in which students’ faces were recorded while they were in a class. It gave a dataset of face pictures, which they proceeded to label as attentive or inattentive, based on expert supervision (human labeling). After that, they trained a machine learning model based on the data set which identifies the attention level of a student's face.</p>
<p>After getting a working model for identifying attention level, they correlated that model with emotions identified using <strong>AWS Rekognition</strong>. They made a correlation between the model attention level and the emotions identified by AWS Rekognition. As part of that correlation, they crafted the following formula which helps in identifying attention levels based on the AWS Rekognition emotions for a face:</p>
<p><img src="https://cdn.prod.website-files.com/5f16ec6886fb3fb049008f9a/66745d9b4aab38c299f746a1_attention%20level%20formula.webp" alt="Image that shows the formula for calculating attention level based on 7 emotions. Each emotion gets multiplied by a “beta” coefficient and the last factor is summing an error coefficient" class="image--center mx-auto" /></p>
<p><sup>Taken from Tabassum, T., Allen, A. A., &amp; De, P. (2020). Non-intrusive Identification of Student Attentiveness and Finding Their Correlation with Detectable Facial Emotions. ACM Southeast Conference - ACMSE 2020 - Session 1, 127-134.</sup></p>
<p>The values of the formula can be found in the following correlation table:</p>
<p><img src="https://cdn.prod.website-files.com/5f16ec6886fb3fb049008f9a/66745e31737ad2e200630e7f_correlation%20table.webp" alt="Image that shows a table of correlation coefficient and standard error used in the calculation of the attentiveness level. Each emotion represents a row, with a coefficient and standard error. Last row is an “Intercept”, which is also used in the calculation of the attention level" class="image--center mx-auto" /></p>
<p><sup>Taken from Tabassum, T., Allen, A. A., &amp; De, P. (2020). Non-intrusive Identification of Student Attentiveness and Finding Their Correlation with Detectable Facial Emotions. ACM Southeast Conference - ACMSE 2020 - Session 1, 127-134.</sup></p>
<h2 id="heading-disclaimer">Disclaimer</h2>
<p>Using this formula we can get the attention level of a student’s face, but it’s important to mention this is an approximation using a correlation between the different emotions of a face and the student’s attention level. Therefore, it’s an academic exercise and should not be considered the reality of a student’s engagement with the class. It’s only an approximation that serves as a starting point for analyzing trends and adapting the teaching strategies accordingly. <strong>It’s critical for educators and technologists to use these tools as supplements, not replacements, in a live education setting.</strong></p>
<h1 id="heading-architecture">Architecture</h1>
<p>So, we want to create a system that measures and gives recommendations to the teacher as the virtual class progresses. In this post, I present an approach using event-driven and serverless architecture to accomplish the following tasks:</p>
<ul>
<li><p>Capture screenshots of students’ faces as they participate in a virtual class</p>
</li>
<li><p>Automatically identify and analyze all students’ faces, and get emotion coefficients using AWS Rekognition</p>
</li>
<li><p>Determine the attention level of each student and the overall attention level for the class</p>
</li>
<li><p>Recommend an activity to the teacher</p>
</li>
</ul>
<p>So, we can identify a couple of modules or services to accomplish it, as shown in the following high-level overview:</p>
<p><img src="https://cdn.prod.website-files.com/5f16ec6886fb3fb049008f9a/66745e91f40eba579d0d2cd5_high%20level%20overview.webp" alt="Image that shows the high-level overview architecture of the solution. There are two icons representing people, one is the “students” and the other one is the “teacher”. Both have arrows that point to a “virtual class with video feed”. The teacher also has an arror that goes to “screenshot analyzer”, which starts app and takes screenshots periodically. that has two arrows, one goes to Rekognition API and the other one goes to “Teacher Recommender”. The teacher also has an arrow that goes to the “teacher recommender” directly to check if any recommendations" class="image--center mx-auto" /></p>
<p>As marked in the diagram, this blog post will cover the “<strong>Screenshot Analyzer</strong>” system. The “<strong>Teacher Recommender</strong>” will be covered in a future blog post</p>
<p>Expanding on the “Screenshot Analyzer” system, we can delve into the detailed design with the following diagram that shows the AWS resources and services that we’ll be using to build the solution:</p>
<p><img src="https://cdn.prod.website-files.com/5f16ec6886fb3fb049008f9a/66745e18d9ee907b48f3fc28_screenshot%20analyzer%20overview.webp" alt="Image that shows the detailed architecture of the screenshot analyzer. It starts with the virtual class with video feedback, with goes into a screenshot node.js REPL app, which uploads screenshots to a “Class Screenshot Bucket” S3. Then that bucket triggers a lambda function on “Object Created”. The lambda is called “screenshot analyzer and goes to AWS Rekognition and an Analysis Results DB" class="image--center mx-auto" /></p>
<p>In the image, we can see that the idea is to have a Node.js console application take screenshots of the virtual class video in a set interval of time. Each screenshot gets uploaded to an S3 bucket where a Lambda function gets triggered every time a new screenshot gets created. This Lambda is responsible for analyzing the screenshot, extracting the faces, and posting the results in a DynamoDB table.</p>
<h1 id="heading-solution">Solution</h1>
<h3 id="heading-screenshot-app">Screenshot App</h3>
<p>For the screenshot application, we can use the npm package “screenshot-desktop” to take the screenshots from our node.js application. Using a simple “setInterval” in javascript, we can set a recurring timer that takes a screenshot, and then uploads the image buffer into an S3 bucket using the AWS SDK for javascript (v3), as shown in the following code snippet:</p>
<pre><code class="lang-javascript">
  <span class="hljs-built_in">setInterval</span>(<span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">const</span> screenshotTime = getTimestamp()
  <span class="hljs-comment">// For example:</span>
  <span class="hljs-comment">// - /classes/2022-01-01/classid-123456/screenshotid-9518945.jpg</span>
  <span class="hljs-comment">// - /classes/2022-01-01/classid-123456/screenshotid-3159841.jpg</span>
  <span class="hljs-keyword">const</span> screenshotFilePath = <span class="hljs-string">`<span class="hljs-subst">${basePath}</span>/screenshotid-<span class="hljs-subst">${screenshotTime}</span>.<span class="hljs-subst">${SCREENSHOTS_FORMAT}</span>`</span>
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> imgBuffer = <span class="hljs-keyword">await</span> screenshot({ <span class="hljs-attr">format</span>: SCREENSHOTS_FORMAT })
    <span class="hljs-keyword">const</span> metadata = {
      <span class="hljs-attr">classId</span>: classId.toString(),
      <span class="hljs-attr">classStartedAtTimestamp</span>: getTimestamp(startDate).toString(),
      <span class="hljs-attr">screenshotTime</span>: screenshotTime.toString(),
      <span class="hljs-attr">screenshotBasePath</span>: basePath,
    }
    <span class="hljs-keyword">await</span> aws.s3.uploadToS3(BUCKET_NAME, screenshotFilePath, imgBuffer, metadata)
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'err'</span>, error)
    logMsg(<span class="hljs-string">'Error taking and saving screenshot.'</span>, error, { screenshotTime, screenshotFilePath })
  }
}, SCREENSHOT_INTERVAL_SECONDS * <span class="hljs-number">1000</span>)
</code></pre>
<p>(Full code repo can be found in the References section at the end of the article)</p>
<h3 id="heading-screenshot-analyzer">Screenshot Analyzer</h3>
<p>After a new screenshot gets added to the S3 bucket, we “hook up” a Lambda to the S3 so that the Lambda gets triggered automatically when a new screenshot gets uploaded. Using the Serverless Framework, this can be done easily as shown in the following code snippet:</p>
<pre><code class="lang-javascript">
<span class="hljs-attr">events</span>: [
    {
      <span class="hljs-attr">s3</span>: {
        <span class="hljs-attr">bucket</span>: <span class="hljs-string">`<span class="hljs-subst">${process.env.BUCKET_NAME || <span class="hljs-string">'${env:BUCKET_NAME}'</span>}</span>-<span class="hljs-subst">${process.env.STAGE || <span class="hljs-string">'${env:STAGE}'</span>}</span>`</span>,
        <span class="hljs-attr">event</span>: <span class="hljs-string">'s3:ObjectCreated:*'</span>,
        <span class="hljs-attr">existing</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">rules</span>: [{ <span class="hljs-attr">suffix</span>: <span class="hljs-string">'.jpg'</span> }],
        <span class="hljs-attr">forceDeploy</span>: <span class="hljs-literal">true</span>, <span class="hljs-comment">// Used to force cloudformation to update the trigger no matter what</span>
      }
    }
  ]
</code></pre>
<p>The reference to the bucket is saved in an environment variable for easier management across different stages and deployments.</p>
<p>In the Lambda code, the following bullet points are needed to complete the analysis:</p>
<ul>
<li><p>Download the screenshot from S3</p>
</li>
<li><p>Identify each face that’s in the screenshot, along with bounding borders and emotions of each face</p>
</li>
<li><p>Use the “sharp” npm package to extract and save each individual face as its own image</p>
</li>
<li><p>Upload each cropped face into another S3 bucket for future reference</p>
</li>
<li><p>Save the results into a DynamoDB Table for other consumers to use</p>
</li>
</ul>
<p>The following code snippet shows the Lambda handler processor, which orchestrates those tasks:</p>
<pre><code class="lang-typescript">
<span class="hljs-keyword">const</span> processEvent = <span class="hljs-keyword">async</span> (eventRecord: S3EventRecord): <span class="hljs-function"><span class="hljs-params">Promise</span> =&gt;</span> {
  <span class="hljs-keyword">const</span> {
    bucket: { name: imageBucketName },
    <span class="hljs-built_in">object</span>: { key: imageObjectKey },
  } = eventRecord.s3
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> faceAnalysisResults = <span class="hljs-keyword">await</span> AttentionService.analyzeImage(imageObjectKey)
    <span class="hljs-keyword">const</span> { Body, Metadata: screenshotMetadata } = <span class="hljs-keyword">await</span> aws.s3.getObject(imageBucketName, imageObjectKey)
    <span class="hljs-keyword">const</span> {
      classid: classId,
      classstarteddatetimestamp: classStartedDateTimestamp,
      screenshottime: screenshotTime,
      screenshotbasepath: screenshotBasePath,
    } = screenshotMetadata


    <span class="hljs-keyword">const</span> completeFaceAnalysisResults = <span class="hljs-keyword">await</span> extractFaces(faceAnalysisResults, Body <span class="hljs-keyword">as</span> Readable, screenshotTime, classId)


    <span class="hljs-keyword">await</span> AttentionService.publishResults(
      completeFaceAnalysisResults,
      classId,
      classStartedDateTimestamp,
      screenshotTime,
      screenshotBasePath,
      imageObjectKey
    )
  }
  <span class="hljs-keyword">catch</span> (ex) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error processing s3 event record'</span>, inspect(eventRecord))
  }
}
</code></pre>
<p>When analyzing the image, we use AWS Rekognition to detect faces and get the levels of emotions per face, as shown in the code snippet below. Note that to calculate the attention level of a face, we use the emotions detected by AWS Rekognition (which gets calculated using the formula explained in the above sections).</p>
<pre><code class="lang-typescript">
<span class="hljs-keyword">const</span> analyzeImage = <span class="hljs-keyword">async</span> (s3ObjectKey: <span class="hljs-built_in">string</span>): <span class="hljs-function"><span class="hljs-params">Promise</span> =&gt;</span> {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> results = <span class="hljs-keyword">await</span> aws.rekognition.detectFaces(BUCKET_NAME, s3ObjectKey)
    <span class="hljs-keyword">const</span> createdAt = getTimestamp()
    <span class="hljs-keyword">return</span> results.FaceDetails
      <span class="hljs-comment">// .filter(face =&gt; shouldDiscardReading(face)) // (TODO): uncomment this line to discard faces with low confidence</span>
      .map(<span class="hljs-function">(<span class="hljs-params">face, i</span>) =&gt;</span> ({
        ...face,
        index: i + <span class="hljs-number">1</span>,
        analysisCreatedAt: createdAt,
        Landmarks: <span class="hljs-literal">undefined</span>,
        attentionLevel: roundNum(calculateAttentionLevel(face.Emotions) * <span class="hljs-number">100</span>, <span class="hljs-number">2</span>), <span class="hljs-comment">// convert to percentage</span>
        isUncertainAnalysis: isUncertainAnalysis(face.Emotions),
      }))
  }
  <span class="hljs-keyword">catch</span> (ex) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error calculating screenshot raw attention'</span>, ex, s3ObjectKey)
  }
}
</code></pre>
<p>For extracting the faces and cropping them, we use the “sharp” npm package, which has an easy-to-use API for this kind of operation (as long as we have the “bounding box” from the AWS Rekognition analysis results) as shown in the code snippet below:</p>
<pre><code class="lang-typescript">
<span class="hljs-keyword">const</span> { BoundingBox } = faceAnalysisResult;
<span class="hljs-keyword">const</span> screenshot = <span class="hljs-keyword">await</span> streamToBuffer(screenshotStream);
<span class="hljs-keyword">const</span> {
  height: screenshotHeight,
  width: screenshotWidth,
} = <span class="hljs-keyword">await</span> sharp(screenshot).metadata();


<span class="hljs-keyword">const</span> faceHeight = <span class="hljs-built_in">Math</span>.round(BoundingBox.Height * screenshotHeight) + (pixelsOffset * <span class="hljs-number">2</span>);
<span class="hljs-keyword">const</span> faceWidth = <span class="hljs-built_in">Math</span>.round(BoundingBox.Width * screenshotWidth) + (pixelsOffset * <span class="hljs-number">2</span>);
<span class="hljs-keyword">const</span> faceTop = <span class="hljs-built_in">Math</span>.round(BoundingBox.Top * screenshotHeight) - pixelsOffset;
<span class="hljs-keyword">const</span> faceLeft = <span class="hljs-built_in">Math</span>.round(BoundingBox.Left * screenshotWidth) - pixelsOffset;


<span class="hljs-comment">// Limits the face extraction to the screenshot size</span>
<span class="hljs-keyword">const</span> height = faceHeight &gt; screenshotHeight ? screenshotHeight : faceHeight;
<span class="hljs-keyword">const</span> width = faceWidth &gt; screenshotWidth ? screenshotWidth : faceWidth;
<span class="hljs-keyword">const</span> top = faceTop &lt;= <span class="hljs-number">0</span> ? screenshotHeight : faceTop;
<span class="hljs-keyword">const</span> left = faceLeft &lt;= <span class="hljs-number">0</span> ? screenshotWidth : faceLeft;
<span class="hljs-comment">// Extracts the face from the screenshot</span>
<span class="hljs-keyword">const</span> faceBuffer = <span class="hljs-keyword">await</span> sharp(screenshot).extract({
  height,
  width,
  top,
  left,
}).toBuffer();
<span class="hljs-keyword">return</span> faceBuffer;
</code></pre>
<p>(Full code repo can be found in the References section at the end of the article)</p>
<h1 id="heading-future-work">Future Work</h1>
<p>While this article gave an overview of the solution as a whole, it only delved into the “screenshot analyzer” section. In future articles, we will explore other components, especially the “Teacher recommender” system, which provides tailored activity suggestions based on the data that was collected by the screenshot analysis.</p>
<p>Stay tuned for future updates where we will dive deeper into these components!</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>In this article, we covered a “learning recommendation system”, which aims to improve the challenge of measuring and enhancing student engagement in virtual learning environments. By using the machine learning capabilities built into AWS Rekognition, we can detect and extract the faces of students in a virtual classroom, then analyze them and save them for future use by a teacher recommender app for tailor-made recommendations that aim to improve the learning experience.</p>
<p>Using serverless architecture, we designed an effective and efficient system that accomplishes the goal of analyzing and calculating student engagement in a virtual classroom.</p>
<p>Stay tuned for the teacher recommender system in a future article!</p>
<h1 id="heading-references">References</h1>
<ul>
<li><p>Paper for calculating attention level:</p>
<ul>
<li>Tabassum, T., Allen, A. A., &amp; De, P. (2020). Non-intrusive Identification of Student Attentiveness and Finding Their Correlation with Detectable Facial Emotions. ACM Southeast Conference - ACMSE 2020 - Session 1, 127-134.</li>
</ul>
</li>
<li><p>AWS Rekognition “Detect Faces” Documentation:</p>
<ul>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/rekognition/latest/dg/faces.html">https://docs.aws.amazon.com/rekognition/latest/dg/faces.html</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/rekognition/latest/dg/faces-detect-images.html">https://docs.aws.amazon.com/rekognition/latest/dg/faces-detect-images.html</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/rekognition/latest/APIReference/API_DetectFaces.html">https://docs.aws.amazon.com/rekognition/latest/APIReference/API_DetectFaces.html</a></p>
</li>
</ul>
</li>
<li><p>Screenshot App Full Code: <a target="_blank" href="https://github.com/LucasVera/eafit-screenshot-app">https://github.com/LucasVera/eafit-screenshot-app</a></p>
</li>
<li><p>Screenshot Analyzer Full Code: <a target="_blank" href="https://github.com/LucasVera/screenshot-analyzer">https://github.com/LucasVera/screenshot-analyzer</a></p>
</li>
<li><p>Link to my master’s Thesis (Spanish): <a target="_blank" href="https://repository.eafit.edu.co/items/191b580f-8415-489e-a5a5-e42776712258">https://repository.eafit.edu.co/items/191b580f-8415-489e-a5a5-e42776712258</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Navigating Post-Payment: AWS Step Functions for E-Commerce]]></title><description><![CDATA[(Originally posted on the Serverless Guru blog at https://www.serverlessguru.com/blog/navigating-post-payment-aws-step-functions-for-e-commerce-excellence)
Introduction
In a typical e-commerce application, a user browses through a catalog of products...]]></description><link>https://blog.lucasdev.info/navigating-post-payment-aws-step-functions-for-e-commerce</link><guid isPermaLink="true">https://blog.lucasdev.info/navigating-post-payment-aws-step-functions-for-e-commerce</guid><category><![CDATA[AWS]]></category><category><![CDATA[stepfunction]]></category><category><![CDATA[ecommerce]]></category><category><![CDATA[Orchestration]]></category><category><![CDATA[serverless]]></category><dc:creator><![CDATA[Lucas Vera Toro]]></dc:creator><pubDate>Mon, 26 Aug 2024 15:26:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1724684314752/9939a611-d300-467a-8141-0ed4bc556b14.webp" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>(Originally posted on the Serverless Guru blog at</em> <a target="_blank" href="https://www.serverlessguru.com/blog/navigating-post-payment-aws-step-functions-for-e-commerce-excellence"><em>https://www.serverlessguru.com/blog/navigating-post-payment-aws-step-functions-for-e-commerce-excellence</em></a><em>)</em></p>
<h1 id="heading-introduction">Introduction</h1>
<p>In a typical e-commerce application, a user browses through a catalog of products, chooses some products, and then proceeds to checkout. In the checkout process, the user needs to pay for the selected products. Once the <strong>payment process is completed</strong>, some <strong>post-payment activities need to be executed</strong> in order to process the order and the products that the user paid for.</p>
<p>These “post-payment” activities usually refer to tasks that may need to be performed in a particular sequence, but at the same time, some other tasks may be performed in parallel. There is also the need to do error handling.</p>
<p>For this use case, a powerful pattern is to <strong>use an orchestrator service that handles all the activities in a streamlined way</strong>. Using AWS Step Functions is a great way to orchestrate these tasks.</p>
<p>Let’s dive into an example use case, along with some high-level requirements, and showcase a couple of features and a combination of services.</p>
<h2 id="heading-requirements">Requirements</h2>
<p>Let’s imagine a typical e-commerce application, where a user can <strong>browse</strong> through different products, add them to some sort of <strong>“shopping basket”</strong> and then, once it’s satisfied, can continue with the <strong>payment process</strong>. After the payment process is done, there are some activities that the e-commerce system must perform, like <strong>updating the inventory</strong> of the products, <strong>sending an order</strong> with enough details to a fulfillment center, sending the customer a <strong>notification</strong> with order details, arrival date, etc. This article will focus on the “post-payment” activities, which, in a typical e-commerce site, come after the payment process is finished. The following is a sample of what some <strong>requirements</strong> could look like for building such an “feature”:</p>
<ul>
<li><p>After the user finishes payment, a “post-payment activities” process must be triggered</p>
</li>
<li><p>The “post-payment activities” process should do the following:</p>
<ul>
<li><p>Verify that payment was completed successfully</p>
<ul>
<li>If payment was not completed successfully, send a general error notification to the user and don’t continue with the rest of the flow</li>
</ul>
</li>
<li><p>Upon successful payment verification, initiate the following tasks in parallel for order processing. Products need to be grouped by fulfillment center and then, for each product group, do the following:</p>
<ul>
<li><p>Update inventory system</p>
</li>
<li><p>Send order details to the fulfillment center</p>
</li>
</ul>
</li>
<li><p>After both tasks are completed, perform the following tasks:</p>
<ul>
<li><p>Send a confirmation email to the customer with details about the order, arrival date, etc</p>
</li>
<li><p>Update the customer’s order history with the new order</p>
</li>
</ul>
</li>
<li><p>Each step must handle errors gracefully. If an error happens in one of the tasks:</p>
<ul>
<li>Log the error for manual review and send a notification email to the customer support team</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>A high-level diagram of the system is shown below:</p>
<p><img src="https://cdn.prod.website-files.com/5f16ec6886fb3fb049008f9a/66081e523f551ea6b7bd18f8_high-level-overview.webp" alt="high level overview of last steps of a typical e-commerce application that shows product selection, payment and post-payment activities like payment verification, product grouping, update inventory, send to fulfillment center and customer notification" class="image--center mx-auto" /></p>
<p>High-level overview of a typical e-commerce application, expanding on the post-payment activities</p>
<p>Notice that the image only shows the functional tasks. Other tasks like error handling are technical aspects that need to be taken into account in the solution.</p>
<h1 id="heading-the-solution">The Solution</h1>
<p>The solution to meet the requirements should involve a way to <strong>sequence different separate processes</strong>. Looking at the requirements, we can simplify the solution to a system that is capable of sequencing the different tasks in a particular order, while at the same time handling parallel steps and error handling. In this case, a simplified <strong>flowchart</strong> of the solution could look like the following image</p>
<p><img src="https://cdn.prod.website-files.com/5f16ec6886fb3fb049008f9a/66081eb49e7b3a03f8da8e12_post-payment-activitires%20flowchart.webp" alt="Flow chart of post-payment activities for a typical e-commerce application that shows a streamlined flow that verifies payment, fetches and groups products, updates inventory, sends to fulfillment centers, sends notifications and updates order history. Also handles errors" class="image--center mx-auto" /></p>
<p>Post-Payment Activities Flow chart describing a streamlined way to meet requirements of a typical e-commerce application</p>
<p>As we can see in the flowchart image, the post-payment activities can be arranged in a specific sequence, with <strong>error handling</strong> in between steps. We can also leverage <strong>parallel processing</strong> where we can in a way that makes sense and also follows the requirements. Notice also that this flowchart is <strong>agnostic of underlying technology/service</strong>. While this article wants to highlight the benefits of step functions, while making an architecture decision it’s <strong>important to consider other solutions</strong> and proposals. Let’s take a look at two possible solutions involving two different patterns, along with its pros and cons:</p>
<h3 id="heading-orchestrator-pattern"><strong>Orchestrator Pattern</strong></h3>
<p>An “<strong>Orchestrator</strong>” is a central control unit which is responsible for sequencing different processes. Much like the conductor in an orchestra, it dictates the <strong>flow of operations</strong> and ensures that each piece plays at the correct time. The “orchestrator” is responsible for initiating processes, coordinating inter-process communication, handling failures and ensuring the workflow goes smoothly. This creates a system that allows for <strong>easier monitoring and managing complex workflows</strong></p>
<h3 id="heading-choreographer-pattern"><strong>Choreographer Pattern</strong></h3>
<p>The “Choreographer” is a <strong>decentralization</strong> pattern. There is no single entity which controls the sequence of the processes. Instead, each <strong>individual process</strong> knows which other process is next for execution after its own completion, and when to do so. This is accomplished by writing and reading <strong>messages</strong>. Each process emits messages after finishing, which gets picked up by other processes. This creates a <strong>loosely coupled</strong> system where individual processes only need to understand messages they care about.</p>
<h3 id="heading-comparison"><strong>Comparison</strong></h3>
<p>While both patterns can help build a system that handles the requirements, choosing the <strong>orchestrator pattern</strong> offers a centralized way to handle processes, allowing easier error handling and even recovery if needed. Monitoring and observability are also simplified, which facilitates troubleshooting.</p>
<h3 id="heading-aws-step-functions"><strong>AWS Step Functions</strong></h3>
<p>AWS Step Functions is a <strong>serverless orchestrator service</strong> that streamlines a particular workflow into a set of states. It excels in handling <strong>complex workflows</strong> that have multiple steps and sequencing those steps. At the same time, it’s <strong>great for handling errors</strong>, <strong>parallel processing, and integrating with other AWS services</strong>. One great benefit of it is its ability to visually represent and edit the workflow through the aws console, which also lets you execute it and see each step’s inputs and outputs clearly.</p>
<p>Given the nature of the requirements of implementing the logic for completing the tasks in a particular sequence, and the requirement to handle errors, <strong>choosing AWS Step Functions to act as an orchestrator is a natural solution</strong>.</p>
<p>Building this solution using AWS Step Functions is possible through the AWS console. While it’s a great tool to <strong>visually build</strong> the step function workflow and execute it to debug it, it can also help in exporting the JSON that corresponds to the workflow steps and configuration. This way it’s possible to paste the JSON in a Cloudformation template to <strong>deploy the step function through Infrastructure as Code</strong> tools like CloudFormation.</p>
<h2 id="heading-assumptions">Assumptions</h2>
<p>In order to keep this article from getting too complex, let’s make a couple of assumptions regarding the following points:</p>
<ul>
<li><p>How the data looks after finishing the payment process</p>
</li>
<li><p>How data is related between the customer’s chosen products, payment and the input to start the post-payment activities</p>
</li>
<li><p>Simplify inventory management to a DynamoDB update</p>
</li>
<li><p>Simplify how to send products to fulfillment centers (invoke a Lambda function)</p>
</li>
<li><p>Simplify how to send notifications to customers (send a message to an SNS topic)</p>
</li>
<li><p>Simplify how to send notifications to the customer support team (send a message to an SNS topic)</p>
</li>
</ul>
<p>With this in mind, let’s explore a bit on how the data looks like before jumping into the actual implementation.</p>
<h3 id="heading-customer-session"><strong>Customer Session</strong></h3>
<p>In order to <strong>simplify</strong> the way the data is handled, let’s make the following simplified data modeling in order to cover the customer’s chosen products and payment status. Before sending for payment, let’s imagine a “session” gets generated. This session contains the following information:</p>
<ul>
<li><p>Chosen products</p>
</li>
<li><p>Payment Status</p>
</li>
</ul>
<p>Let’s also assume that this data gets stored in a simple DynamoDb Table that uses a <code>SessionId</code> (uuid) as the <strong>primary key</strong> to identify the payment session of each customer. Some sample data modeling might look like the following:</p>
<p>Table name: <code>CustomerPaymentSession</code></p>
<ul>
<li><p>SessionId (PK): <code>String</code></p>
</li>
<li><p>CustomerId: <code>String</code></p>
</li>
<li><p>PaymentStatus: <code>String</code></p>
</li>
<li><p>Products: <code>List</code></p>
</li>
</ul>
<p>Let’s also make an assumption that each product has the following structure:</p>
<ul>
<li><p>fulfillmentCenterId: <code>String</code></p>
</li>
<li><p>id: <code>String</code></p>
</li>
<li><p>name: <code>String</code></p>
</li>
<li><p>price: <code>Number</code></p>
</li>
<li><p>quantity: <code>Number</code></p>
</li>
</ul>
<p>As so, an example of a “customer session” may look like the following:</p>
<pre><code class="lang-plaintext">
{
  "SessionId": "a48eb859-c71e-4566-9b05-d8e5e46d8a25",
  "CustomerId": "123",
  "PaymentStatus": "Success",
  "Products": [
    {
      "fulfillmentCenterId": "300aa993-ef76-4ff4-87f6-cb57380057aa",
      "id": "587a2bd7-000d-4f28-b240-d7b57473ecbd",
      "name": "T-Shirt",
      "price": 10,
      "quantity": 5
    },
    {
      "fulfillmentCenterId": "300aa993-ef76-4ff4-87f6-cb57380057aa",
      "id": "9f44cb7f-6c60-4f30-87ca-081206d3a6a2",
      "name": "Pants",
      "price": 15,
      "quantity": 2
    },
    {
      "fulfillmentCenterId": "55ca7c2b-8b00-4419-98d7-f4f959464c97",
      "id": "21a90825-3c6d-4121-9be6-8aa10b72b60d",
      "name": "Shoes",
      "price": 18,
      "quantity": 7
    }
  ]
}
</code></pre>
<h1 id="heading-sample-implementation"><code>‍</code>Sample Implementation</h1>
<p>When using the AWS console and the workflow studio, we can do a simple implementation. With the help of the assumptions and the previous analysis, the <strong>result</strong> would look like the following:</p>
<p><img src="https://cdn.prod.website-files.com/5f16ec6886fb3fb049008f9a/6608201e52bf8dea2057a47c_step%20function%20full%20image.webp" alt="Step function as shown in the aws console workflow. It showcases the start, then get payment record dynamodb get item operation, then a choice state which checks if payment is successful, then if payment isn’t successful it goes to send a notification to the support team. If it’s successful, it fetches and groups products, then for each product group it does a parallel state where inventory is updated and products are sent to fulfillment centers. After that, if there is an error, it’s sent to customer support group notification. If no errors, it continues with another parallel step where a success notification is sent to the customer and its order history is updated. Finally it goes to the end" class="image--center mx-auto" /></p>
<p>Visual representation of the step function in the aws console</p>
<p>Let’s zoom in to cover each part and explain more clearly:</p>
<h2 id="heading-data-flow">Data Flow</h2>
<p><img src="https://cdn.prod.website-files.com/5f16ec6886fb3fb049008f9a/6608203f1112c7b95389f509_step%20function%201.webp" alt="Zoom-in of the first three steps of the step function. First step is the “get payment record”, second step is the choice state “is payment success?” and third step is the lambda execution of “Fetch and group products”" class="image--center mx-auto" /></p>
<p>Zoom-in to the first three steps of the step function workflow</p>
<p>Here we have the start of the step function workflow, the verification of the payment status and a Lambda function that fetches and groups products to continue with the flow.</p>
<p><strong><em>1.</em></strong> In the first step, we get the <strong>input</strong> used to invoke the step function. According to the assumptions and the simplified data model, the step function should be initiated with the <code>SessionId</code> parameter, as is the primary key to the “CustomerPaymentSession” DynamoDb Table, which holds the customer’s products and payment result:</p>
<pre><code class="lang-plaintext">
{
    "SessionId": "a48eb859-c71e-4566-9b05-d8e5e46d8a25"
}
</code></pre>
<p>With this input, we can <strong>fetch the payment record</strong> with the given <code>SessionId</code> and check if payment was executed successfully. The most suited dynamodb operation in this case is a “<strong>GetItem</strong>”, which is declared and configured in the step function itself:</p>
<pre><code class="lang-plaintext">
{
    "TableName": "post-payment-records",
    "Key": {
        "SessionId": {
            "S.$": "$.SessionId"
        }
    }
}
</code></pre>
<p>Notice the usage of <code>$</code>. When referencing a value in the input, we use the <code>$</code> at the <strong>end of the field name</strong>, then the value starts with the sign followed by the field name we want to reference. In this case, the input contains <code>SessionId</code>, so we reference it using <code>$.SessionId</code>. Similarly, we can map the output of the DynamoDB operation using the Step Functions “<strong>Transform result</strong>” feature, which lets us reference values in the result and map them in a way that makes sense. In this case, we can configure the following mapping:</p>
<pre><code class="lang-plaintext">
{
"SessionId.$": "$.Item.SessionId.S",
"PaymentStatus.$": "$.Item.PaymentStatus.S",
"CustomerId.$": "$.Item.CustomerId.S",
"Products.$": "$.Item.Products.L"
}
</code></pre>
<p><code>‍</code>An example output of this step would be the following:</p>
<pre><code class="lang-plaintext">
{
  "Products": [
    {
      "M": {
        "quantity": {
          "N": "5"
        },
        "fulfillmentCenterId": {
          "S": "300aa993-ef76-4ff4-87f6-cb57380057aa"
        },
        "price": {
          "N": "10"
        },
        "name": {
          "S": "T-Shirt"
        },
        "id": {
          "S": "587a2bd7-000d-4f28-b240-d7b57473ecbd"
        }
      }
    },
    {
      "M": {
        "quantity": {
          "N": "2"
        },
        "fulfillmentCenterId": {
          "S": "300aa993-ef76-4ff4-87f6-cb57380057aa"
        },
        "price": {
          "N": "15"
        },
        "name": {
          "S": "Pants"
        },
        "id": {
          "S": "9f44cb7f-6c60-4f30-87ca-081206d3a6a2"
        }
      }
    },
    {
      "M": {
        "quantity": {
          "N": "7"
        },
        "fulfillmentCenterId": {
          "S": "55ca7c2b-8b00-4419-98d7-f4f959464c97"
        },
        "price": {
          "N": "18"
        },
        "name": {
          "S": "Shoes"
        },
        "id": {
          "S": "21a90825-3c6d-4121-9be6-8aa10b72b60d"
        }
      }
    }
  ],
  "CustomerId": "123",
  "SessionId": "a48eb859-c71e-4566-9b05-d8e5e46d8a25",
  "PaymentStatus": "Success"
}
</code></pre>
<p><code>‍</code><strong><em>2.</em></strong> In this step, we need to verify the payment status given by DynamoDB in the previous step. We can make use of a “<strong>Choice State</strong>” from Step Functions, which takes an input and evaluates it against certain conditions to determine the next step. For our use case, we only care that <code>PaymentStatus</code> is exactly <code>Success</code>. We can do so like in the Step Functions configuration shown below:</p>
<p><img src="https://cdn.prod.website-files.com/5f16ec6886fb3fb049008f9a/66082145b0969e1098ea2281_is%20payment%20success%20config.webp" alt="Image that shows configuration of the condition to check if the payment status in the input is equal “Success”" class="image--center mx-auto" /></p>
<p>Condition to check if payment was successful using a step function’s “choice” step</p>
<p><strong><em>3.</em></strong> For Step 3, We need to <strong>fetch and group products</strong> so that they can be processed in groups and can be sent to fulfillment center in groups. As shown in previous steps, the data for each product has a <code>fulfillmentCenterId</code>. For simplicity of this article, let’s imagine a Lambda function takes care of this task, and returns the <strong>grouped products</strong> by <code>fulfillmentCenterId</code>, as shown below:</p>
<pre><code class="lang-plaintext">
[
  {
    "fulfillmentCenterId": "300aa993-ef76-4ff4-87f6-cb57380057aa",
    "products": [
      {
        "fulfillmentCenterId": "300aa993-ef76-4ff4-87f6-cb57380057aa",
        "id": "587a2bd7-000d-4f28-b240-d7b57473ecbd",
        "name": "T-Shirt",
        "price": "10",
        "quantity": "5"
      },
      {
        "fulfillmentCenterId": "300aa993-ef76-4ff4-87f6-cb57380057aa",
        "id": "9f44cb7f-6c60-4f30-87ca-081206d3a6a2",
        "name": "Pants",
        "price": "15",
        "quantity": "2"
      }
    ]
  },
  {
    "fulfillmentCenterId": "55ca7c2b-8b00-4419-98d7-f4f959464c97",
    "products": [
      {
        "fulfillmentCenterId": "55ca7c2b-8b00-4419-98d7-f4f959464c97",
        "id": "21a90825-3c6d-4121-9be6-8aa10b72b60d",
        "name": "Shoes",
        "price": "18",
        "quantity": "7"
      }
    ]
  }
]
</code></pre>
<p><img src="https://cdn.prod.website-files.com/5f16ec6886fb3fb049008f9a/660821cd8efbfed2be862653_step%20function%202.webp" alt="Zoom-in of the next set of steps in the step function. After fetch and group products, (marked #4) the “Map state” which does a “for each product group”. Inside the “map”, (marked #5) is a “parallel” state which executes an update inventory in DynamoDB and a Lambda function invocation to send to fullfilment center" class="image--center mx-auto" /></p>
<p>Zoom-in of the step function, to show a “Parallel step” within a “Map step”</p>
<p><strong><em>4.</em></strong> Step 4 receives the grouped products array as input. Since we need to <strong>loop through</strong> each product group, we can use the Step Function’s “<strong>Map state</strong>” to do so. We just specify the path of the array to loop and it will run what’s inside the box for each entry in the array.</p>
<p><strong><em>5.</em></strong> For each product group, we need to perform two tasks: update product inventory and send to fulfillment center for further processing. We can leverage the usage of a “<strong>Parallel State</strong>” to complete these tasks in parallel, this way the tasks take less time to complete than if they were sequential. For simplicity of the article, we’ll not jump into details on how each task is done.</p>
<p><img src="https://cdn.prod.website-files.com/5f16ec6886fb3fb049008f9a/660821f793f055261bf53034_step%20function%203.webp" alt="Image that shows a zoom-in of the next portion of the step function, in this case the output of 5 is shown to feed step 6, which is a parallel state that executes a sns publish to send a success notification, and a lambda invocation to update the order history of the customer. After that, it goes to step 7 which is the end of the execution of the step function" class="image--center mx-auto" /></p>
<p>Zoom-in of the next portion of the step function, which shows the next set of tasks and the end of the execution</p>
<p><strong><em>6.</em></strong> After each product group finishes executing each task successfully, it means all inventories were updated successfully and products sent to their respective fulfillment centers. So in this case, we can proceed to perform two tasks: send a “success” notification to the customer and update the order history. Again, we can leverage the usage of a <strong>“Parallel Step”</strong> to execute both tasks in parallel. For simplicity’s sake, I will not go into detail of each step.</p>
<p><strong><em>7.</em></strong> After the notification is sent and the history is updated, we are <strong>done</strong>! We can end the execution of the step function there.</p>
<p><img src="https://cdn.prod.website-files.com/5f16ec6886fb3fb049008f9a/66082215af7f72c2d547d4e4_step%20function%204.webp" alt="Image that shows a zoom-in to the step function, showing the error scenarios that are from steps 3, 6 and 7. they are marked as 3.E, 6.E and 7.E. All of them go to the “Send notification to support team” SNS publish operation, and finally after that it goes to the end" class="image--center mx-auto" /></p>
<p>Zoom-in to show the error scenarios in the step function</p>
<p><strong><em>3.E, 6.E, 7.E</em></strong> (Error Scenarios): Notice that so far we have covered the “<strong>happy path</strong>”, where all individual steps succeed. But this is not always the case, as <strong>things can fail</strong> for a number of reasons. For example, the payment of the user may not have completed successfully, maybe the products couldn’t be sent to the fulfillment centers or maybe the success notification couldn’t be sent. In any case, the requirement is to handle these error scenarios.</p>
<p>With Step Functions, we can set a <strong>“catch” configuration</strong>. In that catch we can specify which kind of errors we want to capture and where the Step Functions route the error scenarios. So in this case, we can set a simple catch that <strong>routes failures</strong> to the “Send Notification to Support Team” SNS topic, so that the customer support team can get notifications on failures and take appropriate action:</p>
<p><img src="https://cdn.prod.website-files.com/5f16ec6886fb3fb049008f9a/6608227706f6bdfff275405f_catch%20handler%20config.webp" alt class="image--center mx-auto" /></p>
<p>Configuration of the “Catch” handlers in steps</p>
<p>To handle “non-success” payments in the customer session, we can define a <strong>default rule</strong> in the “Choice Step” for Step 2. This allows us to define where to go if the rest of the choices are not evaluated as true, as shown in the image below:</p>
<p><img src="https://cdn.prod.website-files.com/5f16ec6886fb3fb049008f9a/66082291e825fc2c6007e713_non%20success%20payment%20rule.webp" alt="Image that shows the configuration of the step function “default” state in the “choice” state. The “Default state” is chosen as “Send Notification to Support Team”" class="image--center mx-auto" /></p>
<p>Configuration of the step function “choice” state for default rule</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>In summary, this article outlines a streamlined approach for automating post-payment activities through AWS Step Functions, emphasizing the orchestration of tasks, parallel processing, and error management. By comparing orchestrator and choreographer patterns, it highlights the effectiveness of a centralized orchestration model, implemented in a practical AWS Step Functions workflow. This solution not only meets the requirements of post-payment processing  of a typical e-commerce application, but also offers scalability, reliability, and seamless integration with AWS services, presenting a forward-looking framework for handling complex workflows in cloud environments.</p>
<h1 id="heading-references">References</h1>
<ul>
<li><p><a target="_blank" href="https://aws.amazon.com/step-functions/">https://aws.amazon.com/step-functions/</a></p>
</li>
<li><p><a target="_blank" href="https://aws.amazon.com/blogs/compute/orchestrating-dependent-file-uploads-with-aws-step-functions/">https://aws.amazon.com/blogs/compute/orchestrating-dependent-file-uploads-with-aws-step-functions/</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html">https://docs.aws.amazon.com/step-functions/latest/dg/welcome.html</a></p>
</li>
<li><p><a target="_blank" href="https://camunda.com/blog/2023/02/orchestration-vs-choreography/">https://camunda.com/blog/2023/02/orchestration-vs-choreography/</a></p>
</li>
<li><p><a target="_blank" href="https://docs.aws.amazon.com/lambda/latest/dg/services-stepfunctions.html">https://docs.aws.amazon.com/lambda/latest/dg/services-stepfunctions.html</a></p>
</li>
</ul>
]]></content:encoded></item></channel></rss>