<?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: William Nogueira</title>
    <description>The latest articles on DEV Community by William Nogueira (@williamnogueira).</description>
    <link>https://dev.to/williamnogueira</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1106696%2F84eeb5c1-55bf-4ca5-a957-9a1488950bcb.jpg</url>
      <title>DEV Community: William Nogueira</title>
      <link>https://dev.to/williamnogueira</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/williamnogueira"/>
    <language>en</language>
    <item>
      <title>Distributed Transactions in Java: Why I Skipped the Outbox Pattern</title>
      <dc:creator>William Nogueira</dc:creator>
      <pubDate>Mon, 06 Jul 2026 20:39:13 +0000</pubDate>
      <link>https://dev.to/williamnogueira/distributed-transactions-in-java-why-i-skipped-the-outbox-pattern-1n52</link>
      <guid>https://dev.to/williamnogueira/distributed-transactions-in-java-why-i-skipped-the-outbox-pattern-1n52</guid>
      <description>&lt;p&gt;Picture a payment flowing through a system: authorize the funds, capture the payment, post it to the ledger. Three steps, three services, and no way to wrap them in a single &lt;strong&gt;@Transactional&lt;/strong&gt;, each service has its own database, and the network sits between them.&lt;/p&gt;

&lt;p&gt;Now the interesting part: the authorization succeeds, and the capture &lt;strong&gt;fails&lt;/strong&gt;. You've reserved money on a customer's card for a payment that will never happen. Nothing crashed, no exception went unhandled, every service did its job. The &lt;em&gt;transaction&lt;/em&gt; is what broke.&lt;/p&gt;

&lt;p&gt;This is the problem the &lt;strong&gt;Saga pattern&lt;/strong&gt; exists for, and I built an &lt;a href="https://github.com/William-Nogueira/spring-saga-orchestrator" rel="noopener noreferrer"&gt;orchestration-based saga coordinator&lt;/a&gt; in Spring Boot to explore it properly, compensation, retries, crash recovery, the works. In this article I'll focus on the most controversial decision I made along the way: &lt;strong&gt;I didn't use the Transactional Outbox pattern&lt;/strong&gt;, the thing every microservices reference architecture tells you to use. I'll show you why, and when you still should.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Saga Pattern in 60 Seconds
&lt;/h2&gt;

&lt;p&gt;A saga replaces one impossible distributed transaction with a sequence of local ones. Each step has a &lt;strong&gt;compensation&lt;/strong&gt;: an action that semantically undoes it. If step 3 fails, you run the compensations of steps 2 and 1, in reverse, and the system ends up consistent again.&lt;/p&gt;

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

&lt;p&gt;I chose &lt;strong&gt;orchestration&lt;/strong&gt; over choreography: a central engine owns the state machine, so "where is my payment?" has one answer and one place to attach timeouts, retries, and manual intervention. The orchestrator talks to participants exclusively over RabbitMQ command and reply queues: never in-process, because lost replies and duplicate deliveries are the whole point of the exercise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Textbook Problem: The Dual Write
&lt;/h2&gt;

&lt;p&gt;Here's the trap every saga implementation has to deal with. When the orchestrator advances a step, it must do two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Write&lt;/strong&gt; the new state to its database ("step 2 is now RUNNING")&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publish&lt;/strong&gt; the command to the broker ("capture the payment")&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are two different systems, so you can't commit them atomically. Publish first and the DB write can fail: you've commanded a capture you have no record of. Commit first and the process can crash before publishing: your database says a command is in flight that never left the building.&lt;/p&gt;

&lt;p&gt;The textbook answer is the &lt;strong&gt;Transactional Outbox&lt;/strong&gt;: write the outgoing message into an &lt;strong&gt;outbox&lt;/strong&gt; table &lt;em&gt;in the same DB transaction&lt;/em&gt; as the state change, and let a relay process publish from that table afterwards. It works. It's also a second table, a polling relay, ordering concerns, cleanup jobs, and one more moving part that can fail in its own creative ways.&lt;/p&gt;

