<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Moslem Chalfouh</title>
    <description>The latest articles on DEV Community by Moslem Chalfouh (@moslem_chalfouh_967e323f7).</description>
    <link>https://dev.to/moslem_chalfouh_967e323f7</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%2F1400085%2F0cf443ce-6603-43f7-90f0-9c234cef4131.jpeg</url>
      <title>DEV Community: Moslem Chalfouh</title>
      <link>https://dev.to/moslem_chalfouh_967e323f7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/moslem_chalfouh_967e323f7"/>
    <language>en</language>
    <item>
      <title>Kafka Retry Done Right: The Day I Chose a Simpler Fix Over @RetryableTopic</title>
      <dc:creator>Moslem Chalfouh</dc:creator>
      <pubDate>Sun, 22 Feb 2026 15:56:52 +0000</pubDate>
      <link>https://dev.to/moslem_chalfouh_967e323f7/kafka-retry-done-right-the-day-i-chose-a-simpler-fix-over-retryabletopic-31lp</link>
      <guid>https://dev.to/moslem_chalfouh_967e323f7/kafka-retry-done-right-the-day-i-chose-a-simpler-fix-over-retryabletopic-31lp</guid>
      <description>&lt;h4&gt;
  
  
  When the event is valid but the entity isn’t ready
&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;Context: Spring Kafka, Confluent Cloud, Java enterprise backend.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem: The Update Succeeded… And That Was The Bug
&lt;/h3&gt;

&lt;p&gt;A Kafka consumer. An incoming event carrying data to apply to a core entity. A downstream archival process. Until I found silently corrupted entities in production with &lt;strong&gt;zero errors in the logs&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The update logic was correct. The entity existed. The data in the event was valid.&lt;/p&gt;

&lt;p&gt;But the entity wasn’t in the right &lt;strong&gt;lifecycle state&lt;/strong&gt; to receive this update yet. An internal validation workflow was still in progress upstream.&lt;/p&gt;

&lt;p&gt;My consumer didn’t know. It applied the update anyway — successfully — and &lt;strong&gt;silently corrupted the entity’s lifecycle in production&lt;/strong&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%2Fobp5wvpfeivj27actnap.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%2Fobp5wvpfeivj27actnap.png" alt="Expected lifecycle vs. Reality: The update succeeds, but breaks the logic order." width="800" height="430"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Expected lifecycle vs. Reality: The update succeeds, but breaks the logic order.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A silent lifecycle violation is worse than a crash, nothing alerts, nothing fails visibly: &lt;strong&gt;a technically successful operation at the wrong moment is worse than a failure.&lt;/strong&gt; A failure you can detect. A silent lifecycle corruption you cannot.&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%2Fjk8199zv6ie30j7u40pi.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%2Fjk8199zv6ie30j7u40pi.png" width="741" height="636"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The gap between “Existence” and “Readiness” is where bugs live.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Two Retry Approaches That Look Fine (Until They Hurt)
&lt;/h3&gt;
&lt;h3&gt;
  
  
  Trap #1 — “I’ll just poll until it’s ready”
&lt;/h3&gt;

&lt;p&gt;If I keep the record “in-flight” and refuse to commit the offset until the entity is ready, I don’t only delay this one message. Kafka is FIFO &lt;strong&gt;per partition&lt;/strong&gt; : everything behind that offset on that partition is stuck.&lt;/p&gt;

&lt;p&gt;With concurrency=3, it's not "the broker is blocked" — it's &lt;strong&gt;one partition's entire throughput&lt;/strong&gt; that stalls, silently, under load.&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%2Fjkmocoyvqm3lxf9nbuxr.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%2Fjkmocoyvqm3lxf9nbuxr.png" width="800" height="265"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Holding the offset freezes the queue. Partition 0 is stuck, while others flow.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Trap #2 — “I’ll Thread.sleep() and try again"
&lt;/h3&gt;

&lt;p&gt;Sleeping in the listener thread is a classic way to accidentally trigger a rebalance. If the consumer stops polling for longer than max.poll.interval.ms, the broker assumes the consumer is dead, rebalances the group, and the message gets replayed — potentially forever.&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%2Fj4ozddxwilmdj2o4ugb8.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%2Fj4ozddxwilmdj2o4ugb8.png" width="494" height="556"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The infinite rebalance loop: Broker thinks consumer is dead.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  The “Ideal” Pattern I Didn’t Use (On Purpose)
&lt;/h3&gt;

