<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Nicola Cremaschini</title>
    <description>The latest articles on Forem by Nicola Cremaschini (@niccrema).</description>
    <link>https://forem.com/niccrema</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2053625%2Feae18d90-dfee-4c84-9f5f-09144c1b74a7.jpeg</url>
      <title>Forem: Nicola Cremaschini</title>
      <link>https://forem.com/niccrema</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/niccrema"/>
    <language>en</language>
    <item>
      <title>Spec-Driven Prototyping with Amazon Q and Q-Vibes Memory Banking framework</title>
      <dc:creator>Nicola Cremaschini</dc:creator>
      <pubDate>Mon, 21 Jul 2025 06:00:52 +0000</pubDate>
      <link>https://forem.com/aws-builders/spec-driven-prototyping-with-amazon-q-and-q-vibes-memory-banking-framework-4lc3</link>
      <guid>https://forem.com/aws-builders/spec-driven-prototyping-with-amazon-q-and-q-vibes-memory-banking-framework-4lc3</guid>
      <description>&lt;p&gt;From ideas to prototypes&lt;/p&gt;

&lt;p&gt;We all love when an idea hits sharp, exciting, half-formed. But getting from that spark to something tangible often involves friction: scaffolding, repetition, boilerplate.&lt;/p&gt;

&lt;p&gt;That overhead can kill &lt;strong&gt;momentum&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Prototyping is how we protect the idea. Its the creative phase where we validate assumptions, test viability, and explore possibilities quickly.&lt;/p&gt;

&lt;p&gt;When done well, prototyping accelerates &lt;strong&gt;innovation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Prototyping is a preliminary phase of product development and has its own objectives and constraints that differ from those of other phases of product development.&lt;/p&gt;

&lt;p&gt;In prototyping, you don't yet have a clear idea of the end product. You don't even know if the idea is really good enough to become a product.&lt;/p&gt;

&lt;p&gt;For this reason, we can define these attributes/constraints for prototypes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;They should be &lt;strong&gt;cheap&lt;/strong&gt;. Their realisation should take little money and time, therefore they should be developed by one or two people and not by a whole product team. You must not invest a lot of time in requirements gathering, design, development, etc.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You don't have to share it with a global audience: You can present it to investors, but while you're driving the presentation. A prototype is neither a demo nor a product preview, so you don't have to make it available to others. This means you &lt;strong&gt;don't have to deploy it,&lt;/strong&gt; just run it in a closed environment (maybe your local environment).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Perhaps neither &lt;strong&gt;real data nor real integrations are needed&lt;/strong&gt; if the idea can be explored without them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It's a &lt;strong&gt;throwaway&lt;/strong&gt; : you don't need to extend it to production development. Once the idea has been explored and validated, throw away your prototype and start defining and developing your product.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With generative AI tools such as Amazon Q, Claude or Cursor, prototyping is faster than ever before. This is where vibe coding comes into play.&lt;/p&gt;

&lt;p&gt;Some might think that vibe coding just means that the AI generates random code that you don't understand and that it's not real engineering: that may be true, a drum kit can also just be used to make noise (as my neighbour says).&lt;/p&gt;

&lt;p&gt;Nowadays, vibe coding is often presented as in contast with spec-driven development. In my humble opinion, we could take advantage of vibe coding by directing agents providing them &lt;strong&gt;just enough specs for the goal.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As an engineer, I don't need perfect code when prototyping, but I don't want random code either.&lt;/p&gt;

&lt;h1&gt;
  
  
  Context matters
&lt;/h1&gt;

&lt;p&gt;Generative AI is deeply contextual every response depends on what came before. Even small shifts in input can produce wildly different output. Thats both a strength and a weakness.&lt;/p&gt;

&lt;p&gt;When you're coding by vibe, the AI doesnt truly "know" your intent it infers it. Without clear, consistent context, things go off-track fast:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;You change a prompt slightly, and the AI drops half the logic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A missed constraint (like region or tech stack) leads to subtle regressions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A well-intentioned refactor undoes previous alignment.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Context matters not just in what you say, but &lt;em&gt;what the AI sees every time&lt;/em&gt;. Thats why a reusable, declarative context is so powerful: it's about creating a shared space where your intent lives.&lt;/p&gt;

&lt;p&gt;Structure it, store it, and tell the AI how to handle it and youve got &lt;strong&gt;spec-driven development.&lt;/strong&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  The memory problem
&lt;/h1&gt;

&lt;p&gt;LLMs are brilliant, but forgetful. Especially across sessions or as prompts grow. When prototyping, this causes friction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Goals get redefined without you noticing&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tech stack or constraints subtly change&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Errors repeat because nothing was "remembered"&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The deeper the session, the worse the drift. At some point, youre no longer prototyping youre re-aligning.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;spec-driven prototyping&lt;/strong&gt; enters.&lt;/p&gt;

&lt;p&gt;Writing a simple spec, just a lightweight set of goals, guardrails, and preferred stack helps anchor the AIs responses. It makes collaboration reproducible.&lt;/p&gt;

&lt;p&gt;Combine that with a way to update and feed this spec consistently to your assistant and youve got &lt;strong&gt;memory banking&lt;/strong&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  Introducing Q-Vibes memory banking framework
&lt;/h1&gt;

&lt;p&gt;In my &lt;a href="https://dev.to/aws-builders/building-think-o-matic-a-vibe-coding-journey-with-amazon-q-280l"&gt;last article&lt;/a&gt;, I reported on my direct experience of building my Think-O-Matic prototype with Amazon Q, gave a definition of vibe coding and briefly introduced the concept of memory banking.&lt;/p&gt;

&lt;p&gt;After this experience, I developed a memory banking framework specifically for rapid prototyping with Amazon Q: It's open-source (contributions are welcome!) and &lt;a href="https://github.com/ncremaschini/amazon-q-vibes-memory-banking" rel="noopener noreferrer"&gt;you can find it on github.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To use the framework you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;an idea to explore&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Amazon Q (both CLI or IDEs plugin)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The framework consists in specifications (provided to agent by .md files) and prompts (provided to agent by you via chat).&lt;/p&gt;

&lt;h2&gt;
  
  
  Specifications
&lt;/h2&gt;

&lt;p&gt;Specs consists in 5 MD files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/ncremaschini/amazon-q-vibes-memory-banking/blob/main/q-vibes-memory-banking.md" rel="noopener noreferrer"&gt;q-vibes-memory-banking.md&lt;/a&gt; - the AI Contract. This contains the complete framework instructions that tell the AI &lt;strong&gt;how&lt;/strong&gt; to work with memory banking when initiating a new session, resuming a session and updating docs at the end of an iteration. This is provided, no need to edit it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/ncremaschini/amazon-q-vibes-memory-banking/blob/main/templates/idea.md" rel="noopener noreferrer"&gt;idea.md&lt;/a&gt;: Captures the core concept and success criteria for your prototype. This is your north star - created once and rarely changes. The AI creates this from your initial description, but may ask clarifying questions to complete all sections using the template structure.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/ncremaschini/amazon-q-vibes-memory-banking/blob/main/templates/vibe.md" rel="noopener noreferrer"&gt;vibe.md&lt;/a&gt;: Defines how you want to collaborate with the AI assistant. Specifies your interaction style, tech stack preferences, decision-making approach, git workflow, security practices, documentation requirements, and speed vs quality trade-offs. You have to create and mantain it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/ncremaschini/amazon-q-vibes-memory-banking/blob/main/templates/state.md" rel="noopener noreferrer"&gt;state.md&lt;/a&gt;: The living technical snapshot of your prototype. Updated frequently by the AI as you build. Contains current stack, architecture overview, file structure, what's working/broken, immediate next steps, and current focus.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/ncremaschini/amazon-q-vibes-memory-banking/blob/main/templates/decisions.md" rel="noopener noreferrer"&gt;decisions.md&lt;/a&gt;: Log of key choices made during development. Prevents re-discussing the same decisions. The AI creates and maintains this file as architectural and technical decisions are made, following the template structure.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes the framework complete and self-contained. The AI gets both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;How to work&lt;/strong&gt; (from the framework instructions in &lt;code&gt;q-vibes-memory-banking.md&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What to work on&lt;/strong&gt; (from the 4 context files: &lt;code&gt;idea.md&lt;/code&gt;, &lt;code&gt;vibe.md&lt;/code&gt;, &lt;code&gt;state.md&lt;/code&gt;, &lt;code&gt;decisions.md&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;Let's say you have a brilliant idea and you want to explore it.&lt;/p&gt;

&lt;p&gt;All you need is to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;create a project folder.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;create a &lt;em&gt;.amazonq/vibes&lt;/em&gt; sub-folder and copy templates inside it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;create your &lt;em&gt;vibes.md&lt;/em&gt;, you can start from the template or the provided example.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;prompt your idea and clarify it with the agent.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your prompt should be something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hi! I want to start a new prototype using Q-Vibes Memory Banking.
Please read the framework instructions in .amazonq/vibes/q-vibes-memory-banking.md first to understand how to work with this system.
My prototype idea: [Describe your idea here - can be brief, just the core concept]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent would ask you for further clarification and confirmation of his assumptions, with the intention of narrowing and clarifying the scope.&lt;/p&gt;

&lt;p&gt;Resuming a session is even easier. Just prompt the agent with a simple request to pick up where you left off. Something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hi! I'm resuming work on my prototype using Q-Vibes Memory Banking.
Please read the framework instructions in .amazonq/vibes/q-vibes-memory-banking.md first, then read all the context files in .amazonq/vibes/ folder to understand the current state.
Once you've reviewed everything, please confirm what we're building, where we left off, and what the next steps should be.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please read the &lt;a href="https://github.com/ncremaschini/amazon-q-vibes-memory-banking/blob/main/README.md" rel="noopener noreferrer"&gt;README.md&lt;/a&gt; of the project for a quick setup, complete instructions and a running example.&lt;/p&gt;

&lt;p&gt;Note that you and the agent are &lt;strong&gt;jointly responsible&lt;/strong&gt; for ensuring that the specifications are clear and match. There is no magic here: the better the input, the better the output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benefits:
&lt;/h2&gt;

&lt;p&gt;The key benefits are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;it is very fast: i created the &lt;a href="https://github.com/ncremaschini/amazon-q-vibes-memory-banking/tree/main/examples/builder-tracker" rel="noopener noreferrer"&gt;example provided&lt;/a&gt; in less than 1 hour, while also testing session resuming.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;you provide guard rails: not arbitrary code, but code that suits your needs and style&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AI helps you explore your idea: In my experience, the agent's questions helped narrow down my ideas better&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;no loss of context: you don't have to provide context to the agent&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;it's very fast: I created the &lt;a href="https://github.com/ncremaschini/amazon-q-vibes-memory-banking/tree/main/examples/builder-tracker" rel="noopener noreferrer"&gt;example&lt;/a&gt; in less than 1 hour and also tested the resumption of sessions.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Prototype Memory Product Memory
&lt;/h1&gt;

&lt;p&gt;You might be asking yourself:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why do we need a specific framework? Why not just use a full spec-driven development framework?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because &lt;strong&gt;prototyping has different goals&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Its not just an early phase of development its a different mode entirely.&lt;/p&gt;

&lt;p&gt;In the prototyping phase, youre optimizing for &lt;strong&gt;speed, creativity, and cost-efficiency&lt;/strong&gt; not for durability, scalability, or perfect accuracy. You want to explore, validate, and iterate quickly. That means you can (and should) tolerate some messiness and manual steps, as long as they accelerate learning.&lt;/p&gt;

&lt;p&gt;Thats why the memory needs during prototyping are also different.&lt;/p&gt;

&lt;p&gt;You dont need a persistent, multi-session memory graph. You need just enough structure to help your AI collaborator stay aligned through a rapid, idea-driven loop.&lt;/p&gt;

&lt;p&gt;This framework isnt built for production agents or end-user memory systems. Its not meant to manage complexity across months or teams.&lt;/p&gt;

&lt;p&gt;Instead, its designed for that &lt;strong&gt;middle space between a blank prompt and full-stack dev&lt;/strong&gt; where ideas are still forming, and flow matters more than polish.&lt;/p&gt;

&lt;p&gt;If youre in that zone, a lightweight memory bank gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Direction without rigidity&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Consistency without ceremony&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Momentum without drift&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Conclusions
&lt;/h1&gt;

&lt;p&gt;Vibe-coding is here to stay and its magical when it works. But even vibes need a spine.&lt;/p&gt;

&lt;p&gt;This lightweight memory banking approach gives you structure &lt;em&gt;just enough&lt;/em&gt; to stay aligned, while keeping the creative momentum alive.&lt;/p&gt;

&lt;p&gt;If youre prototyping with Amazon Q or any other Agent / LLM, give it a try.&lt;/p&gt;

&lt;p&gt;The framework is tailored on Amazon Q, but not bounded to it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hashnode.com/@darshitpandya" rel="noopener noreferrer"&gt;Darshit Pandya&lt;/a&gt; (you can find him also on &lt;a href="https://www.linkedin.com/in/darshitpandya/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;) created a version tailored on &lt;a href="https://github.com/Darshitpandya/github-copilot-context-keeper" rel="noopener noreferrer"&gt;Github Copilot&lt;/a&gt;, and we are going to benchmark the framework against the two agents to measure its performance and collaborate to improve it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ncremaschini/amazon-q-vibes-memory-banking" rel="noopener noreferrer"&gt;Check out the framework, fork it&lt;/a&gt;, remix it, build something weird, share it.&lt;/p&gt;

&lt;p&gt;Because vibes are better when they remember what theyre building.&lt;/p&gt;

&lt;p&gt;Specs, just enough.&lt;/p&gt;

</description>
      <category>amazonqdevelopercli</category>
      <category>amazonwebservices</category>
      <category>generativeai</category>
      <category>coding</category>
    </item>
    <item>
      <title>Building Think-o-matic: A Vibe-Coding Journey with Amazon Q</title>
      <dc:creator>Nicola Cremaschini</dc:creator>
      <pubDate>Sun, 22 Jun 2025 22:39:16 +0000</pubDate>
      <link>https://forem.com/aws-builders/building-think-o-matic-a-vibe-coding-journey-with-amazon-q-280l</link>
      <guid>https://forem.com/aws-builders/building-think-o-matic-a-vibe-coding-journey-with-amazon-q-280l</guid>
      <description>&lt;p&gt;Weve all had those moments where inspiration strikes, but the traditional coding workflow planning, scaffolding, testing, debugging feels like too much friction.&lt;/p&gt;