&lt;p&gt;Before adding all that, I stopped and asked what the outbox actually &lt;em&gt;buys&lt;/em&gt; here.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Realization: My Saga Table Was Already an Outbox
&lt;/h2&gt;

&lt;p&gt;Look at what the orchestrator persists when it dispatches a step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;markDispatched&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Instant&lt;/span&gt; &lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;transitionTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StepStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RUNNING&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;attempts&lt;/span&gt;&lt;span class="o"&gt;++;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispatchedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;when&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A step in &lt;strong&gt;RUNNING&lt;/strong&gt; with a &lt;strong&gt;dispatched_at&lt;/strong&gt; timestamp and no reply &lt;strong&gt;is&lt;/strong&gt; the record of a command that must be in flight. That's exactly the information an outbox row carries and I was about to persist it twice.&lt;/p&gt;

&lt;p&gt;So instead, commands leave &lt;em&gt;after&lt;/em&gt; the state is durable, using a post-commit hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;dispatchAfterCommit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StepCommand&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;TransactionUtils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;executeAfterCommit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;commandDispatcher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;dispatch&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
            &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dispatch of step '{}' for saga {} failed; "&lt;/span&gt;
                    &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"the armed timeout will fail the step"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stepName&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sagaId&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;

    &lt;span class="c1"&gt;// armed even if the dispatch above fails: a command that never left is&lt;/span&gt;
    &lt;span class="c1"&gt;// indistinguishable from a reply that never came back, and both end here&lt;/span&gt;
    &lt;span class="nc"&gt;TransactionUtils&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;executeAfterCommit&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;stepTimeoutScheduler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;scheduleTimeoutCheck&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sagaId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stepId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That comment is the entire architecture in one sentence. If the process crashes between commit and dispatch, the exact window the outbox exists for, the command is simply &lt;em&gt;gone&lt;/em&gt;. And from the orchestrator's perspective, a command that never left looks identical to a participant that never answered. I already needed timeout handling for dead participants. Why build a second recovery mechanism for a failure that's indistinguishable from the first?&lt;/p&gt;

&lt;h2&gt;
  
  
  Timeouts Are the Relay
&lt;/h2&gt;

&lt;p&gt;Every dispatched command arms a timeout. If no reply lands in time, the step is retried with linear backoff, and only silence retries. An explicit business failure ("card declined") goes straight to compensation, because retrying a &lt;em&gt;decision&lt;/em&gt; is pointless:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStatus&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;StepStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RUNNING&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAttempts&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;sagaProperties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stepMaxAttempts&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;retryDispatch&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;saga&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getName&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;transitionTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StepStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;FAILED&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;beginCompensation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;saga&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the retries are &lt;strong&gt;attempt-guarded&lt;/strong&gt;: each timeout check carries the attempt number it was armed for, so a stale timer from attempt 1 can't kill a step that's already on attempt 2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crash Recovery: One Sweep, Same Path
&lt;/h2&gt;

&lt;p&gt;The timers live in memory, so a restart loses them. That's fine, they were never the source of truth. A recovery sweeper runs at startup and on a schedule, re-deriving every deadline from persisted state and routing stragglers through the &lt;em&gt;same&lt;/em&gt; timeout handler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Scheduled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fixedDelayString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"${saga.sweep-interval}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;sweep&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clock&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;instant&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;inFlight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sagaStepRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByStatusIn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;StepStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RUNNING&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;StepStatus&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;COMPENSATING&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inFlight&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDispatchedAt&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;plus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sagaProperties&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stepTimeout&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;multipliedBy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAttempts&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isAfter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;sagaOrchestrator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;onStepTimeout&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSaga&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAttempts&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One recovery story, two triggers. And because the handler is state-guarded and attempt-guarded, and the saga row carries an optimistic lock, the sweep is safe to run concurrently, five pods running it against one database would just be four no-ops and a winner.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency Without a Dedupe Table
&lt;/h2&gt;