&lt;p&gt;In theory, &lt;strong&gt;non-blocking retry&lt;/strong&gt; is the cleanest approach: acknowledge the message immediately, park it in a dedicated retry topic, and process it later without blocking the main partition. That’s exactly what Spring Kafka’s @RetryableTopic gives you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@RetryableTopic(
    attempts = "4",
    backoff = @Backoff(delay = 30_000, multiplier = 4),
    include = EntityNotReadyException.class
)
@KafkaListener(topics = "entity-created")
public void consume(String message) { ... }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, RetryableTopic creates dedicated retry topics automatically (e.g. entity-created-retry-0, entity-created-dlt).&lt;/p&gt;

&lt;p&gt;The key benefit: the offset is committed immediately, so the main partition keeps flowing while the message is retried asynchronously.&lt;/p&gt;

&lt;p&gt;It’s a clean option when you’re allowed to create the extra topics.&lt;/p&gt;

&lt;p&gt;One important nuance: RetryableTopic doesn’t make retries disappear — it moves them to intermediate retry topics that Spring Kafka creates and consumes automatically. The delay is enforced by a separate consumer on those retry topics. If the retry topic consumer also fails, you end up managing a second-level DLT. Elegant, yes — but not zero-complexity.&lt;/p&gt;

&lt;p&gt;Why I didn’t use it: in a restricted enterprise environment (managed Confluent Cloud cluster, governance rules), creating extra retry topics can be &lt;strong&gt;forbidden or slow to approve&lt;/strong&gt;. Topic creation goes through a ticket queue — or a flat “no”. I couldn’t assume I had that lever.&lt;/p&gt;

&lt;p&gt;So I went with the best solution available &lt;em&gt;within my constraints&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Business Question That Made The Architecture Obvious
&lt;/h3&gt;

&lt;p&gt;Before designing anything, I asked one question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“How long does it usually take for the entity to reach its stable state after the event fires?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Answer: &lt;strong&gt;“A few minutes. Not guaranteed, but usually fast.”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That changed everything. I didn’t need a sophisticated non-blocking retry topology. I needed a pragmatic delay to cover the common case, plus a safety net for the rest. &lt;strong&gt;I sized the solution for the failure rate we actually observed — not for a theoretical worst case.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Fix I Shipped: Reuse What Was Already There
&lt;/h3&gt;

&lt;p&gt;The project already had a retry mechanism wired through Spring Kafka’s DefaultErrorHandler with exponential backoff — already handling transient failures like network timeouts. I just needed to plug in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// This is essentially the only new business logic I added
if (!isEntityInStableState(fetchedEntity)) {
    throw new EntityNotReadyException(
        "Entity lifecycle not ready: " + event.getEntityId()
    );
}
// → DefaultErrorHandler takes over automatically
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The (simplified for confidentiality) backoff configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ExponentialBackOff backOff = new ExponentialBackOff(120_000L, 2.0);
backOff.setMaxInterval(600_000L); // cap at 10 min per attempt
backOff.setMaxElapsedTime(3_600_000L); // stop retrying after ~1h
DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(kafkaTemplate);
DefaultErrorHandler errorHandler = new DefaultErrorHandler(recoverer, backOff);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Retry schedule: 2 min → 4 min → 8 min → 10 min → 10 min → … → DLQ&lt;/p&gt;

&lt;p&gt;⚠️ Critical rule: max.poll.interval.ms must be strictly greater than maxInterval (your per-attempt cap), not maxElapsedTime. The thread only blocks for one interval at a time, not for the entire retry window.&lt;br&gt;&lt;br&gt;
Concrete example: with maxInterval = 600,000 ms (10 min), set max.poll.interval.ms = 660,000 (11 min). Setting it to match maxElapsedTime (1h+) would dangerously delay dead consumer detection.&lt;/p&gt;

&lt;p&gt;In production, a &lt;strong&gt;CloudWatch alarm on the DLQ&lt;/strong&gt; ensures the on-call team is notified if an entity never reaches its stable state after 1 hour. Don’t ship a retry mechanism without a safety net you can actually see.&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%2F87yr3kke0t2deb7a5gsr.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%2F87yr3kke0t2deb7a5gsr.png" width="800" height="474"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Only 1/3 of throughput is paused during backoff. The rest flows.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision Framework (Cheat Sheet)
&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%2Fjvd7029wkq5c2wxld72s.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%2Fjvd7029wkq5c2wxld72s.png" width="800" height="837"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The cheat sheet: Start simple, reuse existing infrastructure, and only accept partition-blocking if your traffic allows it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In short: if you can create topics → RetryableTopic. If you’re on a governed broker with rare failures → DefaultErrorHandler + backoff. If failures are frequent at high volume → fix the upstream contract.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I Learned: Complexity Is a Choice
&lt;/h3&gt;