&lt;p&gt;What if instead, you could &lt;strong&gt;prototype&lt;/strong&gt; with a different mindset? One that prioritizes momentum, creativity, and &lt;em&gt;just enough&lt;/em&gt; structure to explore an idea?&lt;/p&gt;

&lt;p&gt;Thats where &lt;strong&gt;vibe-coding&lt;/strong&gt; comes in.&lt;/p&gt;

&lt;p&gt;This unconventional approach flips the traditional dev cycle on its head. You dont manually write every line or carefully craft a layered architecture instead, you &lt;strong&gt;describe what you want&lt;/strong&gt; , and let AI tools do the heavy lifting.&lt;/p&gt;

&lt;p&gt;Vibe-coding isnt about writing perfect code. Its about describing intent, trusting the process, and shipping fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vibe-coding: what it is and why it matters
&lt;/h2&gt;

&lt;p&gt;This term comes out from &lt;a href="https://x.com/karpathy/status/1886192184808149383" rel="noopener noreferrer"&gt;this Andrej Karpathys X post&lt;/a&gt;, just few weeks ago, and the industry is trying to converge on a definition and to standardize it.&lt;/p&gt;

&lt;p&gt;Few weeks later, distinguished authors are writing and publishing books about this techniuqe, just the mention a few&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://a.co/d/fvC54LH" rel="noopener noreferrer"&gt;Vibe-Coding by Gene Kim and Steve Yegge&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://a.co/d/iqwhB5u" rel="noopener noreferrer"&gt;Beyond Vibe-Coding by Addy Osmani&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I tried to summarize the key points of vibe-coding from Karpathys post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vibe-coding&lt;/strong&gt; is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Letting AI tools write, fix, and modify the code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Embracing &lt;em&gt;feel&lt;/em&gt; and &lt;em&gt;flow&lt;/em&gt; over full code comprehension.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Typing as little as possible mostly just &lt;em&gt;describe, accept, and run&lt;/em&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Skipping diffs, skimming errors, and trusting AI suggestions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Supervising the AI rather than driving every keystroke.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not traditional coding. Its prototyping for the AI-native era perfect for weekend projects, experiments, or validating ideas before investing in full-scale development.&lt;/p&gt;

&lt;p&gt;If you ever had some idea about building something on your own, it is very clear why it matters: it makes prototyping fast, cheap, and easy.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;A Quick Reminder: Whats Prototyping?&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;In software engineering, prototyping is about:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Creating a preliminary version of a system to explore ideas, validate functionality, and gather user feedback before full-scale development.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Its low-commitment, fast-paced, and feedback-driven which makes it the perfect playground for AI-powered workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fast-prototyping steps:
&lt;/h2&gt;

&lt;p&gt;I have tried to define some steps to structure my vibe coding sessions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Have an idea: this seems obvious, but it's not. You can create something if you have an idea that is clear enough to be built and executed, but also leaves some room for exploration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Set up tools: You need a toolbox that is easy to set up, quick, cheap and that you trust. In these times, I don't think it's worth spending too much time finding the perfect tools or optimizing them: Your perfect tool could be obsolete tomorrow.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Describe your idea: This means you tell your tools what you want to build together.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Follow the vibes: This step is actually an inner loop consisting of three sub-steps:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enough is enough: you are building a prototype, not a product: this means you are not looking for perfection nor a complete system.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;i have schematized these steps like this:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  &lt;strong&gt;A Real Example: Building Think-o-matic&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;To make this more tangible, heres how I prototyped a tool called &lt;strong&gt;Think-o-matic&lt;/strong&gt; an AI-powered copilot to help structure workshops, generate agendas, create Miro boards, and summarize outcomes into actionable Trello tasks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Having an idea
&lt;/h3&gt;

&lt;p&gt;My ideas comes out most of times from real-life problems that i cant fix. Im often wondering if i could build something to help me, and this helps me in three ways:&lt;/p&gt;

&lt;p&gt;First, i love build, i find it fun.&lt;/p&gt;

&lt;p&gt;Second, i go deep in my problem understanding: if you want a solution, you have to taget you problem.&lt;/p&gt;

&lt;p&gt;Third, i got the problem solved! One less&lt;/p&gt;

&lt;p&gt;This is exactly what happend with Thinlk-o-Matic. In my current job role i need to put stakeholders around a table, often virtual, and making them working togheter to target problems, find solutions and explore ideas. In other words, i need to extract information from them, and one effective way to do this is to running workshops.&lt;/p&gt;

&lt;p&gt;Running a workshop involves the following steps, and that is exactly what i need help on:&lt;/p&gt;

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

&lt;p&gt;Between steps 3 and 4 there is the workshop itself.&lt;/p&gt;

&lt;p&gt;I also draft a high-level architecture of the prototype:&lt;/p&gt;

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

&lt;p&gt;A front-end webapp backend by an ExpressJS server running on nodejs environment, that provides integration to miro and trello, and to Amazon Bedrock to provide intelligence to the system: Amazon Nova would generate the workshop agenda and summarize the Miro Board.&lt;/p&gt;

&lt;h3&gt;
  
  
  Set up tools
&lt;/h3&gt;

&lt;p&gt;My toolbox is very easy: terminal, Amazon Q CLI (agentic) backed by Claude Sonnet 4.0. That is.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Describe your idea
&lt;/h3&gt;

&lt;p&gt;I used a greatly simplified version of a technique called memory banking that I learned from &lt;a href="https://cline.bot/blog/memory-bank-how-to-make-cline-an-ai-agent-that-never-forgets" rel="noopener noreferrer"&gt;this blog post from Cline.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In a few words, its a way to remember what's going on in the project and between you and the agent, because otherwise it would easily forget whats going on and have to recreate the context for your agent, leaving the vibes.&lt;/p&gt;

&lt;p&gt;Creating a memory bank helps your AI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Stay aligned with goals across sessions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Remember decisions already made.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reduce repetition and confusion.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In vibe-coding, memory banking becomes your anchor keeping prototypes from drifting too far off course.&lt;/p&gt;

&lt;p&gt;I have provided two files:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ncremaschini/think-o-matic-q/blob/main/.amazonq/specs/prototypes_general_guidelines.md" rel="noopener noreferrer"&gt;Prototype Guidelines&lt;/a&gt;: Instructions on what is a prototype, what is not, and how I want to build prototypes. This prompt does not refer to a specific prototype and is reusable&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ncremaschini/think-o-matic-q/blob/main/.amazonq/specs/thinkomatic_specific_guidelines.md" rel="noopener noreferrer"&gt;Think-o-matic specific guidelines&lt;/a&gt;: Instructions about this specific idea.&lt;/p&gt;

&lt;p&gt;The prompt style is a mix of the Risen framework (role, input, steps, expectation, narrowing) and the Rodes framework (role, objectives, details, examples, sense check).&lt;/p&gt;

&lt;p&gt;Again, I don't want to spend too much time on prompting, and of course I used LLMs to write my prompts.&lt;/p&gt;

&lt;p&gt;My session started with these two files in a folder and a little prompt that went something like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;before doing anything, read these two files and tell me what you think about. Please use the same folder to create your checkpoint files.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this way i also instructed the agent to &lt;em&gt;update&lt;/em&gt; the memory bank while going on with the work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Follow the vibes + Check Results + Make friends
&lt;/h3&gt;

&lt;p&gt;After this little prompt, the agent red the specs i provided and proposed me an action plan. We agreed on the steps and to check-in with me after every step.&lt;/p&gt;

&lt;p&gt;The first iteration result was this:&lt;/p&gt;

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

&lt;p&gt;Basically the first iteration was the working app with all integration mocked, in about 15 minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;What Went Well vs. What Went Weird&lt;/strong&gt;
&lt;/h3&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;AI nailed the folder structure and scaffolding.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The prototype ran with minimal setup.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I stayed in the creative zone.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Wrong AWS region.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No documentation, even if i asked for.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Laughably bad UX.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A few silly bugs.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But thats okay vibe-coding isnt about perfection. Its about fast feedback and learning by doing.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Making Friends: Tune Your Cooperation Style&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Vibe-coding isnt autopilot. Youre not giving up control youre adjusting how you cooperate.&lt;/p&gt;

&lt;p&gt;I think of this part as &lt;strong&gt;making friends&lt;/strong&gt; with the AI. Like any relationship, it needs clear communication and trust but also healthy boundaries.&lt;/p&gt;

&lt;p&gt;Heres a real example: I forgot to specify the AWS Region in a prompt. The AI defaulted to us-east-1 (no idea why). I needed a &lt;strong&gt;Bedrock model&lt;/strong&gt; that was only enabled in eu-west-1. Instead of asking me, the agent silently changed the model to something available in us-east-1.&lt;/p&gt;

&lt;p&gt;Thats when I stepped in. I told the agent:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;For small things, go ahead. But for big architectural decisions &lt;strong&gt;ask me.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That balance is key. You want the AI to be proactive, but aligned. Let it move fast just not in the wrong direction.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  The final result: think-o-matic
&lt;/h2&gt;

&lt;p&gt;I worked with the agent for a few hours: We added one feature after another: Agenda creation, Miro Board creation, Miro Board summary, Trello integration.&lt;/p&gt;

&lt;p&gt;For each feature implemented, Q updated the memory bank.&lt;/p&gt;

&lt;p&gt;The result? You can try it out for yourself by running it locally.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ncremaschini/think-o-matic-q" rel="noopener noreferrer"&gt;Here is the github repo with the code and instructions to run it.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you look at the repo, you might wonder why there is only one branch and one commit: I created it as a private repo and before I made it public, I searched it for secrets and found that Q wrote my secrets to the memory bank. Trust, but verify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This is just from my own notes i took after those two hours:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;do&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;don't&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;State clear goals&lt;/td&gt;
&lt;td&gt;Over-engineer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Define wont do&lt;/td&gt;
&lt;td&gt;Forget the code exists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use memory banks&lt;/td&gt;
&lt;td&gt;Ask for endless validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Work in small chunks&lt;/td&gt;
&lt;td&gt;Force AI to stick to one approach&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create checkpoints&lt;/td&gt;
&lt;td&gt;Ignore drift, it happens!&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tune your cooperation style&lt;/td&gt;
&lt;td&gt;Expect the AI to guess your intent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pro tip: Let the AI drift &lt;em&gt;a bit&lt;/em&gt;. Sometimes, the best ideas emerge sideways.&lt;/p&gt;

&lt;h2&gt;
  
  
  Waiting for the Doom Moment
&lt;/h2&gt;

&lt;p&gt;A few weeks ago, I had the pleasure of leading a &lt;a href="https://www.meetup.com/the-cloud-house/events/306876067/" rel="noopener noreferrer"&gt;roundtable discussion with Jeff Barr&lt;/a&gt;, Chief Evangelist for AWS and one of the most influential engineers in software engineering and cloud computing, and I asked him about the future of Gen AI. I asked, whats next?&lt;/p&gt;

&lt;p&gt;He responded with a story from 1992 - 1993&lt;/p&gt;

&lt;p&gt;in 1992, we all loved &lt;a href="https://en.wikipedia.org/wiki/Wolfenstein_3D" rel="noopener noreferrer"&gt;Wolfstein 3D.&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Despite the name, it was not real 3D, and it was the first in-person shooter game.&lt;/p&gt;

&lt;p&gt;A year later, John Carmack developed the &lt;a href="https://en.wikipedia.org/wiki/Doom_engine" rel="noopener noreferrer"&gt;Doom Engine&lt;/a&gt; using the same technology, and we were all shocked by Doom&lt;/p&gt;

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

&lt;p&gt;Thats where we are with AI and prototyping right now.&lt;/p&gt;

&lt;p&gt;Were still building Wolfensteins.&lt;/p&gt;

&lt;p&gt;But Doom is coming.&lt;/p&gt;

</description>
      <category>vibecoding</category>
      <category>amazonq</category>
      <category>prototyping</category>
      <category>ai</category>
    </item>
    <item>
      <title>Building Atomic Counters with Amazon DocumentDB</title>
      <dc:creator>Nicola Cremaschini</dc:creator>
      <pubDate>Mon, 17 Mar 2025 10:34:57 +0000</pubDate>
      <link>https://forem.com/aws-builders/building-atomic-counters-with-amazon-documentdb-o8a</link>
      <guid>https://forem.com/aws-builders/building-atomic-counters-with-amazon-documentdb-o8a</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;Introduction&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This is the final installment in my &lt;a href="https://haveyoutriedrestarting.com/series/atomic-counter" rel="noopener noreferrer"&gt;atomic counters series&lt;/a&gt; where I explore different distributed databases and how they implement atomic counters.&lt;/p&gt;

&lt;p&gt;This time, were looking at &lt;a href="https://aws.amazon.com/documentdb/" rel="noopener noreferrer"&gt;Amazon DocumentDB&lt;/a&gt; a managed NoSQL document database, &lt;a href="https://www.mongodb.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;MongoDB-compatible&lt;/strong&gt;&lt;/a&gt; and optimized for AWS.&lt;/p&gt;

&lt;p&gt;Atomic counters are a common requirement in distributed applications, whether for tracking views, managing inventory, or implementing rate limiting.&lt;/p&gt;

&lt;p&gt;In this article, well discuss how &lt;strong&gt;DocumentDB handles atomic updates&lt;/strong&gt; and explore a &lt;strong&gt;working example&lt;/strong&gt; from my &lt;a href="https://github.com/ncremaschini/atomic-counter" rel="noopener noreferrer"&gt;GitHub repository.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Serializability and Linearizability in DocumentDB&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before we can dive deep into code, we need to recall few concepts (please refer to &lt;a href="https://dev.to/niccrema/atomic-counter-framing-the-problem-space-324b-temp-slug-9085526"&gt;&lt;strong&gt;the first article of this series for a detailed explanation&lt;/strong&gt; )&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Serializability&lt;/strong&gt; : Operations appear in a consistent sequential order, ensuring correctness.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Linearizability&lt;/strong&gt; : Writes are immediately visible for subsequent reads, ensuring real-time consistency.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;DocumentDB achieves linearizable writes through its &lt;strong&gt;single-primary, multi-replica architecture&lt;/strong&gt; :&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write operations are directed to the primary instance&lt;/strong&gt; , and changes are asynchronously replicated to secondaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read operations from the primary always return the latest committed value&lt;/strong&gt; , ensuring linearizability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Replica reads may return stale data&lt;/strong&gt; due to replication lag, meaning they are eventually consistent.&lt;/p&gt;