&lt;p&gt;The same thinking killed the message-deduplication table. RabbitMQ redelivers, participants retry, timers race replies, but a reply can only act on a step that's in exactly the right state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getStatus&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="no"&gt;RUNNING&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;onStepReply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;saga&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="no"&gt;COMPENSATING&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;onCompensationReply&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;saga&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;warn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ignoring duplicate or late reply for saga {} step {}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sagaId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;reply&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stepId&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A duplicate success reply finds the step already &lt;strong&gt;COMPLETED&lt;/strong&gt; and falls into the default arm. A reply that arrives after the timeout already failed the step? Same. Two threads processing the same reply concurrently? One commits, the other gets an &lt;strong&gt;OptimisticLockException&lt;/strong&gt; and rolls back. The state machine &lt;em&gt;is&lt;/em&gt; the dedupe.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trade-off: When You Should Still Use an Outbox
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What I gave up:&lt;/strong&gt; recovery latency. An outbox relay republishes a lost message within seconds. My design waits out the step timeout first: bounded and tunable, but slower. For a payment saga where the participant itself might take seconds to respond, that's acceptable. For a system where a lost message must be recovered near-instantly, it isn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where the outbox genuinely wins:&lt;/strong&gt; when you're publishing &lt;em&gt;events&lt;/em&gt; rather than commands: fire-and-forget notifications where no reply will ever come back to tell you something was lost. No reply channel means no timeout, and no timeout means my whole recovery story evaporates. If I ever split the participants into real services that publish their own domain events, &lt;em&gt;they&lt;/em&gt; will get outboxes.&lt;/p&gt;

&lt;p&gt;The pattern isn't wrong. I just didn't have the problem it solves: my state machine already solved it as a side effect of existing.&lt;/p&gt;

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

&lt;p&gt;The saga engine ended up with exactly one recovery mechanism: persist intent first, dispatch after commit, and let timeouts plus a sweeper re-drive anything the real world loses. Fewer moving parts, one code path to test: and the test suite hammers it with duplicate replies, dead participants, and simulated crashes against real Postgres and RabbitMQ containers.&lt;/p&gt;

&lt;p&gt;Patterns are answers to specific problems. Before you add one, check whether your system already answers the question: sometimes the table you have is the table the pattern would have given you.&lt;/p&gt;