&lt;p&gt;The temptation in event-driven systems is to reach for the most powerful tool available. Complexity is a choice — and in this case, I chose not to make it.&lt;/p&gt;

&lt;p&gt;This wasn’t a Kafka feature problem. It was a &lt;strong&gt;definition problem&lt;/strong&gt; : the event signaled &lt;em&gt;data availability&lt;/em&gt;, while my process assumed &lt;em&gt;entity readiness&lt;/em&gt;. That gap is invisible until it isn’t — and when it surfaces, it leaves no trace in your logs.&lt;/p&gt;

&lt;p&gt;Technically, I could have fought for @RetryableTopic. I could have built a custom non-blocking retry topology. I could have asked the upstream team to delay their event trigger. Instead, I aligned with business reality ("usually a few minutes") and chose the simplest architecture that respected Kafka's partition semantics and my enterprise constraints.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The best architecture decision I made that week was asking a business question before opening my IDE.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>microservices</category>
      <category>java</category>
      <category>architecture</category>
    </item>
    <item>
      <title>What I Learned Deploying My First RAG System on AWS Bedrock</title>
      <dc:creator>Moslem Chalfouh</dc:creator>
      <pubDate>Wed, 31 Dec 2025 14:55:43 +0000</pubDate>
      <link>https://dev.to/moslem_chalfouh_967e323f7/what-i-learned-deploying-my-first-rag-system-on-aws-bedrock-19lj</link>
      <guid>https://dev.to/moslem_chalfouh_967e323f7/what-i-learned-deploying-my-first-rag-system-on-aws-bedrock-19lj</guid>
      <description>&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%2F7y2vckss33m0kitndlt3.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%2F7y2vckss33m0kitndlt3.png" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For the past two years, I’ve been using Generative AI tools as an enthusiast — experimenting with prompts, testing models, seeing what they could do.&lt;/p&gt;

&lt;p&gt;Over the last 12 months, I shifted my focus to understanding how to use these tools professionally on AWS. Not just “Can I build a chatbot?” but “How do I deploy this securely with proper infrastructure?”&lt;/p&gt;

&lt;p&gt;To validate what I’d been learning about AWS AI workflows, I decided to build a concrete example: a RAG chatbot using &lt;strong&gt;AWS Bedrock Knowledge Bases&lt;/strong&gt; , &lt;strong&gt;Aurora Serverless v2&lt;/strong&gt; , and &lt;strong&gt;Terraform&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This article walks through that build process — what worked, what didn’t, and what I had to do manually because Terraform couldn’t handle it yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The “Why”: RAG and Embeddings
&lt;/h3&gt;

&lt;p&gt;The first step was understanding why we need this complexity. Large Language Models (LLMs) like Claude have a fixed knowledge cutoff and don’t know my private data.&lt;/p&gt;

&lt;p&gt;We could paste documents into the prompt, but that hits token limits quickly. The standard solution is RAG:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ingest:&lt;/strong&gt; Convert documents into vectors (embeddings).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store:&lt;/strong&gt; Save them in a vector database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieve:&lt;/strong&gt; Find relevant chunks when a user asks a question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate:&lt;/strong&gt; Send those chunks to the LLM to write an answer.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  What Are Embeddings?
&lt;/h4&gt;

&lt;p&gt;Embeddings are mathematical representations of text meaning. The Titan Embeddings model converts text into a vector of 1,536 numbers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Tesla Model Y range: 455 km" → [0.23, −0.45, 0.87, …, 0.12]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: &lt;strong&gt;similar meanings produce similar vectors&lt;/strong&gt;. “Range,” “autonomy,” and “distance per charge” all generate nearby vectors, even though the words are different. This solves the problem of traditional keyword search, which only finds exact matches.&lt;/p&gt;

&lt;h4&gt;
  
  
  Why Vector Databases?
&lt;/h4&gt;

&lt;p&gt;Regular databases search for exact matches (WHERE id = 123). With embeddings, you need &lt;strong&gt;similarity search&lt;/strong&gt; : "Find the 5 closest vectors to my query."&lt;/p&gt;