&lt;p&gt;This guarantees that &lt;strong&gt;atomic updates within a single document, like counters using the $inc operator, remain correct and isolated&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;While not required in this specific scenario, it is worth to mention DocumentDB supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/documentdb/latest/developerguide/how-it-works.html#durability-consistency-isolation" rel="noopener noreferrer"&gt;read isolation level configuration&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/documentdb/latest/developerguide/transactions.html" rel="noopener noreferrer"&gt;transactions and their isolation level, read and write concerns configuration.&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Replication and Leader Election in DocumentDB
&lt;/h2&gt;

&lt;p&gt;DocumentDB automatically replicates data across multiple availability zones to ensure durability and availability.&lt;/p&gt;

&lt;p&gt;Key mechanisms include:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single-primary replication&lt;/strong&gt; : A single primary instance handles writes, while replicas asynchronously replicate data and serve read requests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leader election&lt;/strong&gt; : If the primary instance fails, DocumentDB automatically promotes a replica to primary, minimizing downtime and maintaining availability.&lt;/p&gt;

&lt;p&gt;This replication strategy allows DocumentDB to scale reads across replicas while ensuring that writes remain &lt;strong&gt;strongly consistent&lt;/strong&gt; on the primary.&lt;/p&gt;

&lt;p&gt;However, applications must account for &lt;strong&gt;eventual consistency&lt;/strong&gt; when reading from replicas due to asynchronous replication.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Atomic Counter Pattern
&lt;/h1&gt;

&lt;p&gt;The atomic counter pattern enables precise increment operations, even in distributed environments.&lt;/p&gt;

&lt;p&gt;With DocumentDB, you use the &lt;strong&gt;$inc operator&lt;/strong&gt; , which atomically increments a numeric field within a document.&lt;/p&gt;

&lt;p&gt;This ensures that &lt;strong&gt;concurrent increments are safely serialized&lt;/strong&gt; without race conditions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DocumentDB supports conditional increments natively&lt;/strong&gt; : you can use &lt;strong&gt;$inc with $cond in an update operation&lt;/strong&gt; to increment the counter only when certain conditions are met &lt;strong&gt;all in a single atomic operation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This makes DocumentDB a good choice when you need &lt;strong&gt;both unconditional and conditional increments&lt;/strong&gt; , ensuring correctness without requiring complex client-side logic.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Hands-on! Walkthrough of the Deployable Example&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Lets examine how the deployable example in &lt;a href="https://github.com/ncremaschini/atomic-counter" rel="noopener noreferrer"&gt;&lt;strong&gt;this GitHub repository&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This example demonstrates how to implement an atomic counter using &lt;strong&gt;AWS Lambda&lt;/strong&gt; , &lt;strong&gt;API Gateway&lt;/strong&gt; , and &lt;strong&gt;DocumentDB&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;API Gateway&lt;/strong&gt; : Provides HTTP endpoints for interacting with the counter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lambda Functions&lt;/strong&gt; : Implements the business logic for incrementing the counter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DocumentDB&lt;/strong&gt; : Stores the counters.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;In my example project you can decide wheter to use a maximum value for the counter or not: this determine if use or not conditional writes.&lt;/p&gt;

&lt;p&gt;Lets focus on lambda business logic, &lt;strong&gt;from the&lt;/strong&gt; &lt;a href="https://github.com/ncremaschini/atomic-counter/blob/main/lib/lambda/documentDB/counterLambda/index.ts" rel="noopener noreferrer"&gt;docDbCounterLambda&lt;/a&gt; code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const documentDBClient = await buildDocumentDbClient();
await documentDBClient.connect();
const countersCollection = documentDBClient.db("atomic_counter").collection('counters');
const updateFilter = getUpdateFilter(useConditionalWrites, id, maxCounterValue);
const updateResult = await countersCollection.updateOne( updateFilter, { $inc: { atomic_counter: 1 } }, { upsert: true, });
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here I use the &lt;strong&gt;$inc&lt;/strong&gt; operator with the &lt;em&gt;upsert&lt;/em&gt; flag set to true: this makes the method work both the first time, when the counter does not exist and is therefore initialized to zero, and for further increment operations.&lt;/p&gt;

&lt;p&gt;What changes between conditional and unconditional write operations is the &lt;em&gt;updateFilter&lt;/em&gt; returned by the &lt;em&gt;getUpdateFilter&lt;/em&gt; method.&lt;/p&gt;