&lt;p&gt;The full implementation, compensation, manual intervention endpoints, metrics, the whole state machine is on &lt;a href="https://github.com/William-Nogueira/spring-saga-orchestrator" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>Scaling Java with Write-Behind Caching</title>
      <dc:creator>William Nogueira</dc:creator>
      <pubDate>Wed, 24 Dec 2025 03:02:17 +0000</pubDate>
      <link>https://dev.to/williamnogueira/scaling-java-with-write-behind-caching-fnd</link>
      <guid>https://dev.to/williamnogueira/scaling-java-with-write-behind-caching-fnd</guid>
      <description>&lt;p&gt;If you ask a Junior Developer to build a URL Shortener (like Bit.ly) that tracks click analytics, the logic usually looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; User clicks the short link (&lt;code&gt;/abc&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt; App fetches the original URL from the database.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;App increments the click counter in the database (&lt;code&gt;UPDATE urls SET clicks = clicks + 1 ...&lt;/code&gt;).&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt; App redirects the user.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This works great for 100 users. It works okay for 1,000 users.&lt;br&gt;
But at &lt;strong&gt;10,000 requests per second (RPS)&lt;/strong&gt;? Your database locks up, latency spikes, and your application crashes.&lt;/p&gt;

&lt;p&gt;Why? Because &lt;strong&gt;Database I/O is expensive&lt;/strong&gt;. Blocking a thread to write a "+1" to disk for every single click is a massive waste of resources.&lt;/p&gt;

&lt;p&gt;In this article, I’ll show you how I solved this using the &lt;strong&gt;Write-Behind Caching&lt;/strong&gt; pattern, &lt;strong&gt;Redis&lt;/strong&gt;, and &lt;strong&gt;Java 25 Virtual Threads&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Bottleneck: Synchronous Writes
&lt;/h2&gt;

&lt;p&gt;In the naive approach, the "Read" (finding the URL) and the "Write" (tracking the click) are coupled.&lt;/p&gt;

&lt;p&gt;If your database takes 50ms to perform the update, your maximum throughput per thread is capped. If traffic spikes, your connection pool exhausts, and requests start timing out.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Solution: Write-Behind Caching
&lt;/h2&gt;

&lt;p&gt;To achieve high scale, we need to &lt;strong&gt;decouple&lt;/strong&gt; the analytics from the redirect. The user wants to go to Google.com; they don't care if we counted their click &lt;em&gt;right now&lt;/em&gt; or 5 minutes from now.&lt;/p&gt;

&lt;p&gt;Here is the architecture I implemented in my project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Fast Path:&lt;/strong&gt; Increment the counter in &lt;strong&gt;Redis&lt;/strong&gt; (Memory is fast!).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Dirty Tracking:&lt;/strong&gt; Add the Short Code to a "Dirty Set" in Redis so we know which records changed.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Slow Path (Async):&lt;/strong&gt; A background scheduler wakes up, reads the Dirty Set, and flushes the counts to the Database (DynamoDB/Postgres) in &lt;strong&gt;batches&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's look at the code.&lt;/p&gt;


&lt;h3&gt;
  
  
  Step 1: The "Fire and Forget" Increment
&lt;/h3&gt;

&lt;p&gt;I used &lt;strong&gt;Spring Boot 4&lt;/strong&gt; and &lt;strong&gt;RedisTemplate&lt;/strong&gt;. When a request comes in, we hit Redis. This operation takes sub-milliseconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Async&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"clickExecutor"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;incrementClickCount&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Atomic Increment in Redis (Super fast)&lt;/span&gt;
    &lt;span class="n"&gt;redisTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;opsForValue&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;increment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"clicks:"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. Mark this code as "Dirty" so the scheduler knows it changed&lt;/span&gt;
    &lt;span class="n"&gt;redisTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;opsForSet&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dirty_urls"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;@Async&lt;/code&gt;. The controller returns the redirect immediately. This logic ensures the user experiences zero latency penalty for our analytics tracking:&lt;br&gt;
&lt;/p&gt;

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

    &lt;span class="nd"&gt;@Bean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"clickExecutor"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Executor&lt;/span&gt; &lt;span class="nf"&gt;clickExecutor&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TaskExecutorAdapter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Executors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newVirtualThreadPerTaskExecutor&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: The Virtual Thread Scheduler
&lt;/h3&gt;

&lt;p&gt;This is where &lt;strong&gt;Java 25&lt;/strong&gt; shines.&lt;/p&gt;

&lt;p&gt;Historically, background tasks that talk to databases were heavy because they blocked OS threads. With &lt;strong&gt;Project Loom (Virtual Threads)&lt;/strong&gt;, we can spawn threads that are lightweight. If the database is slow, the Virtual Thread "parks" itself, freeing up the carrier thread to do other work.&lt;/p&gt;

&lt;p&gt;Here is the scheduler that runs every 5 minutes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Scheduled&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cron&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0 */5 * * * *"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Runs every 5 mins&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;persistClicks&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Starting sync job..."&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 1. Pop a batch of codes (e.g., 1000 at a time) from the Set&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;codes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redisTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;opsForSet&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;pop&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dirty_urls"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Objects&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isNull&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;codes&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;codes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// 2. Process in parallel using Virtual Threads&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Executors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newVirtualThreadPerTaskExecutor&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;codes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;forEach&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;submit&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;syncToDb&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;())));&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: The Atomic Sync
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;syncToDb&lt;/code&gt; method is responsible for moving the data from Cache to Storage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;syncToDb&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Get the count from Redis and RESET it to 0 atomically&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;clickCountObj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redisTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;opsForValue&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getAndSet&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"clicks:"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clickCountObj&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;clicks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parseLong&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clickCountObj&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clicks&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// 2. Update the Database (in this case, DynamoDB)&lt;/span&gt;
        &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateClickCount&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clicks&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;updateClickCount&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;totalClicks&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;dynamoDbClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateItem&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tableName&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"urls"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"code"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;AttributeValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;s&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;()))&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;updateExpression&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ADD clicks :inc"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;expressionAttributeValues&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                    &lt;span class="s"&gt;":inc"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                    &lt;span class="nc"&gt;AttributeValue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;n&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;valueOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;totalClicks&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;))&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Trade-off: What about Data Loss?