&lt;p&gt;This requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Specialized indexes (HNSW) to organize vectors spatially&lt;/li&gt;
&lt;li&gt;Distance calculations (cosine similarity) across thousands of vectors&lt;/li&gt;
&lt;li&gt;Fast retrieval (milliseconds, not seconds)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s what pg_vector adds to Postgres—a vector(1536) column type and similarity search operators. Without it, searching embeddings would be impossibly slow.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Stack: Turning Theory Into Practice
&lt;/h3&gt;

&lt;p&gt;Now that we understand the concepts, how do we actually implement this on AWS? I wanted an architecture that handled all the embedding and vector search complexity while staying simple to operate.&lt;/p&gt;

&lt;p&gt;Here’s what I chose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Compute:&lt;/strong&gt; AWS Bedrock (Serverless AI).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embedding Model:&lt;/strong&gt; Titan Embeddings G1 (managed by Bedrock).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vector Storage:&lt;/strong&gt; Aurora Serverless v2 with pg_vector extension.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document Storage:&lt;/strong&gt; S3 (for source PDFs).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IaC:&lt;/strong&gt; Terraform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UI:&lt;/strong&gt; Streamlit (Python).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why this stack?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Bedrock Knowledge Base handles the entire RAG workflow automatically — chunking documents, calling the Titan embedding model, and storing vectors in Aurora. I don’t write the embedding logic; I just configure where the vectors go.&lt;/p&gt;

&lt;p&gt;Aurora with pg_vector was chosen over specialized vector databases (like Pinecone or Weaviate) for simplicity. It's Postgres with a vector extension—one SQL command to enable, and I can use standard database tooling I already know.&lt;/p&gt;

&lt;h4&gt;
  
  
  Aurora Limitations to Know
&lt;/h4&gt;

&lt;p&gt;pg_vector works great for this use case, but keep in mind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HNSW indexes load into memory. With ~10,000 documents (50k chunks), you’re looking at ~300MB of vector data.&lt;/li&gt;
&lt;li&gt;Query performance may degrade above 100,000 vectors. At that scale, consider OpenSearch Serverless.&lt;/li&gt;
&lt;li&gt;No distributed search — Aurora is single-instance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For knowledge bases under 5,000 documents, Aurora + pg_vector is the simplest choice.&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%2Fa25mbjd89z9c3d494dg8.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%2Fa25mbjd89z9c3d494dg8.png" width="800" height="449"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The complete workflow: User → Guardrail → Bedrock KB → Aurora → Claude&lt;/em&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%2Fp5enzso6npb3uf8b6a2h.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%2Fp5enzso6npb3uf8b6a2h.png" width="800" height="500"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The final result: A Streamlit interface querying proprietary data via Bedrock&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  3. The Terraform Struggle: Circular Dependencies
&lt;/h3&gt;

&lt;p&gt;When I tried to automate the deployment, I hit a logic problem. Bedrock Knowledge Base needs the Aurora Cluster ARN to know where to store data. However, the IAM Role for Bedrock needs permission to access specific Aurora tables.&lt;/p&gt;

&lt;p&gt;Trying to do this in one terraform apply resulted in errors because Terraform couldn't resolve the dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt; I split the project into two separate stacks.&lt;/p&gt;
&lt;h4&gt;
  
  
  Stack 1: Infrastructure
&lt;/h4&gt;

&lt;p&gt;Deploys the VPC, S3 bucket, and Aurora Cluster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Aurora cluster
resource "aws_rds_cluster" "aurora_serverless" {
  engine = "aurora-postgresql"
  engine_mode = "provisioned"
  serverlessv2_scaling_configuration {
    min_capacity = 0.5
    max_capacity = 16
  }
}