&lt;p&gt;Lets have a look at it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const getUpdateFilter = (useConditionalWrites: boolean, id: number, maxCounterValue: string) =&amp;gt; { 
const unconditionalWriteParams = { counter_id: id } 
const conditionalWriteParams = { counter_id: id, $and: [{ atomic_counter: { $lt: Number(maxCounterValue) } }], } 
return useConditionalWrites ? conditionalWriteParams : unconditionalWriteParams;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For unconditional writes, the only filter is the &lt;em&gt;counter_id&lt;/em&gt; attribute.&lt;/p&gt;

&lt;p&gt;For conditional writes, the construct &lt;strong&gt;$lt&lt;/strong&gt; (lower than) is added as an additional condition to check whether the value is below the maximum value.&lt;/p&gt;

&lt;p&gt;Since the update is performed for a single document and the increment operation is performed on the server side, atomicity is guaranteed and the counter value cannot exceed the maximum value&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhhmycrrr8ryzarg8r44n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhhmycrrr8ryzarg8r44n.png" alt="if two concurrrent increments are requested and the second one would exceed the maximum value, the first is accepted while the second is rejected" width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Trade-Offs and Conclusion&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Like other databases in this series, DocumentDB comes with &lt;strong&gt;trade-offs&lt;/strong&gt; when used for atomic counters:&lt;/p&gt;

&lt;h2&gt;
  
  
  Strenghts:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;MongoDB Compatibility&lt;/strong&gt; : Developers familiar with MongoDB can reuse existing knowledge.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Managed Scaling&lt;/strong&gt; : AWS handles &lt;strong&gt;replication, backups, and failover&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Atomic Updates on a Single Document&lt;/strong&gt; : &lt;strong&gt;$inc&lt;/strong&gt; ensures updates are atomic.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limitations:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Eventual Consistency for Replicas&lt;/strong&gt; : Secondary reads may return stale data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Higher Latency for Stronger Consistency&lt;/strong&gt; : To ensure &lt;strong&gt;fresh data&lt;/strong&gt; , queries must be sent to the primary instance.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Key Takeaways:&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Atomic counters in DocumentDB&lt;/strong&gt; can be implemented using the &lt;strong&gt;$inc&lt;/strong&gt; operator, ensuring &lt;strong&gt;atomic updates at the document level&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Conditional increments are fully supported&lt;/strong&gt; using &lt;strong&gt;$inc&lt;/strong&gt; combined with &lt;strong&gt;$cond&lt;/strong&gt; , allowing for server-side enforcement of constraints.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DocumentDB follows a single-primary, multi-replica model&lt;/strong&gt; , meaning writes are &lt;strong&gt;strongly consistent&lt;/strong&gt; , but replica reads may be &lt;strong&gt;eventually consistent&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automatic leader election&lt;/strong&gt; ensures high availability by promoting a replica to primary in case of failure.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can find the &lt;strong&gt;full runnable example&lt;/strong&gt; in my GitHub repository: &lt;a href="https://github.com/ncremaschini/atomic-counter" rel="noopener noreferrer"&gt;atomic-counter&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This marks the end of the &lt;strong&gt;atomic counter series&lt;/strong&gt;! 🚀&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>mongodb</category>
      <category>awsdocumentdb</category>
    </item>
    <item>
      <title>Building Atomic Counters with TiDB</title>
      <dc:creator>Nicola Cremaschini</dc:creator>
      <pubDate>Sun, 16 Feb 2025 18:00:04 +0000</pubDate>
      <link>https://forem.com/aws-builders/building-atomic-counters-with-tidb-2lji</link>
      <guid>https://forem.com/aws-builders/building-atomic-counters-with-tidb-2lji</guid>
      <description>&lt;p&gt;Distributed SQL databases have become a cornerstone for applications that require &lt;strong&gt;global scalability and strong consistency&lt;/strong&gt; , and this problem has existed since the very first deployment of a database on two distinct servers: how to achieve strong consistency and scalability without compromising availability?&lt;/p&gt;

&lt;p&gt;Is it possible?&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://en.wikipedia.org/wiki/CAP_theorem" rel="noopener noreferrer"&gt;CAP theorem&lt;/a&gt; states that this isn't possible: if you consider consistency, availability and partitioning as fundamental properties of data storage, you can only choose two out of three properties.&lt;/p&gt;

&lt;p&gt;In this fourth part of the series on atomic counters, we'll explore how the pattern can be implemented using &lt;a href="https://www.pingcap.com/tidb-cloud-serverless/" rel="noopener noreferrer"&gt;TiDB&lt;/a&gt;, &lt;a href="https://en.wikipedia.org/wiki/NewSQL" rel="noopener noreferrer"&gt;NewSql's&lt;/a&gt; database class, as an example, focusing on global partitioning, strong consistency and high availability.&lt;/p&gt;

&lt;p&gt;This article will provide a closer look at TiDBs unique architecture, discuss trade-offs, and refer to a practical implementation found in &lt;a href="https://github.com/ncremaschini/atomic-counter" rel="noopener noreferrer"&gt;this GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Serializability, Linearizability, and TiDB&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;TiDB guarantees &lt;strong&gt;strong consistency&lt;/strong&gt; across its distributed nodes by adopting a &lt;strong&gt;two-phase commit (2PC)&lt;/strong&gt; protocol. This ensures that all transactions, including atomic increments, are serialized and linearizable.&lt;/p&gt;

&lt;p&gt;To provide high availability, TiDB replicates data using &lt;a href="https://en.wikipedia.org/wiki/Raft_(algorithm)" rel="noopener noreferrer"&gt;&lt;strong&gt;Raft&lt;/strong&gt;&lt;/a&gt;, a consensus algorithm that ensures data consistency across regions. This makes TiDB well-suited for use cases requiring globally consistent counters.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Replication and Leader Election in TiDB&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;TiDBs replication model is built on &lt;strong&gt;Raft&lt;/strong&gt; , where each region has a leader and multiple followers.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Raft leader&lt;/strong&gt; handles writes and ensures consistency through consensus.&lt;/p&gt;

&lt;p&gt;Followers replicate data for high availability and enable failover in case of leader failure.&lt;/p&gt;

&lt;p&gt;This replication mechanism ensures that even in multi-region deployments, TiDB maintains consistency and availability.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;The Atomic Counter Pattern&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;The &lt;strong&gt;atomic counter pattern&lt;/strong&gt; ensures precise, consistent counter increments even in distributed environments.&lt;/p&gt;

&lt;p&gt;With TiDB, you can achieve this using &lt;strong&gt;SQL transactions&lt;/strong&gt; and &lt;strong&gt;atomic operations&lt;/strong&gt; like UPDATE ... SET.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Hands-On! Walkthrough of the Deployable Example&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Lets examine how the deployable example in &lt;a href="https://github.com/ncremaschini/atomic-counter" rel="noopener noreferrer"&gt;this GitHub repository&lt;/a&gt; implements an atomic counter using &lt;strong&gt;AWS Lambda&lt;/strong&gt; , &lt;strong&gt;API Gateway&lt;/strong&gt; , and &lt;strong&gt;TiDB&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Api Gateway:&lt;/strong&gt; Provides HTTP endpoints for interacting with the counter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lamba function:&lt;/strong&gt; Implements the business logic for incrementing the counter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;TiDB:&lt;/strong&gt; Stores the counters.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;In my example project you can decide wheter to use a maximum value for the counter or not: this determine if use or not conditional writes.&lt;/p&gt;

&lt;p&gt;Lets focus on lambda business logic, &lt;strong&gt;from the&lt;/strong&gt; &lt;a href="https://github.com/ncremaschini/atomic-counter/blob/main/lib/lambda/tiDB/counterLambda/index.ts" rel="noopener noreferrer"&gt;tiDBAtomicCounter Lambda code:&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;connection = await createDbConnection(DB); 
const updateFilter = getUpdateFilter(useConditionalWrites); 
const params = { id: id, max_value: maxCounterValue } 
const [rows] = await connection.query&amp;lt;RowDataPacket[](updateFilter, params);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;em&gt;getUpdateFilter(useConditionalWrites)&lt;/em&gt; method provides a specific filter based on &lt;em&gt;useConditionalWrites&lt;/em&gt; boolean variable.&lt;/p&gt;

&lt;p&gt;The method is very simple, and i kept it a little verbose than required just for better comprehension.&lt;/p&gt;

&lt;p&gt;It basically returns one of two static SQL statements, very similar to each other, with a little but really important difference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const getUpdateFilter = (useConditionalWrites: boolean): string =&amp;gt; { 
const unconditionalWriteParams = 'SELECT counter_value FROM counters WHERE counter_id = :id FOR UPDATE; 
\ INSERT INTO counters (counter_id, counter_value) VALUES (:id, 1) \ ON DUPLICATE KEY UPDATE counter_value = counter_value + 1; \ SELECT counter_value FROM counters WHERE counter_id = :id; \ COMMIT;'; 
const conditionalWriteParams = 'SELECT counter_value FROM counters WHERE counter_id = :id FOR UPDATE; \ 
INSERT INTO counters (counter_id, counter_value) VALUES (:id, 1) \ ON DUPLICATE KEY UPDATE counter_value = IF(counter_value &amp;lt; :max_value, counter_value + 1, counter_value);\ 
SELECT counter_value FROM counters WHERE counter_id = :id; \ COMMIT;'; 
return useConditionalWrites ? conditionalWriteParams : unconditionalWriteParams;}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lets break down each SQL statements:&lt;/p&gt;

&lt;h2&gt;
  
  
  First statement
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT counter_value FROM counters WHERE counter_id = :id FOR UPDATE;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This statement tells to the db engine that you are selecting the specific table row for update.&lt;/p&gt;

&lt;p&gt;Based on the db engine lock mechanism, it locks the row for the duration of the transascion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Pessimistic locking: immediately when executing the statement, preventing other concurrent transactions from modifying it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Optmistic locking: lock is not acquired immediately, but the engine would check for conflicts at commit time and retries if conflicts occur.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TiDB default has changed over time, and it is &lt;a href="https://docs.pingcap.com/tidb/stable/pessimistic-transaction" rel="noopener noreferrer"&gt;configurable&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I suggest to consider carefully tradeoffs between the two modes: the right one really depends on your specific use case.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.pingcap.com/tidb/stable/transaction-overview" rel="noopener noreferrer"&gt;Knowledge is free at the library. Just bring your own container.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Second statement
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INSERT INTO counters (counter_id, counter_value) VALUES (:id, 1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing special, just tells the db to insert the new row.&lt;/p&gt;

&lt;p&gt;But, wait: we were supposed to talk about incrementing counters, not to inserting new rows!&lt;/p&gt;

&lt;p&gt;The third statement is where the magic happens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Third statement (unconditional writes):
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ON DUPLICATE KEY UPDATE counter_value = counter_value + 1;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This statement tells to db engine what to do if the previous statement fails for duplicate key, because we are trying to insert two rows with the same &lt;em&gt;counter_id&lt;/em&gt; wich is the tables primary key.&lt;/p&gt;

&lt;p&gt;We are basically asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;please increment by one the counter_value field of the row&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With the second and the third statement togheter, we are telling to the db engine:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;please insert this new counter, but if the counter is already present, dont panic and just increment it&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Third statement, with conditional writes:
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ON DUPLICATE KEY UPDATE counter_value = IF(counter_value &amp;lt; :max_value, counter_value + 1, counter_value);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;just as the unconditional write version, we are telling the db engine to increment the existing row but only if the current counter value is below the &lt;em&gt;max_value&lt;/em&gt; parameter.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;please insert this new counter, but if the counter is already present, dont panic and just increment it if it is below the max value.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Fourth statement
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SELECT counter_value FROM counters WHERE counter_id = :id
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is just for retrevieng of the value after the insert / update.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final statement
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;COMMIT;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This seems like the simplest statement, but this is where all the magic happens: based on your DB Engine configuration, this is where our five-statement transaction is executed atomically on the server, conflicts are solved and then data replicated if it was successful.&lt;/p&gt;

&lt;p&gt;Since transactions are executed on the server and are all-or-nothing statements, they fit the atomic counter pattern perfectly.&lt;/p&gt;

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

&lt;h1&gt;
  
  
  &lt;strong&gt;Trade-Offs and Conclusion&lt;/strong&gt;
&lt;/h1&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Strengths&lt;/strong&gt; :
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Strong Consistency&lt;/strong&gt; : TiDBs Raft-based replication and 2PC protocol ensure consistent increments even in globally distributed environments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQL Familiarity&lt;/strong&gt; : Developers can use familiar SQL syntax, reducing the learning curve.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Limitations&lt;/strong&gt; :
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Latency&lt;/strong&gt; : Cross-region communication for strong consistency may increase latency, especially with pessimistic locking configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operational Complexity&lt;/strong&gt; : While TiDB Cloud simplifies management, understanding distributed SQL concepts is necessary for effective use.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Key Takeaway&lt;/strong&gt; :
&lt;/h2&gt;

&lt;p&gt;TiDB is an excellent choice for globally distributed applications requiring strong consistency. Its support for SQL transactions and automatic scaling makes it a powerful tool for implementing atomic counters in multi-region setups.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>distributedsystem</category>
      <category>sql</category>
    </item>
    <item>
      <title>Building Atomic Counters with Momento</title>
      <dc:creator>Nicola Cremaschini</dc:creator>
      <pubDate>Thu, 02 Jan 2025 16:20:28 +0000</pubDate>
      <link>https://forem.com/aws-builders/building-atomic-counters-with-momento-1ekh</link>
      <guid>https://forem.com/aws-builders/building-atomic-counters-with-momento-1ekh</guid>
      <description>&lt;p&gt;In the world of distributed systems, &lt;strong&gt;serverless caching&lt;/strong&gt; is gaining traction for its simplicity and scalability. &lt;a href="https://www.gomomento.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;Momento&lt;/strong&gt;&lt;/a&gt;, a fully managed serverless cache, builds on the core concepts of caching while eliminating infrastructure management.&lt;/p&gt;

&lt;p&gt;In this third installment of the &lt;a href="https://haveyoutriedrestarting.com/series/atomic-counter" rel="noopener noreferrer"&gt;atomic counter series&lt;/a&gt;, well explore how to implement the pattern using &lt;a href="https://www.gomomento.com/" rel="noopener noreferrer"&gt;&lt;strong&gt;Momento&lt;/strong&gt;&lt;/a&gt;. By comparing it to &lt;a href="https://redis.io/" rel="noopener noreferrer"&gt;&lt;strong&gt;Redis&lt;/strong&gt;&lt;/a&gt;, well highlight how Momento simplifies caching for developers, discuss its trade-offs, and guide you through a practical implementation using the code in &lt;a href="https://github.com/ncremaschini/atomic-counter" rel="noopener noreferrer"&gt;this GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Serializability, Linearizability, and Momento&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Unlike traditional caching systems, Momento operates as a &lt;strong&gt;serverless service&lt;/strong&gt; , meaning you dont manage nodes, replicas, or clusters.&lt;/p&gt;

&lt;p&gt;However, like Redis, it provides atomic operations such as increment.&lt;/p&gt;

&lt;p&gt;Momentos atomicity ensures that counter updates are serialized within its storage layer. However, consistency across distributed systems can vary based on use cases, which aligns with the &lt;strong&gt;eventual consistency&lt;/strong&gt; model in serverless architectures.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Replication and Leader Election in Momento&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;As a managed service, &lt;strong&gt;Momento abstracts replication and failover&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You dont have visibility into specific replicas or leaders, but the platform ensures high availability by handling replication and redundancy under the hood.&lt;/p&gt;

&lt;p&gt;This is a notable difference from Redis, where you control and configure replication explicitly.&lt;/p&gt;

&lt;p&gt;Momento offers simplicity at the cost of operational transparency and fine-grained control.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Atomic Counter pattern
&lt;/h1&gt;

&lt;p&gt;The &lt;strong&gt;atomic counter pattern&lt;/strong&gt; enables precise increment operations, even in distributed environments.&lt;/p&gt;

&lt;p&gt;With Momento, you use its increment operation, which automatically initializes the counter if it doesnt exist, similar to Redis.&lt;/p&gt;

&lt;p&gt;This approach works very well if you need to increment your counter unconditionally, regardless its current value.&lt;/p&gt;

&lt;p&gt;If you need to implement conditional increment, Momento doesnt provide methods to do so on server side: you have to handle it on client side, and this could lead to race conditions.&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;strong&gt;Hands-on! Walkthrough of the Deployable Example&lt;/strong&gt;
&lt;/h1&gt;

&lt;p&gt;Lets examine how the deployable example in &lt;a href="https://github.com/ncremaschini/atomic-counter" rel="noopener noreferrer"&gt;this GitHub repository&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This example demonstrates how to implement an atomic counter using &lt;strong&gt;AWS Lambda&lt;/strong&gt; , &lt;strong&gt;API Gateway&lt;/strong&gt; , and &lt;strong&gt;Momento&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;API Gateway&lt;/strong&gt; : Provides HTTP endpoints for interacting with the counter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lambda Functions&lt;/strong&gt; : Implements the business logic for incrementing the counter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Momento&lt;/strong&gt; : Stores the counters.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;In my example project you can decide wheter to use a maximum value for the counter or not: this determine if use or not conditional writes.&lt;/p&gt;

&lt;p&gt;Lets focus on lambda business logic, &lt;strong&gt;from the&lt;/strong&gt; &lt;a href="https://github.com/ncremaschini/atomic-counter/blob/main/lib/lambda/momento/index.ts" rel="noopener noreferrer"&gt;&lt;strong&gt;momentoAtomicCounter&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;Lambda&lt;/strong&gt; code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const momentoCacheClient = await buildMomentoClient();let counter = 0;if (useConditionalWrites) { counter = await handleConditionalWrites(momentoCacheClient,cacheName, id, maxCounterValue); } else { counter = await handleUnconditionalWrites(momentoCacheClient,cacheName, id); }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, i wrote two different methods to handle conditional and unconditional writes.&lt;/p&gt;

&lt;p&gt;Lets dive into the simpler one, &lt;em&gt;handleUnconditionalWrites&lt;/em&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async function handleUnconditionalWrites(momentoClient: CacheClient,cacheName: string, id: string) { let counter = 0; const cacheIncrementResponse = await momentoClient.increment(cacheName, id, 1); switch (cacheIncrementResponse.type) { case CacheIncrementResponse.Success: counter = cacheIncrementResponse.value(); break; case CacheIncrementResponse.Error: throw new Error(cacheIncrementResponse.message()); } return counter}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The method simply levearage on the &lt;em&gt;increment&lt;/em&gt; method from momento sdk.&lt;/p&gt;

&lt;p&gt;It increments a key value by an integer (one, in this example) regardless key existence or key current value.&lt;/p&gt;

&lt;p&gt;Things get more interesting when it comes to handle conditional writes to increment the counter only if it is below a specified threshold.&lt;/p&gt;

&lt;p&gt;Momento does not provide any conditional increment method, but provides few useful conditional writes methods such &lt;em&gt;setIfPresentAndNotEqual&lt;/em&gt; and &lt;em&gt;setIfAbsent.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Lets dive into &lt;em&gt;handleContionalWrites&lt;/em&gt; implementation (response handling logic is removed better readability):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async function handleConditionalWrites(momentoClient: CacheClient, cacheName: string, id: string, maxCounterValue: string){ let counter = 0; const cacheGetResponse = await momentoClient.get(cacheName, id); switch (cacheGetResponse.type) { case CacheGetResponse.Hit: const currentCounter = Number(cacheGetResponse.value()); const nextCounter = currentCounter + 1; const strNextCounter = nextCounter.toString(); counter = await handleSetIfPresentAndNotEqual(momentoClient,cacheName, id, strNextCounter, maxCounterValue); break; case CacheGetResponse.Miss: counter = await hanldeSetIfAbsent(momentoClient, cacheName, id, '1'); break; case CacheGetResponse.Error: throw new Error(cacheGetResponse.toString()); } return counter}async function handleSetIfPresentAndNotEqual(momentoClient: CacheClient,cacheName: string, id: string, nextCounter: string, maxCounterValue: string) { const cacheSetIfPresentAndNotEqualResponse = await momentoClient.setIfPresentAndNotEqual(cacheName, id, nextCounter, maxCounterValue); ...}async function hanldeSetIfAbsent(momentoClient: CacheClient,cacheName: string, id: string, value: string) { let counter = 0; const setIfAbsentResponse = await momentoClient.setIfAbsent(cacheName, id, value); ...}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These methods perform the following logic:&lt;/p&gt;

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

&lt;p&gt;and this ensures consistency, since race conditions are checked by the two check-and-set methods on server-side.&lt;/p&gt;

&lt;p&gt;This is how key initialization works (&lt;em&gt;set if not present&lt;/em&gt; branch):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1736245870347%2Ff55ef434-e011-4de1-85fd-e9022fa5bb3e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1736245870347%2Ff55ef434-e011-4de1-85fd-e9022fa5bb3e.png" width="800" height="302"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;and this is how an existing key increment works (&lt;em&gt;set if present and not equals&lt;/em&gt; branch):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1736245944886%2Fddf6c0c1-fd30-4134-aa68-36600c849d0a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1736245944886%2Fddf6c0c1-fd30-4134-aa68-36600c849d0a.png" width="800" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Trade-Offs and conclusion
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Strenghts:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Serverless Simplicity&lt;/strong&gt; : No infrastructure to manage, reducing operational overhead.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Built-In Scalability&lt;/strong&gt; : Automatically scales to meet demand without manual intervention.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Limitations:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reduced Control&lt;/strong&gt; : Lack of visibility into replication and cluster configurations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Eventual Consistency&lt;/strong&gt; : While atomic operations are supported, consistency guarantees may differ in highly distributed setups.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Performance: since an initial GET is required, more network trips are required compared to other solution that implements conditional increments.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Conclusion
&lt;/h1&gt;

&lt;p&gt;Momento showcases how a &lt;strong&gt;serverless-first approach&lt;/strong&gt; simplifies distributed caching.&lt;/p&gt;

&lt;p&gt;By eliminating the need to manage infrastructure, it allows developers to focus on building applications rather than worrying about operational overhead.&lt;/p&gt;

&lt;p&gt;For atomic counters, Momentos increment operation makes implementation straightforward and reliable. However, this convenience comes with trade-offs: you lose the granular control over replication and failover configurations that traditional systems like Redis offer.&lt;/p&gt;

&lt;p&gt;If youre exploring distributed counters for your application, I highly recommend trying out the example provided in the &lt;a href="https://github.com/ncremaschini/atomic-counter" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Stay tuned for the next installment in the series, where well delve into &lt;strong&gt;DocumentDB&lt;/strong&gt; &lt;/p&gt;

</description>
      <category>aws</category>
      <category>database</category>
      <category>distributedsystem</category>
      <category>caching</category>
    </item>
    <item>
      <title>Building Atomic Counters with Elasticache Redis</title>
      <dc:creator>Nicola Cremaschini</dc:creator>
      <pubDate>Sun, 22 Dec 2024 16:51:45 +0000</pubDate>
      <link>https://forem.com/aws-builders/building-atomic-counters-with-elasticache-redis-c9c</link>
      <guid>https://forem.com/aws-builders/building-atomic-counters-with-elasticache-redis-c9c</guid>
      <description>&lt;p&gt;When working with high-throughput, low-latency applications, &lt;strong&gt;Redis&lt;/strong&gt; an in-memory data storestands out as an excellent choice for implementing the &lt;strong&gt;atomic counter pattern&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;With its atomic operations and simple APIs, Redis offers a straightforward approach to incrementing counters while ensuring high performance.&lt;/p&gt;

&lt;p&gt;In this article, well explore how to build an atomic counter using &lt;strong&gt;AWS ElastiCache Redis&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Youll gain a practical understanding of Redis concepts like &lt;strong&gt;atomic operations&lt;/strong&gt; , its &lt;strong&gt;replication model&lt;/strong&gt; , and how to implement counters with the code from &lt;a href="https://github.com/ncremaschini/atomic-counter" rel="noopener noreferrer"&gt;this GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Serializability, Linearizability, and Redis&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before we can dive deep into code, we need to recall few concepts (please refer to &lt;a href="https://hashnode.com/post/cm3syajxr000009mk7pwz56if" rel="noopener noreferrer"&gt;&lt;strong&gt;the first article of this series for a detailed explanation&lt;/strong&gt; )&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Serializability&lt;/strong&gt; : Operations appear in a consistent sequential order, ensuring correctness.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Linearizability&lt;/strong&gt; : Writes are immediately visible for subsequent reads, ensuring real-time consistency.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Redis processes commands in a &lt;strong&gt;single-threaded event loop&lt;/strong&gt; , ensuring that each command is executed in the order its received. This guarantees atomicity at the command level for operations like &lt;em&gt;INCR&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;While Redis operations on a single node can be considered &lt;strong&gt;linearizable&lt;/strong&gt; , in a distributed Redis setup (e.g., with clustering or replicas), this strict ordering can break. Writes to replicas are propagated asynchronously, so they may lag behind the primary node.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Replication and Leader election in Redis&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Redis employs a &lt;strong&gt;primary-replica architecture&lt;/strong&gt; , where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The &lt;strong&gt;primary node&lt;/strong&gt; handles all writes and propagates updates to replicas asynchronously.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Redis Sentinel&lt;/strong&gt; handles failover, promoting a replica to primary in case of failure.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For atomic counters, a single primary node is typically sufficient. If clustering is used, counter keys should be kept on a single shard to maintain atomicity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Atomic Counter Pattern
&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;atomic counter pattern&lt;/strong&gt; allows you to increment a value reliably, even in distributed systems, by ensuring operations are conflict-free and consistent.&lt;/p&gt;

&lt;p&gt;Redis supports this pattern natively through the &lt;em&gt;INCR&lt;/em&gt; command, which atomically increments a keys value by 1.&lt;/p&gt;

&lt;p&gt;However, if it is necessary to carry out the increment depending on the current status of the counter, race conditions may be possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Hands-on! Walkthrough of the Deployable Example&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Lets dive into the example provided in the &lt;a href="https://github.com/ncremaschini/atomic-counter" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This example demonstrates how to implement an atomic counter using &lt;strong&gt;AWS Lambda&lt;/strong&gt; , &lt;strong&gt;API Gateway&lt;/strong&gt; , and &lt;strong&gt;ElastiCache Redis&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;API Gateway&lt;/strong&gt; : Provides HTTP endpoints for interacting with the counter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lambda Functions&lt;/strong&gt; : Implements the business logic for incrementing the counter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ElastiCache Redis&lt;/strong&gt; Cluster: Stores the counters with atomicity guarantees.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;In my example project you can decide wheter to use a maximum value for the counter or not: this determine if use or not conditional writes.&lt;/p&gt;

&lt;p&gt;Lets focus on lambda business logic, &lt;a href="https://github.com/ncremaschini/atomic-counter/blob/main/lib/lambda/redis/index.ts" rel="noopener noreferrer"&gt;from the redisAtomicCounter Lambda&lt;/a&gt; code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const redisClient = await buildRedisClient();
const result = await redisClient.eval(getLuaScript(useConditionalWrites), 1, id, maxCounterValue);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code snippet simply sends an &lt;em&gt;eval&lt;/em&gt; command and gets the new updated counter value, using the &lt;a href="https://github.com/redis/ioredis" rel="noopener noreferrer"&gt;ioredis client&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Let see how the &lt;em&gt;getLuaScript()&lt;/em&gt; works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const getLuaScript = (useConditionalWrites: boolean) =&amp;gt; { 
const unconditionalIncrementScript = ` redis.call('INCR', KEYS[1]) local counter = redis.call('GET', KEYS[1]) return counter `; 
const conditionalIncrementScript = ` 
local counter = redis.call('GET', KEYS[1]) 
local maxValue = tonumber(ARGV[1]) 
if not counter 
then counter = 0 
end 
counter = tonumber(counter) 
if counter &amp;lt; maxValue then 
redis.call('INCR', KEYS[1]) 
counter = redis.call('GET', KEYS[1]) 
return counter 
else 
return 'Counter has reached its maximum value of: ' .. maxValue 
end `; 
return useConditionalWrites ? conditionalIncrementScript : unconditionalIncrementScript;}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unconditional writes don't require actually to be executed inside a LUA Script: we could use the &lt;em&gt;incr()&lt;/em&gt; method provided by &lt;em&gt;ioredis&lt;/em&gt; client.&lt;/p&gt;

&lt;p&gt;But for conditional writes, it is required to check the counter value before incrementing it to avoid race conditions: if we perform the check on client side, another client might increment the counter between the &lt;em&gt;get()&lt;/em&gt; and the &lt;em&gt;incr()&lt;/em&gt; instructions execution on the first client.&lt;/p&gt;

&lt;p&gt;Let's see an example: assuming the maximum value for the counter is 10, Alice and Bob perform a &lt;em&gt;GET&lt;/em&gt; for the same key 1, when the counter value is 9.&lt;/p&gt;

&lt;p&gt;They check if the current value is below the maximum value, and then they both send an &lt;em&gt;INC&lt;/em&gt; command to redis.&lt;/p&gt;

&lt;p&gt;Since &lt;em&gt;INC&lt;/em&gt; command is executed unconditionally on server side, the counter is incremented by two and exceeds the maximum value.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Redis secret sauce: LUA Script
&lt;/h2&gt;

&lt;p&gt;The solution is to check the counter value on server side, executing a LUA Script.&lt;/p&gt;

&lt;p&gt;It gets the counter value, check if it is below the maximum value and eventually increment it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;local counter = redis.call('GET', KEYS[1])
local maxValue = tonumber(ARGV[1])
if not counter 
then counter = 0
endcounter = tonumber(counter)
if counter &amp;lt; maxValue 
then 
redis.call('INCR', KEYS[1]) 
counter = redis.call('GET', KEYS[1]) 
return counter
else 
return 'Counter has reached its maximum value of: ' .. maxValue
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you look the code, you might think this code is not safe too: another script could increment the counter between the &lt;em&gt;GET&lt;/em&gt; and the &lt;em&gt;INCR&lt;/em&gt; command execution.&lt;/p&gt;

&lt;p&gt;The magic of LUA Script is that just one script can be executed at the same time, &lt;a href="https://redis.io/docs/latest/develop/interact/programmability/eval-intro/" rel="noopener noreferrer"&gt;as reported in the documentation&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Redis guarantees the script's atomic execution. While executing the script, all server activities are blocked during its entire runtime. These semantics mean that all of the script's effects either have yet to happen or had already happened.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;and that is exactly what we need when dealing with atomic counters: Since only one script is executed, there is no concurrency and no race conditions, as &lt;strong&gt;serializability is&lt;/strong&gt; guaranteed.&lt;/p&gt;

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

&lt;p&gt;Moreover, we have better performance, which is good:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Because scripts execute in the server, reading and writing data from scripts is very efficient.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Trade-Offs and Conclusion&lt;/strong&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Strengths&lt;/strong&gt; :
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Redis provides &lt;strong&gt;low-latency&lt;/strong&gt; atomic operations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;em&gt;INCR&lt;/em&gt; command is inherently atomic, simplifying counter implementation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;LUA Script execution prevent race conditions on conditional writes, achieving &lt;strong&gt;serializability.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Limitations&lt;/strong&gt; :
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Asynchronous Replication&lt;/strong&gt; : Updates may not immediately reflect on replicas.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Durability Risks&lt;/strong&gt; : Without persistence, counters may reset after a failure or restart.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Conclusion:&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Redis is ideal for high-performance, in-memory atomic counters where latency is a top priority, and simplifies building atomic counters with its native support for atomic operations and low-latency access.&lt;/p&gt;

&lt;p&gt;However, consider its replication and durability trade-offs for production use.&lt;/p&gt;

&lt;p&gt;Check out the &lt;a href="https://github.com/ncremaschini/atomic-counter" rel="noopener noreferrer"&gt;deployable example here&lt;/a&gt;, and stay tuned for the next article in the series, where well explore &lt;strong&gt;Momento&lt;/strong&gt; as an alternative for serverless caching.&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>aws</category>
      <category>database</category>
      <category>redis</category>
    </item>
    <item>
      <title>Building Atomic Counters with DynamoDB</title>
      <dc:creator>Nicola Cremaschini</dc:creator>
      <pubDate>Mon, 09 Dec 2024 06:00:50 +0000</pubDate>
      <link>https://forem.com/aws-builders/building-atomic-counters-with-dynamodb-fae</link>
      <guid>https://forem.com/aws-builders/building-atomic-counters-with-dynamodb-fae</guid>
      <description>&lt;p&gt;DynamoDB, a serverless NoSQL database, is a go-to choice for implementing atomic counters due to its built-in support for atomic operations and managed scalability. This article will guide you through how DynamoDB ensures consistency and replication, a refresher on the atomic counter pattern, and a hands-on walkthrough of a deployable example from &lt;a href="https://github.com/ncremaschini/atomic-counter" rel="noopener noreferrer"&gt;this GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;By the end, youll understand how to leverage DynamoDB for atomic counters and know the trade-offs involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Serializability and Linearizability in DynamoDB&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Before we can dive deep into code, we need to recall few concepts (please refer to &lt;a href="https://hashnode.com/post/cm3syajxr000009mk7pwz56if" rel="noopener noreferrer"&gt;the first article of this series for a detailed explanation&lt;/a&gt;)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Serializability&lt;/strong&gt; : Operations appear in a consistent sequential order, ensuring correctness.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Linearizability&lt;/strong&gt; : Writes are immediately visible for subsequent reads, ensuring real-time consistency.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;DynamoDB achieves linearizable writes through its &lt;strong&gt;single-leader replication model&lt;/strong&gt; :&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Write operations are directed to the leader node for the partition key, and changes are propagated to replicas.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Strongly consistent reads (optional) ensure the latest value is returned immediately after a write.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This guarantees DynamoDB can safely implement atomic operations like counters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replication and Leader election in DynamoDB
&lt;/h2&gt;

&lt;p&gt;DynamoDB automatically manages replication across multiple availability zones to ensure durability and availability. Key mechanisms include:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Single-leader replication&lt;/strong&gt; : A leader node handles writes, maintaining consistency while replicas handle reads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leader election&lt;/strong&gt; : If a leader fails, DynamoDB promotes another replica seamlessly, ensuring high availability without manual intervention.&lt;/p&gt;

&lt;p&gt;This replication strategy enables DynamoDB to handle distributed workloads while maintaining data consistency.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;A Note on Synchronized Timestamps in Distributed Databases&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Synchronized timestamps play a critical role in distributed databases, especially for ensuring consistency across geographically dispersed replicas.&lt;/p&gt;

&lt;p&gt;Without synchronized clocks, it becomes challenging to determine the order of operations accurately, leading to potential consistency issues in global-scale applications.&lt;/p&gt;

&lt;p&gt;In the AWS ecosystem, the &lt;strong&gt;AWS Time Sync Service&lt;/strong&gt; provides a highly accurate and reliable time source synchronized across all AWS Regions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://aws.amazon.com/about-aws/whats-new/2023/11/amazon-time-sync-service-microsecond-accurate-time/" rel="noopener noreferrer"&gt;Announced last year&lt;/a&gt;, this service offers nanosecond-level precision and a consistent view of time, serving as a foundational piece for distributed systems.&lt;/p&gt;

&lt;p&gt;Recently, AWS built upon this foundation to announce &lt;a href="https://press.aboutamazon.com/2024/12/aws-announces-new-database-capabilities-including-amazon-aurora-dsql-the-fastest-distributed-sql-database#:~:text=To%20ensure%20each%20Region%20sees,provide%20microseconds%20level%20accurate%20time" rel="noopener noreferrer"&gt;&lt;strong&gt;strong consistency for DynamoDB global tables&lt;/strong&gt;&lt;/a&gt;. This new feature allows applications to perform strongly consistent reads and writes across multiple regions, ensuring the same data is visible no matter where the query originates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is this important?&lt;/strong&gt; Strong consistency in global tables depends on synchronized timestamps to ensure that write propagation across regions respects causal ordering. This prevents race conditions and ensures data correctness even in high-latency or failure scenarios.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Impact on Atomic Counters&lt;/strong&gt; : If your atomic counter spans multiple regions via global tables, synchronized timestamps enable accurate propagation of updates, preserving the order and integrity of increments.&lt;/p&gt;

&lt;p&gt;This synergy of the AWS Time Sync Service and DynamoDB advancements showcases how synchronized time is more than just an infrastructure detailits a cornerstone of achieving robust distributed consistency.&lt;/p&gt;

&lt;h2&gt;
  
  
  DynamoDB Conditional Writes
&lt;/h2&gt;

&lt;p&gt;DynamoDBs &lt;strong&gt;conditional write&lt;/strong&gt; feature allows you to execute write operations (&lt;em&gt;PutItem, UpdateItem, DeleteItem&lt;/em&gt;) only if specific conditions are met.&lt;/p&gt;

&lt;p&gt;This capability is crucial for enforcing business rules, ensuring data integrity, and preventing race conditions in distributed systems.&lt;/p&gt;

&lt;p&gt;When you perform a conditional write, you include a &lt;em&gt;ConditionExpression&lt;/em&gt; in the request.&lt;/p&gt;

&lt;p&gt;DynamoDB evaluates this condition against the items existing attributes before executing the operation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;If the condition evaluates to &lt;strong&gt;true&lt;/strong&gt; , the write operation proceeds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If the condition evaluates to &lt;strong&gt;false&lt;/strong&gt; , the operation fails with a &lt;em&gt;ConditionalCheckFailedException&lt;/em&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Few use cases for conditional writes includes:&lt;/p&gt;

&lt;h3&gt;
  
  
  Enforcing constraints
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Ensure unique records in a table by verifying an attribute does not exist:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ConditionExpression: "attribute_not_exists(partitionKey)"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Prevent counter increments beyond a maximum value:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ConditionExpression: "counterValue &amp;lt; :maxValue"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Concurrent Updates without conflicts&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Safely update an item only if its version matches a known value (optimistic locking):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ConditionExpression: "version = :expectedVersion"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;strong&gt;Transactional Integrity&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;enforce rules like only update if another attribute matches a specific state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Benefits:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Atomicity:&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data Integrity:&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Performance:&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The atomic counter pattern
&lt;/h2&gt;

&lt;p&gt;The atomic counter pattern ensures safe, concurrent updates to a counter without losing increments due to race conditions. In distributed systems:&lt;/p&gt;

&lt;p&gt;Operations must be atomic (all or nothing).&lt;/p&gt;

&lt;p&gt;DynamoDB achieves this with the &lt;em&gt;UpdateItem&lt;/em&gt; operation and the ADD attribute update expression, which ensures the counter is incremented atomically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hands-on! &lt;strong&gt;Walkthrough of the Deployable Example&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Lets explore the inner workings of the example in the &lt;a href="https://github.com/ncremaschini/atomic-counter" rel="noopener noreferrer"&gt;GitHub repositor&lt;/a&gt;y, focusing on how DynamoDB is used for atomic counters.&lt;/p&gt;

&lt;p&gt;The implementation includes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;API Gateway&lt;/strong&gt; : Provides HTTP endpoints for interacting with the counter.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lambda Functions&lt;/strong&gt; : Implements the business logic for incrementing the counter and enforcing optional constraints like a maximum value.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;DynamoDB Table&lt;/strong&gt; : Stores the counters with atomicity guarantees.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;In my example project you can decide wheter to use a maximum value for the counter or not: this determine if use ore not conditional writes.&lt;/p&gt;

&lt;p&gt;Lets focus on this logic,&lt;a href="https://github.com/ncremaschini/atomic-counter/blob/main/lib/lambda/dynamo/index.ts" rel="noopener noreferrer"&gt;from the dynamoDbAtomicCounter Lambda&lt;/a&gt; code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const id = event.pathParameters?.id;
const writeParams = getWriteParams(useConditionalWrites, id, maxCounterValue);
const dynamoDBClient = await buildDynamoDbClient();
const result = await dynamoDBClient.send(new UpdateItemCommand(writeParams));
const counter = Number(result.Attributes?.atomic_counter.N);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code snippet simply sends an &lt;em&gt;UpdateItemCommand&lt;/em&gt; and gets the new updated counter value, using the AWS SDK DynamoDBClient.&lt;/p&gt;

&lt;p&gt;Let see how the &lt;em&gt;getWriteParams&lt;/em&gt; works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const getWriteParams = (useConditionalWrites: boolean, id: string, maxCounterValue: string) =&amp;gt; { 
const TABLE_NAME = process.env.TABLE_NAME || ''; 
const unconditionalWriteParams = { 
TableName: TABLE_NAME, Key: { id: { S: id }, }, 
UpdateExpression: 'ADD atomic_counter :inc', ExpressionAttributeValues: { ':inc': { N: '1' } }, 
ReturnValues: 'UPDATED_NEW' as const, }; 
const conditionalWriteParams = { 
TableName: TABLE_NAME, Key: { id: { S: id }, }, 
UpdateExpression: 'ADD atomic_counter :inc', ConditionExpression: 'attribute_not_exists(atomic_counter) or atomic_counter &amp;lt; :max', ExpressionAttributeValues: {':inc': { N: '1' }, ':max': { N: maxCounterValue }, }, ReturnValues: 'UPDATED_NEW' as const, }; return useConditionalWrites ? conditionalWriteParams : unconditionalWriteParams;}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This method checks if conditional writes are required: the &lt;em&gt;update expression&lt;/em&gt; is the same in both cases, and it leverages the &lt;em&gt;ADD&lt;/em&gt; command.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UpdateExpression: 'ADD atomic_counter :inc'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If conditional writes are required, a &lt;em&gt;Condition Expression&lt;/em&gt; is used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ConditionExpression: 'attribute_not_exists(atomic_counter) or atomic_counter &amp;lt; :max'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Trade-Offs and Conclusion
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Advantages
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;DynamoDBs managed infrastructure handles replication and scaling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Simple and efficient atomic updates using &lt;em&gt;UpdateItem&lt;/em&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Limitations&lt;/strong&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Hot partitions&lt;/strong&gt; : High traffic to a single counter may cause throttling.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Throughput limits&lt;/strong&gt; : Monitor RCUs/WCUs to avoid performance degradation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Eventual consistency&lt;/strong&gt; : Use strongly consistent reads when precise counter values are critical.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;DynamoDB provides a simple and scalable solution for implementing atomic counters in distributed systems.&lt;/p&gt;

&lt;p&gt;Its built-in atomicity and managed replication make it a strong candidate for this pattern.&lt;/p&gt;

&lt;p&gt;Explore the &lt;a href="https://github.com/ncremaschini/atomic-counter" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; &lt;a href="https://github.com/ncremaschini/atomic-counter" rel="noopener noreferrer"&gt;to deploy the example&lt;/a&gt; and experiment with atomic counters in DynamoDB, and stay tuned for the next article, &lt;a href="https://haveyoutriedrestarting.com/building-atomic-counters-with-elasticache-redis" rel="noopener noreferrer"&gt;where well explore the &lt;strong&gt;ElastiCache Redis implementation&lt;/strong&gt; of the atomic counter pattern.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dynamodb</category>
      <category>aws</category>
      <category>distributedsystem</category>
      <category>database</category>
    </item>
    <item>
      <title>Atomic counter: framing the Problem Space</title>
      <dc:creator>Nicola Cremaschini</dc:creator>
      <pubDate>Fri, 22 Nov 2024 16:23:29 +0000</pubDate>
      <link>https://forem.com/aws-builders/atomic-counter-framing-the-problem-space-37im</link>
      <guid>https://forem.com/aws-builders/atomic-counter-framing-the-problem-space-37im</guid>
      <description>&lt;p&gt;Why Atomic Counters Matter in Distributed Systems&lt;/p&gt;




&lt;p&gt;In distributed systems, ensuring accuracy and consistency in concurrent operations is a core challenge. Atomic countersa mechanism for maintaining precise, incrementing countsare a common requirement in applications like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rate Limiting&lt;/strong&gt; : Tracking API usage to enforce quotas.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Inventory Management&lt;/strong&gt; : Keeping stock levels accurate in real time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Leaderboards&lt;/strong&gt; : Recording scores and ranks in games or applications.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Analytics&lt;/strong&gt; : Counting events such as clicks or views for reporting.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Challenge: Scaling Atomicity in Distributed Systems&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;When multiple processes update a shared counter, ensuring accuracy without conflicts is difficult. Challenges include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Race Conditions&lt;/strong&gt; : Concurrent updates may result in incorrect counts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data Integrity&lt;/strong&gt; : Systems must ensure updates are not lost, even in failure scenarios.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scalability vs. Consistency&lt;/strong&gt; : Distributed systems trade off latency, fault tolerance, and strict consistency.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This trade-off is encapsulated in the &lt;strong&gt;CAP theorem&lt;/strong&gt; , which states that a distributed database can only guarantee two of the following three properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Consistency&lt;/strong&gt; : Every read reflects the most recent write.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Availability&lt;/strong&gt; : Every request receives a response, even if some nodes are down.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Partition Tolerance&lt;/strong&gt; : The system operates even when network partitions occur.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Atomic counters live at the intersection of these challenges. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Choosing &lt;strong&gt;consistency and partition tolerance&lt;/strong&gt; ensures correctness but may sacrifice availability during failures.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Prioritizing &lt;strong&gt;availability and partition tolerance&lt;/strong&gt; may allow stale or conflicting updates.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Serializability and Linearizability&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Atomic counters require precise semantics to maintain correctness:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Serializability&lt;/strong&gt; ensures that concurrent operations are executed in a sequence that could occur in a single-threaded system. Its the gold standard for consistency in databases but can be computationally expensive.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Linearizability&lt;/strong&gt; , a stronger guarantee, ensures that operations appear instantaneous and reflect the latest state globally. This is crucial for atomic counters where every increment must reflect an up-to-date value.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why These Databases?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;For this series, Ive chosen &lt;a href="https://aws.amazon.com/dynamodb/" rel="noopener noreferrer"&gt;DynamoDB&lt;/a&gt; &lt;strong&gt;,&lt;/strong&gt; &lt;a href="https://aws.amazon.com/it/documentdb/" rel="noopener noreferrer"&gt;DocumentDB&lt;/a&gt; &lt;strong&gt;,&lt;/strong&gt; &lt;a href="https://aws.amazon.com/redis/" rel="noopener noreferrer"&gt;Elasticache Redis&lt;/a&gt; &lt;strong&gt;,&lt;/strong&gt; &lt;a href="https://www.gomomento.com/" rel="noopener noreferrer"&gt;GoMomento&lt;/a&gt;, and &lt;a href="https://pingcap.com/products/tidb/" rel="noopener noreferrer"&gt;TiDB&lt;/a&gt; for several key reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Serverless and SaaS Models&lt;/strong&gt; : DynamoDB, DocumentDb and the SaaS version of TiDB handle infrastructure and scaling for you. Similarly, ElastiCache and Momento offer managed caching solutions, focusing on simplicity and performance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Diverse Strategies&lt;/strong&gt; : These systems represent a variety of approaches to critical aspects of distributed systems:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Specialized Solutions&lt;/strong&gt; : By comparing these systems, well uncover insights into how different architectures tackle the shared challenge of atomicity, equipping you to make informed choices in your projects.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Why a Pattern Matters&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The atomic counter pattern provides structured solutions to navigate these complexities, leveraging the unique strengths of various databases and caching systems. By using native features such as conditional writes, Lua scripts, or distributed transactions, developers can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Ensure correctness under concurrent updates.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Balance consistency, availability, and scalability based on system needs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Simplify implementation by relying on proven database capabilities.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this series, well explore how to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Understand the trade-offs of implementing atomic counters in distributed environments.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Build practical solutions using &lt;strong&gt;Node.js&lt;/strong&gt; and &lt;strong&gt;AWS CDK&lt;/strong&gt; , supported by real-world examples.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Apply atomic counter patterns across databases like &lt;strong&gt;DynamoDB, Redis, TiDB&lt;/strong&gt; , and SaaS services like &lt;strong&gt;Momento&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Lets set the stage for building reliable atomic counters with a strong foundation in distributed systems theory and practical implementations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ncremaschini/atomic-counter" rel="noopener noreferrer"&gt;Here's the github repository with deployable stack to explore the different implementations&lt;/a&gt;&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>aws</category>
      <category>database</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Evaluating Performance: A Benchmark Study of Serverless Solutions for Message Delivery to Containers on AWS Cloud - Episode 2</title>
      <dc:creator>Nicola Cremaschini</dc:creator>
      <pubDate>Fri, 10 May 2024 13:25:30 +0000</pubDate>
      <link>https://forem.com/aws-builders/evaluating-performance-a-benchmark-study-of-serverless-solutions-for-message-delivery-to-1c8k</link>
      <guid>https://forem.com/aws-builders/evaluating-performance-a-benchmark-study-of-serverless-solutions-for-message-delivery-to-1c8k</guid>
      <description>&lt;p&gt;This post follows &lt;a href="https://dev.to/niccrema/evaluating-performance-a-benchmark-study-of-serverless-solutions-for-message-delivery-to-8lg-temp-slug-1484756"&gt;my previous post on this topic&lt;/a&gt;, and it measures the performance of another solution for the same problem, &lt;strong&gt;how to forward events to private containers using serverless services and fan-out patterns.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;Suppose you have a cluster of containers and you need to notify them when a database record is inserted or changed, and these changes apply to the internal state of the application. A fairly common use case.&lt;/p&gt;

&lt;p&gt;Let's say you have the following requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The tasks are in an autoscaling group, so their number may change over time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A task is only healthy if it can be updated when the status changes. In other words, all tasks must have the same status. Containers that do not change their status must be marked as unhealthy and replaced.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When a new task is started, it must be in the last known status.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Status changes must be in near real- time. Status changes in the database must be passed on to the containers in less than 2 seconds.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Solutions
&lt;/h2&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/niccrema/evaluating-performance-a-benchmark-study-of-serverless-solutions-for-message-delivery-to-8lg-temp-slug-1484756"&gt;first post about this&lt;/a&gt; i explored two options and measured performance of this one:&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The AppSync API receives mutations and stores derived data in the DynamoDB table&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The DynamoDB streams the events&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Lambda function is triggered by the DynamoDB stream&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Lambda function sends the events to the SNS topic&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The SNS topic sends the events to the SQS queues&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Fargate service reads the events from the SQS queues&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If events are not processed within a timeout, they are moved to the DLQ&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A Cloudwatch alarm is triggered if the DLQ is not empty&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The even more serverless version
&lt;/h3&gt;

&lt;p&gt;An even more serverless version of the above solution replaces Lambda and SNS with Eventbridge&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The AppSync API receives mutations and stores derived data in the DynamoDB table&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The DynamoDB stream the events&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;EventBridge is used to filter, transform and...&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;...fan-outs events to SQS queues&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Fargate service reads the events from the SQS queues&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If events are not processed within a timeout, they are moved to the DLQ&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A Cloudwatch alarm is triggered if the DLQ is not empty&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;The only code i wrote here is the code to consume SQS from my application, no glue-code is required.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Trust, but verify
&lt;/h2&gt;

&lt;p&gt;I've conducted a benchmark to verify the performance of this configuration, in terms of latency from the mutation being posted to Appsync to the message received by the client polling SQS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key system parameters
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Region: eu-south-1&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Number of tasks: 20&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Event bus: 1 SQS per task, 1 DLQ per SQS, all SQS subscribed to one SNS&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SQS Consumer: provided by AWS SDK, configured for long polling (20s)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Task configuration: 256 CPU, 512 Memory, Docker image based on &lt;a href="https://hub.docker.com/layers/library/node/20-slim/images/sha256-80c3e9753fed11eee3021b96497ba95fe15e5a1dfc16aaf5bc66025f369e00dd?context=explore" rel="noopener noreferrer"&gt;&lt;strong&gt;Official Node Image 20-slim&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DynamoDB Configured in PayPerUseMode, stream enabled&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;EventBridge configured to intercept and forwards all events from Dynamo stream to SQS queues&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Benchmark parameters
&lt;/h3&gt;

&lt;p&gt;I used a basic postman collection runner to perform a mutation to Appsync every 5 seconds, for 720 iterations.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Goal
&lt;/h3&gt;

&lt;p&gt;The goal was to verify if containers would be updated within 2 seconds, and to verify performance against &lt;a href="https://dev.to/niccrema/evaluating-performance-a-benchmark-study-of-serverless-solutions-for-message-delivery-to-8lg-temp-slug-1484756"&gt;the first version&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measurements
&lt;/h3&gt;

&lt;p&gt;i used the following Cloudwatch provided metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Appsync latency&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Dynamo stream latency&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;EventBridge Pipe duration&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;EventBridge Rules latency&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The SQS time taken custom metric is calculated from SQS provided attributes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Disclaimer: some latency measurements are calculated on consumers' side, and we all know that synchronizing clocks in a distributed system is a hard problem.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Still, measurements are performed by the same computing nodes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Please consider following latencies not as precise measurements but as coarse indicators.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here screenshots from my Cloudwatch dashboard&lt;/p&gt;

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

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

&lt;p&gt;Few key data, from Average numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Most of the time is taken by EventBridge rule, I couldn't do anything to lower this latency. The rule is as simple as possible and it is integrated natively by AWS.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The average total time taken is &lt;strong&gt;210.74 ms&lt;/strong&gt; , versus &lt;strong&gt;108.39 ms&lt;/strong&gt; taken by the first version with Lambda and SNS.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The average response time measured by my client, which covers my client's network latency, is 175 ms. Given Appsync AVG Latency is 62.7 ms, my Avg network latency is 112,13 ms. This means that from my client sending the mutation to consumers receiving the message there are 175 + 113.13 = &lt;strong&gt;288.13 ms&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;This solution has proven to be fast and reliable and requires little configuration to set up and no glue-code to write.&lt;/p&gt;

&lt;p&gt;Since everything is managed, there is no space for tuning and improvements.&lt;/p&gt;

&lt;p&gt;The latency of this solution is worse than the first version by &lt;strong&gt;194.44%.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;However, EventBridge offers many more capabilities than SNS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;In this article, I have presented you with a solution that I had to design as part of my work and my approach to solution development: this includes clarifying the scope and context, evaluating different options, and having a good knowledge of the parts involved and the performance and quality attributes of the overall system, writing code and benchmarking where necessary, but always with the clear awareness that there are no perfect solutions.&lt;/p&gt;

&lt;p&gt;I hope it was helpful to you, and &lt;a href="https://github.com/ncremaschini/fargate-notifications" rel="noopener noreferrer"&gt;here is the GitHub repo to deploy both versions of the solution&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Bye 👋!&lt;/p&gt;

</description>
      <category>fanout</category>
      <category>aws</category>
      <category>serverless</category>
      <category>ecs</category>
    </item>
    <item>
      <title>Evaluating Performance: A Benchmark Study of Serverless Solutions for Message Delivery to Containers on AWS Cloud</title>
      <dc:creator>Nicola Cremaschini</dc:creator>
      <pubDate>Sun, 03 Mar 2024 21:48:21 +0000</pubDate>
      <link>https://forem.com/aws-builders/evaluating-performance-a-benchmark-study-of-serverless-solutions-for-message-delivery-to-kbl</link>
      <guid>https://forem.com/aws-builders/evaluating-performance-a-benchmark-study-of-serverless-solutions-for-message-delivery-to-kbl</guid>
      <description>&lt;p&gt;In this article i'll show you how to forward events to private containers using serverless services and fan-out pattern.&lt;/p&gt;

&lt;p&gt;I'll explore possible solutions within AWS ecosystem, but all are applicable regardless the actual service / implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;Suppose you have a cluster of containers and you need to notify them when a database record is inserted or changed, and these changes apply to the internal state of the application. A fairly common use case.&lt;/p&gt;

&lt;p&gt;Let's say you have the following requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The tasks are in an autoscaling group, so their number may change over time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A task is only healthy if it can be updated when the status changes. In other words, all tasks must have the same status. Containers that do not change their status must be marked as unhealthy and replaced.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When a new task is started, it must be in the last known status.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Status changes must be in near real- time. Status changes in the database must be passed on to the containers in less than 2 seconds.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Given these requirements, let's explore a few options.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: tasks directly querying the database
&lt;/h2&gt;

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

&lt;h3&gt;
  
  
  Pros:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;easy to implement: The task is just to perform a simple query and get the current status, assuming it can be queried.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;fast: It really depends on the DB resources and the complexity of the query, but there are not many hops and can be configured to be fast. You can configure polling time to match our requirement of 2 seconds requirement, e.g. every 1 second.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;easy to mark as unhealthy tasks that fails to perform queries. The application could catch errors in queries and mark itself as unhealthy if it has enough resources. Otherwise, the load balancer's health check would fail.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cons:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;waste of resources: Your application queries the database even if no changes have been made. If your database does not change more frequently than the polling rate, most queries are useless.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;your database is a single point of failure: If the database cannot serve queries, tasks cannot be notified.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;it does not scale well: As the number of tasks grows, the number of queries grows and you may need to scale the database as well, or you may need a very large cluster running all the time to accommodate any scaling, wasting resources.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;difficult to monitor: How can you check if an individual task is in the right state?&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In such a scenario, I definitely don't like polling.&lt;/p&gt;

&lt;p&gt;Let's try a different and opposite approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: Db streams changes to containers
&lt;/h2&gt;

&lt;p&gt;Instead of having tasks asking to the database, let's have the database notifying them for changes.&lt;/p&gt;

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

&lt;p&gt;Before go into the pros and cons, i must say that it would be very hard if not impossible to implement this solution exactly as i drown it. We can use a very popular pattern, called &lt;em&gt;fan-out.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This is the &lt;a href="https://en.wikipedia.org/wiki/Fan-out_(software)" rel="noopener noreferrer"&gt;wikipedia&lt;/a&gt; definition:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In message-oriented middleware solutions, fan-out is a messaging pattern used to model an information exchange that implies the delivery (or spreading) of a message to one or multiple destinations possibly in parallel, and not halting the process that executes the messaging to wait for any response to that message&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To make things a little more concrete, let's use some popular AWS services that are commonly used to implement this pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;DynamoDB: NoSql database with native event streaming&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SNS: pub/sub event bus&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SQS: queue service&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The solution looks like this:&lt;/p&gt;

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

&lt;p&gt;Now let's explore pros and cons:&lt;/p&gt;

&lt;h3&gt;
  
  
  Pros:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;first of all, you can see that arrows turned into dotted lines. This architecture is completely asynchronous&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;easy to implement: all integrations you need are native. You need just to configure serverless services and to implement a SQS consumer in your application.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;very scalable: you can add as many task as you want without affecting the database, your limit here is SNS but is very high. As stated in &lt;a href="https://docs.aws.amazon.com/general/latest/gr/sns.html" rel="noopener noreferrer"&gt;official docs&lt;/a&gt; a single topic supports up to 12,500,000 subscriptions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;no waste of resources: a.k.a really cost-effective. This solution leverages on pay-per-use services, and they would be used only when actual changes occurs on db.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;very easy to monitor: both SNS and SQS supports Dead Letter Topic / Queue: if a message isn't consumed within the timeout, it can be moved into a DLQ. You can set up an alarm if a DLQ is not empty, and kill the associated task.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;easy to recover: If a container cannot consume a message, it can try again. In other words, it does not have to be online and ready to receive the message at the moment it is delivered, as the queues are persistent.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;very fast: i did a benchmark on this solution, &lt;a href="https://github.com/ncremaschini/fargate-notifications" rel="noopener noreferrer"&gt;here the github repo with the actual code&lt;/a&gt;. Later on in this article we'll see results&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cons
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;more moving parts: even if the integration code is not required since it's provided by AWS, connecting things and tuning connections is not straightforward as performing a query.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;not so easy to troubleshoot. As every distributed system, i would say.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;it strongly depends on serverless services: if one link in the chain slows down or are not available, your containers can't be notified. We have to say that all involved services have a very good SLA: &lt;a href="https://aws.amazon.com/it/messaging/sla/" rel="noopener noreferrer"&gt;3 nines for SQS and SNS&lt;/a&gt; and &lt;a href="https://aws.amazon.com/it/dynamodb/sla/" rel="noopener noreferrer"&gt;4 nines for DynamoDB&lt;/a&gt;. Not sure about Dynamo stream, since it appears to be not included in DynamoDB SLA. I suppose dynamo streams are backed by Kinesis Streams, &lt;a href="https://aws.amazon.com/it/kinesis/sla/" rel="noopener noreferrer"&gt;which also has 3 nines of availabilit&lt;/a&gt;y.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Open points:
&lt;/h3&gt;

&lt;p&gt;The main open point here, to me, was: is this fast enough? Let's verify it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trust, but verify
&lt;/h2&gt;

&lt;p&gt;I couldn't find any official SLA about latency for involved services nor any AWS official benchmark.&lt;/p&gt;

&lt;p&gt;So i decided to perform one myself, and i scripted a basic application using typescript and CDK / SDK.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ncremaschini/fargate-notifications" rel="noopener noreferrer"&gt;Here the github repo with the actual code&lt;/a&gt; and details on how the system is implemented.&lt;/p&gt;

&lt;p&gt;Before going ahead, bare in mind that i performed this benchmark with the goal to understand if this combination of services / configuration could fit for my specific context / use case. Your context may be different, and this configuration may not fit with it.&lt;/p&gt;

&lt;h3&gt;
  
  
  System design and data flow
&lt;/h3&gt;

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

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The AppSync API receives mutations and stores derived data in the DynamoDB table&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The DynamoDB stream the events&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Lambda function is triggered by the DynamoDB stream&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Lambda function sends the events to the SNS topic&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The SNS topic sends the events to the SQS queues&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Fargate service reads the events from the SQS queues&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If events are not processed within a timeout, they are moved to the DLQ&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A Cloudwatch alarm is triggered if the DLQ is not empty&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Key system parameters:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Region: eu-south-1&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Number of tasks: 20&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Event bus: 1 SQS per task, 1 DLQ per SQS, all SQS subscribed to one SNS&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SQS Consumer: provided by AWS SDK, configured for long polling (20s)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Task configuration: 256 CPU, 512 Memory, Docker image based on &lt;a href="https://hub.docker.com/layers/library/node/20-slim/images/sha256-80c3e9753fed11eee3021b96497ba95fe15e5a1dfc16aaf5bc66025f369e00dd?context=explore" rel="noopener noreferrer"&gt;Official Node Image 20-slim&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DynamoDB Configured in PayPerUseMode, stream enabled to trigger Lambda&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Lambda stream handler written in node20 bundled with &lt;a href="https://esbuild.github.io/" rel="noopener noreferrer"&gt;ESBuild&lt;/a&gt;, configured with 128MB&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Benchmark parameters
&lt;/h3&gt;

&lt;p&gt;I used a basic postman collection runner to perform a mutation to Appsync every 5 seconds, for 720 iterations.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Goal
&lt;/h3&gt;

&lt;p&gt;The goal was to verify if containers would be updated within 2 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measurements
&lt;/h3&gt;

&lt;p&gt;I used the following Cloudwatch provided metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Appsync latency&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Lambda latency&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Dynamo stream latency&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and I created two custom metrics for measuring SQS and SNS time taken.&lt;/p&gt;

&lt;p&gt;Time-taken custom metrics are calculated from the SNS and SQS-provided attributes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SNS Timestamp: &lt;a href="https://docs.aws.amazon.com/sns/latest/dg/sns-message-and-json-formats.html" rel="noopener noreferrer"&gt;from AWS doc&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;The time (GMT) when the notification was published.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;ApproximateFirstReceiveTimestamp: &lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_ReceiveMessage.html" rel="noopener noreferrer"&gt;from AWS doc&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;returns the time the message was first received from the queue (epoch time in milliseconds).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;SentTimestamp: &lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_ReceiveMessage.html" rel="noopener noreferrer"&gt;from AWS doc&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Returns the time the message was sent to the queue (epoch time in milliseconds).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The following code snippet shows you how attributes are used to calculate &lt;em&gt;sns time taken in millis&lt;/em&gt; and &lt;em&gt;sqs time taken in millis&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//despite the name, this is the ISO Date the message was sent to the SNS topiclet sns
ReceivedISODate = messageBody.Timestamp;
if (snsReceivedISODate &amp;amp;&amp;amp; message.Attributes) {
clientReceivedTimestamp = message.Attributes.ApproximateFirstReceiveTimestamp!; sqsReceivedTimestamp = message.Attributes.SentTimestamp!; 
let snsReceivedDate = new Date(snsReceivedISODate);
snsReceivedTimestamp = snsReceivedDate.getTime(); 
clientReceivedDate = new Date(clientReceivedTimestamp!); 
sqsReceivedDate = new Date(sqsReceivedTimestamp!); 
snsTimeTakenInMillis = sqsReceivedTimestamp - snsReceivedTimestamp; 
sqsTimeTakenInMillis = clientReceivedTimestamp -sqsReceivedTimestamp;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;i didn't calculate the time taken by the client to parse the message because it really depends on the logic the client applies to parsing the message.&lt;/p&gt;

&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Disclaimer: some latency measurements are calculated on consumers' side, and we all know that synchronizing clocks in a distributed system is a hard problem.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Still, measurements are performed by the same computing nodes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Please consider following latencies not as precise measurements but as coarse indicators.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here screenshots from my Cloudwatch dashboard&lt;/p&gt;

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

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

&lt;p&gt;Few key data, from Average numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Most of time is taken by Appsync, i couldn't do anything to lower this latency since i used native Appsync native integration with DynamoDB.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The only custom code is the Lambda stream processor code, and lamba duration is the second slowest component here. As you can see in the graph, the lambda cold start is the killer, but considering this we can observe a very good latency on avg (38 ms).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The average total time taken is &lt;strong&gt;108.39 ms&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The average response time measured by my client, that cover my client network latency, is 92 ms. Given Appsync AVG Latency is 60.5 ms, my Avg network latency is 29.5 ms. This means that from my client sending the mutation to consumers receiving the message there are 108.39 + 29.5 = &lt;strong&gt;137.89 ms&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;This solution has proven to be fast and reliable and requires little configuration to set up.&lt;/p&gt;

&lt;p&gt;Since almost everything is managed, there is little space for tuning and improvements. In this particular configuration, I could simply give the Stream Processor Lambda more memory, but memory and latency do not scale (inversely) together.&lt;/p&gt;

&lt;p&gt;&lt;del&gt;I could remove Lambda and replace it with Event Bridge Pipe. I haven't tried it yet, but i'm going to use the exact same benchmark and compare the results.&lt;/del&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UPDATE:&lt;/strong&gt; &lt;a href="https://haveyoutriedrestarting.com/evaluating-performance-a-benchmark-study-of-serverless-solutions-for-message-delivery-to-containers-on-aws-cloud-episode-2" rel="noopener noreferrer"&gt;here the benchmark of the aforementioned solution with EventBridge&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Last but not least, keep in mind that AWS does not always include latency in the service SLA. I've run this benchmark a few times with comparable results, but I can't be sure that I will always get the same results over time. If your system requires stable and predictable performance over time, you can't go with services that don't include performance metrics in their SLA. You're better off taking control of the layers below, which means &lt;a href="https://engineering.dunelm.com/pizza-as-a-service-2-0-5085cd4c365e" rel="noopener noreferrer"&gt;you should consider going to a restaurant or even making your own pizza at home.&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;In this article, I have presented you with a solution that I had to design as part of my work and my approach to solution development: this includes clarifying the scope and context, evaluating different options and having a good knowledge of the parts involved and the performance and quality attributes of the overall system, writing code and benchmarking where necessary, but always with the clear awareness that there are no perfect solutions.&lt;/p&gt;

&lt;p&gt;I hope it was helpful to you, and &lt;a href="https://github.com/ncremaschini/fargate-notifications" rel="noopener noreferrer"&gt;here is the GitHub repo to deploy both versions of the solution&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Bye 👋!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>sns</category>
      <category>sqs</category>
      <category>dynamodb</category>
    </item>
    <item>
      <title>Serverless social login with AWS Cognito</title>
      <dc:creator>Nicola Cremaschini</dc:creator>
      <pubDate>Sun, 24 Dec 2023 16:30:24 +0000</pubDate>
      <link>https://forem.com/aws-builders/serverless-social-login-with-aws-cognito-301a</link>
      <guid>https://forem.com/aws-builders/serverless-social-login-with-aws-cognito-301a</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclaimer: This is not a step-by-step guide, just my trade-off analysis on using Amazon Cognito to provide social login for your app and some pitfalls I found in my experience.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In this article, I'll show you my serverless solution to add social identity providers as a login option for web and mobile applications, based on managed services and native integrations, and how I mitigated some issues I encountered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;Let's assume you have an application for which your users do not have to register, but can log in with their social identity.&lt;/p&gt;

&lt;p&gt;If you 're wondering why, think about it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;registration could be a barrier to entry for users as it requires more steps and sharing of data&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;most internet users have at least one social identity. All mobile users have at least one (Google identity for Android users, Apple identity for Apple users)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;it is very easy for users to access your app if most of the login is done without a password&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;you can receive users data from social providers, if users allow your app to give their data to your app.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most popular social IdPs are Facebook, Google, Apple, Amazon, LinkedIn, Github and many others.&lt;/p&gt;

&lt;p&gt;Considering that every IdP should implement the OpenID Connect standard (we'll come back to this later...), which is a layer above the OAuth2 standard, and that every IdP requires some configuration, let's explore some options.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: Native integration
&lt;/h2&gt;

&lt;p&gt;Every one has its own SDK and apis to integrate natively, so you can code in your app the integration for IdPs you want to use.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Pros
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;fine-grained control over each individual IdP integration. Since each IdP is natively integrated, you can customise the specific UX via configuration and handle IdP requests that are not included in the OAuth standard (we'll get to that later...)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;direct integration, no intermediary, straightforward architecture. You can rely on robust implementations (Google / Facebook / Amazon provides good code in their SDK) and IdP resilience and H-A.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cost-effective: usually IdPs provide a free-tier for their api, so there aren't any costs from that side.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cons
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Difficult to scale: Each IdP has its own SDK and its own customs (someone said "standard?"), and it takes a lot of code is required to handle them. Even if you put your authentication logic into a library, you have to distribute it to all clients to get a change.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Hard to test / troubleshoot: more code, more tests. Moreover, different integrations require you to know IdP customs.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Option 2: use an OAuth Provider
&lt;/h2&gt;

&lt;p&gt;Since Social IdP adhere to a standard, it's easy to abstract the specific implementation (SDK) and work with interfaces, integrating with an OAuth 2 service provider.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Pros
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Just one integration, between your client and di OAuth identity platform. Less code, less test, less releases, more speed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Easy to scale: you can add/remove IdP without impacting clients (see previous bullet)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Authentication flow configuration and governance is now centralised. You can create consistent auth flow regardless the specific IdP you support, and you can monitor it and gather metrics and statistics in one place.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You build your auth flow on standards.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;There are Identity Platform as a service out there (AWS Cognito, Auth0, Google Firebase and many others)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cons
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Your integration choices are limited to IdPs supported by your OAuth provider.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Your system complexity is higher, since you add components to it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The OAuth provider could be a single point of failure. If it is not available, you cannot offer authentication to your customers. Therefore, you need to think carefully about the reliability and scaling of your OAuth provider.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My choice: Option 2 with AWS Cognito
&lt;/h2&gt;

&lt;p&gt;I'm aware that you may have many constraints and to be brief, I cannot list them all: given my context, I went with option 2 and used AWS Cognito as OAuth provider. I did a spike on Auth0 and some other services.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I decided to accept the constraints and costs of Cognito in exchange for a low-code implementation and easy setup, in other words for faster delivery, because I wasn't sure if it would be worth it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here my actual implementation&lt;/p&gt;

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

&lt;p&gt;All you need is to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;configure your integration on Social Provider side. Here a reference for each of provider i integrated with&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;configure Cognito integration. &lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/external-identity-providers.html" rel="noopener noreferrer"&gt;Here AWS Doc for each supported providers&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://aws.amazon.com/cognito/dev-resources/?nc1=h_ls" rel="noopener noreferrer"&gt;Integrate your application with Amazon Cognito&lt;/a&gt;. Cognito provides an &lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-app-integration.html" rel="noopener noreferrer"&gt;hosted ui&lt;/a&gt; for the login page, but you can create your own.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pitfalls: things to be careful about
&lt;/h2&gt;

&lt;p&gt;Here I list some of the pitfalls I have encountered in this integration. This is not an exhaustive list of what goes wrong with Amazon Cognito and the social login flow, but again my personal experience or in other words things I found during my working days.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watch out for Cognito limits
&lt;/h3&gt;

&lt;p&gt;Serverless does not mean infinite, and Cognito is one of the services that best demonstrates this.&lt;/p&gt;

&lt;p&gt;In one sentence: Cognito's scaling policy is not designed for spiky patterns.&lt;/p&gt;

&lt;p&gt;The scaling pattern is (reasonably) tied to the size of your user pool: the more users, the more TPS provided.&lt;/p&gt;

&lt;p&gt;But, and here comes the first pitfall, the first threshold is up to 1 million users. From 1 to 999999 users, you have the same TPS.&lt;/p&gt;

&lt;p&gt;This means that if your login pattern is fairly consistent, you probably won't have any problems. However, if your login pattern is spiky, perhaps because your app is tied to certain time periods in some way, your app will struggle with a lot of throttling errors from Cognito.&lt;/p&gt;

&lt;p&gt;These diagram show successful federation logins and throttling errors:&lt;/p&gt;

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

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

&lt;p&gt;i split into two distinct diagrams for better visualisation, but i want to point out that&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;around 20:50 i had ~7K throttling errors and ~1.5K of success (total requests: ~8.5K)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;around 21:20 i had ~6K throttling errors and ~1.4K success (total requests: ~7.5K)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;around 22:30 i had ~1.3K success with ZERO throttling errors&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cognito TPS calculation rules can be found &lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/limits.html" rel="noopener noreferrer"&gt;at this specific section of Cognito docs&lt;/a&gt;, and you have to carefully consider them.&lt;/p&gt;

&lt;p&gt;As you can see from the successfully login metric diagram, handling the throttling exception in your app can mitigate the user impact: they would be able to successfully login anyway, but waiting a little bit more.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;I decided that it could be acceptable, and i traded it for easy setup and integration with Social Providers.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Since this decision would impact our customer experience i tried to mitigate it as much as possible, for instance sending push notifications before traffic spikes to encourage users to log in and spread log in requests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Standards are not prescriptive
&lt;/h3&gt;

&lt;p&gt;I love standards, everybody should love them in engineering.&lt;/p&gt;

&lt;p&gt;Unfortunately, sometimes for good reasons and sometimes not, giants have bias to force standards a little bit.&lt;/p&gt;

&lt;p&gt;Apple, i'm pointing my finger at you!&lt;/p&gt;

&lt;p&gt;First, Apple's guidelines require you to log in to Apple if you want to distribute your app in the Apple Store and your app has a social login feature. That may be a bit rude, but it's fair.&lt;/p&gt;

&lt;p&gt;Again,Apple prescribe you that the "User cancellation" function must be accessible and clear. That is also fair.&lt;/p&gt;

&lt;p&gt;And here Apple does not adhere to the OAuth standard: if an Apple user allows Apple to share their data with your app, some kind of association between your app and the user also takes place in the Apple system, and if a user wants to cancel from your app (also known as your user pool), this association should also be removed.&lt;/p&gt;

&lt;p&gt;To do that, you have to invoke Apple apis to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;generate a valid access or refresh token.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;invalidate the freshly generated token.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://developer.apple.com/documentation/sign_in_with_apple/revoke_tokens" rel="noopener noreferrer"&gt;Sounds weird, but this is exactly what this doc page prescribes.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And, guess what? Cognito doesn't handle it.&lt;/p&gt;

&lt;p&gt;Even if Cognito could handle it because it has all the information it needs, especially the private key you created on the Apple side and provided to Cognito to request the tokens, that's reasonable from a product perspective: Cognito adheres to standards and can't track every specific implementation.&lt;/p&gt;

&lt;p&gt;But it does mean that Apple won't include your app in the store if you don't take care of it.&lt;/p&gt;

&lt;p&gt;So let's take a look at how to implement it.&lt;/p&gt;

&lt;p&gt;You can't implement it in the app: i used Cognito to decouple the app from auth providers, and i don't want to violate that requirement. Besides, you don't want to store your private key on the device, do you?&lt;/p&gt;

&lt;p&gt;So you need to implement it on the backend side. My first idea was to use events to respond to the Cognito event to delete a user and trigger a lambda that calls the Apple api to delete the user on the Apple side.&lt;/p&gt;

&lt;p&gt;As far as I know, Cognito today has&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html#cognito-user-pools-lambda-trigger-event-parameter-shared" rel="noopener noreferrer"&gt;Lambda triggers&lt;/a&gt;: user deletion not supported&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-info-in-cloudtrail.html" rel="noopener noreferrer"&gt;Cloudtrail tracks all management api calls&lt;/a&gt;, and user cancellation is a management api. But Cloudtrail event doesn't have any reference to actual user (and it saved my day in an audit session, but this is another story)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-events.html" rel="noopener noreferrer"&gt;Cognito Sync&lt;/a&gt;: it seems to handle user deletion. Quoting:&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is how it looks like:&lt;/p&gt;

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

&lt;p&gt;I see two problems here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;first, you have to put your Apple's private key in Cognito and in Secret Manager. Cognito can't retrieve it from Secret Manager. I raised this issue to Cognito team, keep you posted on this.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;second, Cognito user cancellation and Apple user cancellation are asynchronous: what if it success on Cognito side and than fails on Apple side? User wont be in our Cognito user pool anymore, so we can't rollback the operation. So you need to handle failures, and to handle it you need to store it. Let's add a DLQ for our deletion lambda&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;After saving, you must analyse why the deletion failed and try again. How long can this take? It depends on the cause and your process, but until you've done that, users will still see their user associated with your app, and I'm not sure Apple would like it and approve your app submission.&lt;/p&gt;

&lt;p&gt;You need to reverse the order of deletion, first on the Apple side and then on the Cognito side. If the Apple deletion fails, you can send an error message to the user and inform he/she that the deletion cannot be performed and they should try again later.&lt;/p&gt;

&lt;p&gt;In the case of a Cognito error, you will have to do this later, but at least the user will not see that their user is linked to your app and Apple should be satisfied and approve your request.&lt;/p&gt;

&lt;p&gt;Let's see how it looks like&lt;/p&gt;

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

&lt;p&gt;I still see two problems here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Again , you have to put your Apple's private key in Cognito and in Secret Manager.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Your app now is integrated with two systems: Cognito for Sign-in operation and your custom api for user deletion&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both solutions somehow solve the problem and both raise new concerns, so I had to opt for the less bad one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I decided to implement a custom api for Apple user deletion because it can be implemented just in half our code base (not for Android version of the app), the integration is quite simple and Apple would be happy with this solution, but probably not with the alternative solution. Still an error handling mechanism still need to be implemented to catch Cognito deletion errors and to recover them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Wrap up
&lt;/h2&gt;

&lt;p&gt;I have shown you my solution to real-world problems and how you can make informed decisions by carefully weighing trade-offs between different solutions that best fit your context and constraints.&lt;/p&gt;

&lt;p&gt;In other words, the daily work of an architect, simplified.&lt;/p&gt;

&lt;p&gt;Architectures need to evolve as the context and constraints change over time. So always design your solutions so that they can easily evolve with them.&lt;/p&gt;

&lt;p&gt;I hope it was useful for you!&lt;/p&gt;

&lt;p&gt;Bye 👋!&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>cognito</category>
      <category>sociallogin</category>
      <category>oauth</category>
    </item>
  </channel>
</rss>