&lt;/h3&gt;

&lt;p&gt;In distributed systems, everything is a trade-off. By using Write-Behind, we gain massive performance, but we introduce a risk: &lt;strong&gt;Durability&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If the Redis instance crashes completely &lt;em&gt;before&lt;/em&gt; the 5-minute sync triggers, we lose the clicks that happened in that window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why is this acceptable?&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Business Value:&lt;/strong&gt; Losing 5 minutes of click analytics is usually acceptable. Losing the &lt;em&gt;actual URL mappings&lt;/em&gt; (Create operation) is not. (Creation is synchronous; Analytics is asynchronous).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Mitigation:&lt;/strong&gt; We can enable Redis AOF (Append Only File) persistence to minimize data loss on crash.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;By implementing this pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Latency:&lt;/strong&gt; The &lt;code&gt;GET /{code}&lt;/code&gt; endpoint response time dropped from &lt;strong&gt;~60ms&lt;/strong&gt; (DB write) to &lt;strong&gt;~5ms&lt;/strong&gt; (Redis write).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Throughput:&lt;/strong&gt; The database is no longer hammered by every single click. It only receives bulk updates every few minutes.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Cost:&lt;/strong&gt; We reduced Database Write Units (WCUs), which is the most expensive part of cloud databases like DynamoDB.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;You don't always need microservices to scale. Sometimes, you just need to stop treating your database like a notepad. Patterns like &lt;strong&gt;Write-Behind Caching&lt;/strong&gt;, combined with modern tools like &lt;strong&gt;Virtual Threads&lt;/strong&gt;, allow monoliths to handle massive loads efficiently.&lt;/p&gt;

&lt;p&gt;You can check out the full implementation (including the Docker Compose setup and Testcontainers integration) in the repository below:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/William-Nogueira/spring-url-shortener" rel="noopener noreferrer"&gt;GitHub Repo: Distributed URL Shortener&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>performance</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Comparing Database Management Systems: MySQL vs PostgreSQL</title>
      <dc:creator>William Nogueira</dc:creator>
      <pubDate>Sat, 02 Sep 2023 19:08:04 +0000</pubDate>
      <link>https://dev.to/williamnogueira/comparing-rdbms-mysql-vs-postgresql-5515</link>
      <guid>https://dev.to/williamnogueira/comparing-rdbms-mysql-vs-postgresql-5515</guid>
      <description>&lt;p&gt;If you're a beginner looking to dive into the world of relational database management systems (RDBMS), you may find yourself faced with the question of which one to learn first. Two popular options in the RDBMS landscape are MySQL and PostgreSQL. In this article, we will explore the key differences between these two systems, their areas of application, and help you decide which one might be the best fit for your needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview of MySQL and PostgreSQL
&lt;/h2&gt;

&lt;p&gt;MySQL and PostgreSQL are both open-source RDBMS that have been widely adopted by developers and organizations around the world. While they share some similarities, they also have distinct characteristics that set them apart. Let's take a closer look at each of these database systems.&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%2Fbgxshwy2np0unci2cguj.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%2Fbgxshwy2np0unci2cguj.png" alt="MySQL and PostgreSQL" width="800" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  MySQL
&lt;/h3&gt;

&lt;p&gt;MySQL has a long history and is known for its simplicity and ease of use. It was initially developed by Michael Widenius and David Axmark in 1994 and has since become one of the most popular RDBMS options available. MySQL is particularly favored in web development, thanks to its seamless integration with popular web technologies like PHP. It offers a wide range of features, including support for multiple storage engines, replication, and high availability.&lt;/p&gt;

&lt;h3&gt;
  
  
  PostgreSQL