# S3 bucket for documents
resource "aws_s3_bucket" "documents" {
  bucket = "my-bedrock-documents"
}
output "aurora_cluster_arn" {
  value = aws_rds_cluster.aurora_serverless.arn
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Stack 2: Bedrock Knowledge Base
&lt;/h4&gt;

&lt;p&gt;Reads the outputs from Stack 1 via terraform_remote_state and deploys the Knowledge Base.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data "terraform_remote_state" "stack1" {
  backend = "s3"
  config = {
    bucket = "my-terraform-state"
    key = "stack1/terraform.tfstate"
  }
}
resource "aws_bedrockagent_knowledge_base" "main" {
  name = "my-bedrock-kb"
  role_arn = aws_iam_role.bedrock_kb_role.arn
  knowledge_base_configuration {
    vector_knowledge_base_configuration {
      embedding_model_arn = "arn:aws:bedrock:us-west-2::foundation-model/amazon.titan-embed-text-v1"
    }
    type = "VECTOR"
  }
  storage_configuration {
    type = "RDS"
    rds_configuration {
      credentials_secret_arn = data.terraform_remote_state.stack1.outputs.aurora_secret_arn
      resource_arn = data.terraform_remote_state.stack1.outputs.aurora_cluster_arn
      database_name = "myapp"
      table_name = "bedrock_integration.bedrock_kb"
      field_mapping {
        vector_field = "embedding"
        text_field = "chunks"
        metadata_field = "metadata"
        primary_key_field = "id"
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation made the state management much cleaner and avoided the circular dependency hell.&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%2F1fk7bdpm0kopgiq5eiai.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%2F1fk7bdpm0kopgiq5eiai.png" width="800" height="792"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Success: The Knowledge Base deployed via Terraform and ready in the console&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  The IAM Role Bedrock Needs
&lt;/h4&gt;

&lt;p&gt;Getting IAM right was critical. Bedrock needs specific permissions to talk to S3, Secrets Manager, and Aurora.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "aws_iam_role_policy" "bedrock_kb_policy" {
  role = aws_iam_role.bedrock_kb_role.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = ["s3:GetObject", "s3:ListBucket"]
        Resource = ["${aws_s3_bucket.documents.arn}", "${aws_s3_bucket.documents.arn}/*"]
      },
      {
        Effect = "Allow"
        Action = ["bedrock:InvokeModel"]
        Resource = "arn:aws:bedrock:*::foundation-model/amazon.titan-embed-text-v1"
      },
      {
        Effect = "Allow"
        Action = ["secretsmanager:GetSecretValue"]
        Resource = aws_secretsmanager_secret.aurora_credentials.arn
      },
      {
        Effect = "Allow"
        Action = ["rds-data:ExecuteStatement", "rds-data:BatchExecuteStatement"]
        Resource = aws_rds_cluster.aurora_serverless.arn
      }
    ]
  })
}

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

&lt;/div&gt;



&lt;p&gt;Missing any of these results in silent failures during sync or query.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The Manual Parts (and One That Is Becoming Obsolete)
&lt;/h3&gt;

&lt;p&gt;Despite using Terraform, I realized that AWS Bedrock isn’t fully automatable yet. However, the platform is maturing fast.&lt;/p&gt;

&lt;h4&gt;
  
  
  Model Access (The “Ghost” Step)
&lt;/h4&gt;

&lt;p&gt;When I started this project back in October, I hit a wall: AccessDeniedException. I had to manually go into the AWS Console and request access for "Titan Embeddings" and "Claude". It was a one-time toggle that Terraform couldn't handle.&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%2Fv27a2qspbnw07ux9j3l7.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%2Fv27a2qspbnw07ux9j3l7.png" width="800" height="499"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Model Access screen (a necessary stop for older accounts)&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Good news for you:&lt;/strong&gt; As of late 2025, AWS has largely removed this requirement. Most serverless models are now enabled by default in supported regions. If you are building this today, you likely won’t need to touch this, but if you get a permission error, check the &lt;strong&gt;Model Access&lt;/strong&gt; page just in case.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;
  
  
  Database Schema
&lt;/h4&gt;

&lt;p&gt;This is still a manual friction point. Bedrock expects the table to exist before it can sync, but it won’t create it for you. I had to connect to Aurora and run the SQL setup manually:&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%2F8zi5upvcoxhv9ejivwbi.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%2F8zi5upvcoxhv9ejivwbi.png" width="800" height="637"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Manually running the SQL in the Query Editor to create the pg_vector schema&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;CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE bedrock_integration.bedrock_kb (
  id uuid PRIMARY KEY,
  embedding vector(1536), -- Matches Titan G1 output
  chunks text,
  metadata json
);
CREATE INDEX ON bedrock_integration.bedrock_kb USING hnsw (embedding vector_cosine_ops);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Data Sync
&lt;/h4&gt;

&lt;p&gt;Uploading a file to S3 doesn’t automatically trigger ingestion. You still have to manually click “Sync” in the console or trigger it via the API (start_ingestion_job).&lt;/p&gt;

&lt;h3&gt;
  
  
  5. The Python Layer: Keeping It Simple