&lt;/h3&gt;

&lt;p&gt;PostgreSQL, often referred to as Postgres, is a powerful and feature-rich RDBMS that has gained a strong following in the developer community. Initially developed at the University of California, Berkeley, in 1985, PostgreSQL has evolved into a robust and highly extensible database system. It boasts advanced features such as support for complex data types, JSON, and full-text search. PostgreSQL is known for its adherence to SQL standards and its ability to handle complex queries and large amounts of data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Differences between MySQL and PostgreSQL
&lt;/h2&gt;

&lt;p&gt;While MySQL and PostgreSQL are both excellent choices for managing relational databases, they have some fundamental differences that may influence your decision. Let's examine these differences in more detail.&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%2Fu2vucisittrgg5q3t7zf.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%2Fu2vucisittrgg5q3t7zf.png" alt="MySQL vs PostgreSQL" width="600" height="425"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Types and Flexibility
&lt;/h3&gt;

&lt;p&gt;One significant distinction between MySQL and PostgreSQL lies in their approach to data types. MySQL is a purely relational database, meaning it primarily deals with structured data. On the other hand, PostgreSQL is an object-relational database, which allows for the storage and manipulation of more complex data structures. PostgreSQL supports features such as arrays, hstore, and even user-defined data types, making it a more versatile choice for developers working with diverse data formats.&lt;/p&gt;

&lt;h3&gt;
  
  
  ACID Compliance
&lt;/h3&gt;

&lt;p&gt;ACID compliance is crucial for ensuring data integrity and consistency in a database. MySQL offers ACID compliance when used with certain storage engines like InnoDB and NDB Cluster. In contrast, PostgreSQL is fully ACID compliant in all configurations. This means that PostgreSQL guarantees transactional integrity, even in complex scenarios, making it a reliable choice for applications that require strict data consistency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Concurrency Control
&lt;/h3&gt;

&lt;p&gt;Concurrency control is essential for handling multiple simultaneous database operations. MySQL uses a locking mechanism for concurrency control, which can sometimes result in performance bottlenecks when dealing with high levels of concurrent transactions. PostgreSQL, on the other hand, employs a more advanced approach called Multiversion Concurrency Control (MVCC). MVCC allows for better concurrency by creating duplicate copies of records, enabling multiple users to read and update the same data simultaneously without conflicts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Indexing Options
&lt;/h3&gt;

&lt;p&gt;Indexes play a crucial role in optimizing database performance by enabling faster data retrieval. MySQL supports B-tree and R-tree indexes, which are suitable for hierarchical and spatial data, respectively. PostgreSQL offers a wider range of indexing options, including B-tree, expression indexes, partial indexes, and hash indexes. This flexibility allows developers to fine-tune their database performance based on specific requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  Views and Materialized Views
&lt;/h3&gt;

&lt;p&gt;Views are virtual tables derived from the result of a query. They provide a convenient way to organize and access data from multiple tables. While both MySQL and PostgreSQL support views, PostgreSQL takes it a step further with materialized views. Materialized views are precomputed views that store the results of a query as a physical table. This can significantly improve query performance, especially when dealing with complex calculations or aggregations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stored Procedures and Triggers
&lt;/h3&gt;

&lt;p&gt;Stored procedures and triggers are essential tools for implementing complex business logic within a database. Both MySQL and PostgreSQL support stored procedures, which are reusable blocks of code that can be called from within the database. However, PostgreSQL has an advantage in that it allows stored procedures to be written in languages other than SQL, providing greater flexibility and enabling developers to leverage their preferred programming languages.&lt;/p&gt;

&lt;p&gt;Triggers, on the other hand, are database actions that automatically execute in response to specific events. MySQL supports triggers for SQL INSERT, UPDATE, and DELETE statements. PostgreSQL goes a step further by offering INSTEAD OF triggers, which allow for more complex SQL statements using functions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing the Right RDBMS for Your Needs
&lt;/h2&gt;

&lt;p&gt;Now that we've explored the key differences between MySQL and PostgreSQL, you may be wondering which one is the best choice for you as a beginner. The answer ultimately depends on your specific requirements and the nature of the projects you'll be working on.&lt;/p&gt;

&lt;p&gt;If you're looking for a lightweight and easy-to-use database system with seamless integration into web development frameworks, MySQL may be the better option. Its simplicity and wide adoption make it an excellent choice for beginners and small to medium-sized projects.&lt;/p&gt;

&lt;p&gt;On the other hand, if you're working on more complex applications that require advanced features, support for diverse data types, and robust transactional capabilities, PostgreSQL may be the preferred choice. Its adherence to SQL standards and extensive feature set make it well-suited for enterprise-level applications and developers seeking maximum flexibility and control.&lt;/p&gt;

</description>
      <category>database</category>
      <category>mysql</category>
      <category>postgres</category>
      <category>beginners</category>
    </item>
    <item>
      <title>The Problem of Bloat in Web Development: Do We Really Need Front-End Frameworks?</title>
      <dc:creator>William Nogueira</dc:creator>
      <pubDate>Fri, 23 Jun 2023 07:26:54 +0000</pubDate>
      <link>https://dev.to/williamnogueira/the-problem-of-bloat-in-web-development-do-we-really-need-front-end-frameworks-14ma</link>
      <guid>https://dev.to/williamnogueira/the-problem-of-bloat-in-web-development-do-we-really-need-front-end-frameworks-14ma</guid>
      <description>&lt;p&gt;In recent years, the web development industry has witnessed a surge in the use of front-end frameworks for building web applications. These frameworks, such as Angular, React, and Vue.js, offer developers a plethora of tools and functionalities to streamline the development process. However, with the increasing popularity of these frameworks, concerns have been raised about the issue of bloat in web development. This article aims to explore the problem of bloat and question the necessity of front-end frameworks. Is it possible to build efficient and user-friendly web applications without relying on these frameworks? Let's delve deeper and find out.&lt;/p&gt;




&lt;h2&gt;
  
  
  Understanding Bloat in Web Development
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Growing Size of Web Pages
&lt;/h3&gt;

&lt;p&gt;Over the years, web pages have become increasingly bloated in terms of size. This bloat can be attributed to various factors, including the excessive use of JavaScript libraries, heavy media content, and overcomplicated design elements. To put things into perspective, let's take a look at the average size of web pages over the past decade:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;March 2011: Average size 735kB&lt;/li&gt;
&lt;li&gt;March 2012: Average size 1008kB&lt;/li&gt;
&lt;li&gt;March 2013: Average size 1335kB&lt;/li&gt;
&lt;li&gt;March 2014: Average size 1728kB&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%2F8bgw2ceylfvyjc9rlr5p.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%2F8bgw2ceylfvyjc9rlr5p.png" alt="Page Bloat" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As we can see, there has been a significant increase in the average size of web pages, indicating a growing problem of bloat. This bloat not only affects the loading speed of websites but also leads to a poor user experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Role of Front-End Frameworks in Bloat
&lt;/h3&gt;

&lt;p&gt;Front-end frameworks, while offering numerous advantages in terms of development speed and code organization, can contribute to the problem of bloat in web development. These frameworks often come bundled with a plethora of features and functionalities, many of which may not be necessary for every project. This results in unnecessary code bloat, leading to larger file sizes and slower loading times.&lt;br&gt;
Additionally, front-end frameworks often rely heavily on JavaScript, which can further contribute to the bloat. JavaScript files can be large and resource-intensive, especially when multiple libraries and dependencies are included. This not only affects the performance of the web application but also increases the load on the user's device, particularly on mobile devices with limited resources.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Case for Static Pages
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Simplicity and Efficiency of Static Pages
&lt;/h3&gt;

&lt;p&gt;Static pages, built using basic HTML and CSS, offer a simpler and more efficient approach to web development. By eliminating the need for complex JavaScript frameworks, static pages can significantly reduce the bloat and improve the overall performance of a website. With static pages, the focus is on delivering content quickly and efficiently, without unnecessary bells and whistles.&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%2F5u5easj7tvz3wunlv4js.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%2F5u5easj7tvz3wunlv4js.png" alt="Static Website" width="800" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Advantages of Using Basic HTML and CSS
&lt;/h3&gt;