&lt;/h3&gt;

&lt;p&gt;I’m not a Python developer, so I kept the code minimal and modular. Two files handle everything:&lt;/p&gt;

&lt;h4&gt;
  
  
  bedrock_utils.py (The RAG Logic)
&lt;/h4&gt;

&lt;p&gt;This file contains key functions using two different Bedrock clients.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import boto3

# Two separate clients for different purposes
bedrock_runtime = boto3.client('bedrock-runtime')  
bedrock_agent_runtime = boto3.client('bedrock-agent-runtime')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;bedrock-runtime:&lt;/strong&gt; For invoking foundation models (Claude)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;bedrock-agent-runtime:&lt;/strong&gt; For querying the Knowledge Base&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation was confusing at first, but it makes sense once you understand that the Knowledge Base is technically an “agent” service, while model invocation is a “runtime” service.&lt;/p&gt;

&lt;h4&gt;
  
  
  app.py (The Streamlit Interface)
&lt;/h4&gt;

&lt;p&gt;The UI is straightforward — 54 lines total. The Streamlit framework handles the chat history and UI rendering automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. The Guardrail Pattern (Saving Cost &amp;amp; Tokens)
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Most RAG tutorials skip this step. They assume every query is valid. In reality, users will ask off-topic questions (“What’s the weather?”), which triggers expensive vector searches for nothing.&lt;/p&gt;

&lt;p&gt;Adding a 10-token classification step before RAG saves both money and UX.&lt;/p&gt;

&lt;p&gt;During testing, I noticed that every question triggered the full RAG process, which takes time and costs money. A simple “Hello” shouldn’t trigger an expensive vector search.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I added a validation step before RAG using bedrock-runtime directly to classify the intent with a cheaper model (Claude Haiku). The function checks if the user's question falls into predefined categories before triggering the expensive RAG workflow.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Note:&lt;/em&gt;&lt;/strong&gt; &lt;em&gt;My categories are specific to my test documents (local biodiversity, town history, Tesla specs). For your use case, replace these with your own domain-specific categories. The key is to keep it simple — 3–4 categories maximum for reliable classification.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&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%2Fav1bq8hxjs6unnk1gmr0.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%2Fav1bq8hxjs6unnk1gmr0.png" width="800" height="503"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The logs showing the guardrail in action: Valid categories trigger RAG, while off-topic inputs are filtered out&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This simple pattern saved tokens and made the application feel more responsive. The guardrail uses only ~10 tokens, while a full RAG query can use 200–500 tokens.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Conclusion
&lt;/h3&gt;

&lt;p&gt;Building this project clarified a few things for me about the AWS AI ecosystem:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure is key:&lt;/strong&gt; The Python code is short, but the IAM roles, Terraform configuration, and Network setup took the most time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aurora is enough:&lt;/strong&gt; You don’t necessarily need a specialized vector DB; Postgres works fine for this scale and is easier to maintain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bedrock abstractions work:&lt;/strong&gt; The retrieve_and_generate API effectively hides the complexity of vector search, letting you focus on the application logic.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What’s Missing for Production
&lt;/h3&gt;

&lt;p&gt;This is a &lt;strong&gt;learning project&lt;/strong&gt; , not a production system. To deploy this in a real enterprise environment, you’d need to add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authentication &amp;amp; Authorization:&lt;/strong&gt; No login system, no role-based access control&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Gateway + Lambda:&lt;/strong&gt; Replace Streamlit with a proper REST API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD Pipeline:&lt;/strong&gt; Automated testing and deployment (GitHub Actions, CodePipeline)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost Monitoring:&lt;/strong&gt; Budget alerts, usage tracking per user/department&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging &amp;amp; Observability:&lt;/strong&gt; CloudWatch dashboards, distributed tracing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security hardening:&lt;/strong&gt; VPC endpoints, encryption at rest, audit trails&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting:&lt;/strong&gt; Prevent abuse and control Bedrock costs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal here was to understand how the pieces fit together, not to build a turnkey solution.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/cmoslem/terraform-bedrock-rag" rel="noopener noreferrer"&gt;👉 View on GitHub: terraform-bedrock-rag&lt;/a&gt;&lt;/p&gt;




</description>
      <category>aws</category>
      <category>cloudcomputing</category>
      <category>generativeaitools</category>
      <category>terraform</category>
    </item>
    <item>
      <title>ECS, Lambda, or EC2? How Hexagonal Architecture Made the Choice Irrelevant</title>
      <dc:creator>Moslem Chalfouh</dc:creator>
      <pubDate>Mon, 22 Dec 2025 17:28:45 +0000</pubDate>
      <link>https://dev.to/moslem_chalfouh_967e323f7/ecs-lambda-or-ec2-how-hexagonal-architecture-made-the-choice-irrelevant-24bj</link>
      <guid>https://dev.to/moslem_chalfouh_967e323f7/ecs-lambda-or-ec2-how-hexagonal-architecture-made-the-choice-irrelevant-24bj</guid>
      <description>&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%2Fpbl3uvr81y88emv5hafi.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%2Fpbl3uvr81y88emv5hafi.png" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You start a project locally. Everything runs smoothly, tests are green, and infrastructure feels like a “later” problem. Then “later” arrives — bringing deadlines, security compliance, and platform changes.&lt;/p&gt;

&lt;p&gt;On a recent project, a Kafka-driven Java service went through &lt;strong&gt;three major infrastructure pivots&lt;/strong&gt; before hitting production: containers, serverless, and finally classic EC2. The service was designed to generate business documents and call on-premises APIs.&lt;/p&gt;

&lt;p&gt;The only reason I could pivot the project without a full rewrite was strict adherence to &lt;strong&gt;Hexagonal Architecture&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is the story of how that structure absorbed the chaos.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Use Case: Kafka In, Legacy Out
&lt;/h3&gt;

&lt;p&gt;On paper, the functional requirement was deceptively simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Consume&lt;/strong&gt; events from a Kafka topic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply&lt;/strong&gt; routing and validation rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate&lt;/strong&gt; a business document.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Call&lt;/strong&gt; an on-premises API to update downstream processes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Locally, it was just a Spring Boot app with some JSON and a few services.&lt;/p&gt;

&lt;p&gt;I focused purely on the domain model and boundaries, ignoring whether the entry point would eventually be a Lambda handler, a Kafka listener, or a container.&lt;/p&gt;

&lt;h3&gt;
  
  
  Act I — The Container Hype (ECS)
&lt;/h3&gt;

&lt;p&gt;Initially, the plan was to use &lt;strong&gt;Amazon ECS&lt;/strong&gt;. It was the exciting option: containerize the app, push it, and run it in a managed cluster.&lt;/p&gt;

&lt;p&gt;But there was a hidden constraint. While ECS was trendy among delivery teams, it &lt;strong&gt;was not yet an officially approved standard&lt;/strong&gt; for our security and compliance department. This meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extra validation steps.&lt;/li&gt;
&lt;li&gt;Uncertain timelines.&lt;/li&gt;
&lt;li&gt;A high risk of a “No-Go” decision right before launch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a project under strict delivery pressure, betting on a platform still awaiting approval was a gamble we couldn’t afford. I had to pivot.&lt;/p&gt;

&lt;h3&gt;
  
  
  Act II — The Serverless Promise (Lambda + Kafka Connector)
&lt;/h3&gt;

&lt;p&gt;The logical plan B was &lt;strong&gt;Serverless&lt;/strong&gt;. Infrastructure teams wanted to avoid OS patching, and AWS Lambda fit that bill perfectly.&lt;/p&gt;

&lt;p&gt;The architecture seemed elegant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Messages arrive in Kafka (Confluent).&lt;/li&gt;
&lt;li&gt;A Kafka connector pushes them to a Lambda trigger.&lt;/li&gt;
&lt;li&gt;The Lambda processes the event, generates the document, calls the API, and vanishes.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Java on Lambda: Debunking Myths
&lt;/h3&gt;

&lt;p&gt;Despite skepticism about running Java on Lambda (cold starts, heavy runtime), I leveraged a modern stack: &lt;strong&gt;Java 21 + Spring Boot 3&lt;/strong&gt;. I used &lt;strong&gt;Virtual Threads&lt;/strong&gt; for I/O-bound efficiency and &lt;strong&gt;SnapStart&lt;/strong&gt; to reduce cold start latency.&lt;/p&gt;

&lt;p&gt;Technically, it worked. Locally and in non-prod, the Lambda accepted payloads, mapped them to domain objects, and executed the business logic perfectly.&lt;/p&gt;

&lt;p&gt;Then &lt;strong&gt;organizational reality hit&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Compliance Wall
&lt;/h3&gt;