&lt;p&gt;Building web pages with basic HTML and CSS has several advantages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lightweight and Fast:&lt;/strong&gt; Static pages are lightweight and load quickly, providing a seamless user experience. Without the overhead of complex JavaScript frameworks, the page size is significantly reduced, resulting in faster load times.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Better SEO:&lt;/strong&gt; Static pages are highly optimized for search engine indexing. The simplicity of static pages allows search engines to crawl and index the content more efficiently, leading to better search engine rankings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Improved Accessibility:&lt;/strong&gt; Basic HTML and CSS ensure compatibility across different devices and browsers, making the website accessible to a wider audience. This is particularly important for users with slower internet connections or older devices. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Easier Maintenance:&lt;/strong&gt; With static pages, there is less code complexity, making maintenance and updates easier. Changes can be made quickly without the need to navigate through a complex framework structure.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Role of Front-End Frameworks in Modern Web Development
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Benefits of Front-End Frameworks
&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%2Fojkxgl2s56zlj2mrmnhe.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%2Fojkxgl2s56zlj2mrmnhe.png" alt="Angular, Vue, React" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Despite the concerns surrounding bloat, front-end frameworks offer several benefits that make them a popular choice among web developers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rapid Development:&lt;/strong&gt; Front-end frameworks provide pre-built components, libraries, and tools that streamline the development process. This allows developers to build complex web applications more efficiently and with fewer lines of code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Code Organization:&lt;/strong&gt; Frameworks promote modular code structures, making it easier to manage and scale large projects. Components can be reused across different parts of the application, improving code maintainability.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Developer Community:&lt;/strong&gt; Front-end frameworks have vibrant and active developer communities, providing access to extensive documentation, tutorials, and support. This can be invaluable for developers seeking assistance or looking to learn new techniques.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Rich User Interfaces:&lt;/strong&gt; Front-end frameworks offer a range of UI components and styling options, enabling developers to create visually appealing and interactive user interfaces. This can enhance the overall user experience and engagement.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Assessing the Need for Front-End Frameworks
&lt;/h3&gt;

&lt;p&gt;While front-end frameworks offer numerous advantages, it is essential to assess the specific needs of a project before deciding to incorporate them. Consider the following factors:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Project Complexity:&lt;/strong&gt; If the project involves complex user interactions, real-time updates, or extensive data manipulation, a front-end framework may be beneficial. Frameworks provide the necessary tools and abstractions to handle these complexities efficiently.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Development Speed:&lt;/strong&gt; Front-end frameworks excel in rapid development scenarios, where time-to-market is crucial. If the project requires quick iteration and deployment, a framework can expedite the development process.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Resource Constraints:&lt;/strong&gt; Consider the target audience and their devices. If the majority of users have limited resources, such as slow internet connections or older devices, opting for a lightweight approach without a framework might be more appropriate.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Long-Term Maintenance:&lt;/strong&gt; Evaluate the long-term maintenance and scalability requirements of the project. Front-end frameworks can facilitate code organization and scalability, making it easier to maintain and extend the application over time.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




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

&lt;p&gt;In conclusion, the problem of bloat in web development is a growing concern, as web pages continue to increase in size and complexity. While front-end frameworks offer numerous benefits in terms of development speed and code organization, they can contribute to the bloat problem. However, static pages built with basic HTML and CSS provide a lightweight and efficient alternative, particularly for simpler projects or those with resource constraints.&lt;/p&gt;

&lt;p&gt;The decision to use a front-end framework should be based on a careful assessment of the project's needs, considering factors such as complexity, development speed, resource constraints, and long-term maintenance. By weighing the advantages and drawbacks of front-end frameworks, developers can make informed decisions and strike a balance between functionality and performance in their web applications.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>react</category>
      <category>frontend</category>
    </item>
  </channel>
</rss>