&lt;p&gt;Just before go-live, a new constraint dropped: the specific Confluent connector required to trigger the Lambda was &lt;strong&gt;not qualified for production&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The consequences were immediate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The push model (Connector → Lambda) was banned.&lt;/li&gt;
&lt;li&gt;The service had to &lt;strong&gt;consume directly from Kafka&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Serverless was effectively dead for this release.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I needed a third option. Fast.&lt;/p&gt;

&lt;h3&gt;
  
  
  Act III — Landing on EC2 (And Why It Didn’t Hurt)
&lt;/h3&gt;

&lt;p&gt;With two options off the table, we turned to the most battle-tested solution available: &lt;strong&gt;a classic EC2 instance&lt;/strong&gt; running a Spring Boot application.&lt;/p&gt;

&lt;p&gt;In a tightly coupled architecture, this would have required major surgery: rewriting entry points, refactoring message parsing, and risking regression in the business logic.&lt;/p&gt;

&lt;p&gt;But for us? The impact was trivial.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Real Hero: Hexagonal Architecture
&lt;/h3&gt;

&lt;p&gt;Because I had structured the service around clear Hexagonal principles, our project structure looked 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%2F00nirz5ieifmrn45z1y8.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%2F00nirz5ieifmrn45z1y8.png" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;only&lt;/strong&gt; thing that changed across our three AWS pivots was the &lt;em&gt;Driving Adapter&lt;/em&gt; (the left side).&lt;/p&gt;

&lt;h3&gt;
  
  
  The Stable Center
&lt;/h3&gt;

&lt;p&gt;The use case remained untouched:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// application/port/in/ProcessRequestUseCase.java
public interface ProcessRequestUseCase {
    void process(BusinessRequestEvent event);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The domain model never knew if the data came from a Lambda JSON payload or a Kafka ConsumerRecord.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adapter 1 — The Lambda Approach (Abandoned)
&lt;/h3&gt;

&lt;p&gt;When we targeted Lambda, our entry point looked 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;// infrastructure/lambda/LambdaHandler.java
public class LambdaHandler implements RequestHandler&amp;lt;Map&amp;lt;String, Object&amp;gt;, String&amp;gt; {
    private final ProcessRequestUseCase useCase;

    @Override
    public String handleRequest(Map&amp;lt;String, Object&amp;gt; event, Context context) {
        // Adapt JSON -&amp;gt; Domain
        BusinessRequestEvent domainEvent = map(event);
        useCase.process(domainEvent);
        return "OK";
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Adapter 2 — The EC2 Approach (Final Production)
&lt;/h3&gt;

&lt;p&gt;When we switched to EC2, we simply swapped in a Spring Kafka listener:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// infrastructure/kafka/KafkaConsumerListener.java
@Component
public class KafkaConsumerListener {
    private final ProcessRequestUseCase useCase;

    @KafkaListener(topics = "${app.kafka.topic}", groupId = "${app.kafka.group}")
    public void onMessage(ConsumerRecord&amp;lt;String, String&amp;gt; record) {
        // Adapt Kafka Record -&amp;gt; Domain
        BusinessRequestEvent domainEvent = map(record.value());
        useCase.process(domainEvent);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What changed?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Infrastructure concerns (annotations, configuration, scaling).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What stayed the same?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Domain Model.&lt;/li&gt;
&lt;li&gt;The Use Case API.&lt;/li&gt;
&lt;li&gt;The entire business logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This decoupling is precisely what allowed the service to survive three infrastructure decisions without rewriting a single line of business code.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Takeaway
&lt;/h3&gt;

&lt;p&gt;The story isn’t about “Containers vs. Serverless vs. EC2.” Those are implementation details that will inevitably change based on cost, trends, and governance.&lt;/p&gt;

&lt;p&gt;The real lesson is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure is volatile&lt;/strong&gt;  — internal standards and compliance rules are moving targets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business logic should be stable&lt;/strong&gt;  — it shouldn’t break because you changed a compute platform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture buys you options&lt;/strong&gt;  — the freedom to pivot without panic.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By keeping the domain pure and the adapters thin, I absorbed an ECS experiment, a Serverless attempt, and an EC2 fallback.&lt;/p&gt;

&lt;p&gt;Infrastructure decisions will keep changing. Good architecture is what lets you sleep at night when they do.&lt;/p&gt;

</description>
      <category>java</category>
      <category>kafka</category>
      <category>architecture</category>
      <category>aws</category>
    </item>
  </channel>
</rss>
