<?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: Rohit Babu Gaddeti</title>
    <description>The latest articles on DEV Community by Rohit Babu Gaddeti (@rohitbabugaddeti).</description>
    <link>https://dev.to/rohitbabugaddeti</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%2F390294%2Fb458d46c-b10a-4bba-a992-35779493ec23.jpeg</url>
      <title>DEV Community: Rohit Babu Gaddeti</title>
      <link>https://dev.to/rohitbabugaddeti</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rohitbabugaddeti"/>
    <language>en</language>
    <item>
      <title>The work and its checkpoint can never be atomic</title>
      <dc:creator>Rohit Babu Gaddeti</dc:creator>
      <pubDate>Tue, 27 Oct 2026 15:15:49 +0000</pubDate>
      <link>https://dev.to/rohitbabugaddeti/the-work-and-its-checkpoint-can-never-be-atomic-1o3</link>
      <guid>https://dev.to/rohitbabugaddeti/the-work-and-its-checkpoint-can-never-be-atomic-1o3</guid>
      <description>&lt;h2&gt;
  
  
  Description
&lt;/h2&gt;

&lt;p&gt;When one service does the work and another records it, some work runs twice. Idempotency is a correctness requirement.&lt;/p&gt;

&lt;p&gt;This post covers two writes that look like one step: doing some work, and recording that it was done.&lt;/p&gt;

&lt;p&gt;In this design the work runs in one service. Another service writes the record. No transaction spans both. So some work will run twice. The shape of the design guarantees it, and high load is not required. That makes idempotency a correctness requirement. If a stage cannot be made idempotent, the architecture has to change. Tighter timeouts will not fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;The platform is an enterprise quoting system. It runs as Spring Boot services on EKS, and around 30 of them share one self-managed Oracle 12c instance.&lt;/p&gt;

&lt;p&gt;Each in-flight workflow has one row in a &lt;code&gt;PENDING_WORK&lt;/code&gt; table. The row is keyed by &lt;code&gt;GROUP_ID&lt;/code&gt; and &lt;code&gt;QUOTE_ID&lt;/code&gt; and carries a &lt;code&gt;STAGE&lt;/code&gt; and a &lt;code&gt;STAGE_STATE&lt;/code&gt;. A scheduler runs on every pod, and ShedLock makes it a singleton. It orchestrates and holds no business logic. Each stage is an HTTP endpoint on a core service. The orchestrator calls the endpoint, waits for success, then writes the next stage. Both services can update the row, but only the core service writes the business tables. Retry is bounded, after which the row is marked non-resumable. One user submission fans out into roughly 120 external API calls and database writes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two commits, one gap
&lt;/h2&gt;

&lt;p&gt;The core service commits the work. Later, the orchestrator commits the checkpoint. These are two commits in two processes, and every ordering leaves a gap. Work then record risks duplication. Record then work risks silent loss, because the row says done and nothing will retry it. There is no third ordering. This design does the work first, which is the better failure.&lt;/p&gt;

&lt;p&gt;Here is the window:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The orchestrator reads a row at &lt;code&gt;PRICING&lt;/code&gt; / &lt;code&gt;PENDING&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It sends &lt;code&gt;POST /stages/pricing&lt;/code&gt; to the core service.&lt;/li&gt;
&lt;li&gt;The core service does the work and commits.&lt;/li&gt;
&lt;li&gt;The core service returns &lt;code&gt;200 OK&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The orchestrator pod dies before it updates the row. A node drain or an OOM kill is enough.&lt;/li&gt;
&lt;li&gt;The ShedLock lease expires. A scheduler on another pod acquires it.&lt;/li&gt;
&lt;li&gt;That scheduler reads the row. It still says &lt;code&gt;PRICING&lt;/code&gt; / &lt;code&gt;PENDING&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It sends the same request. The work runs twice.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Step 5 can also be a read timeout after step 3. The orchestrator sees a failure and retries. The team built that retry as the recovery path. It is also what produces the duplicate. The team also spent real effort sizing executors and queues against the connection pool. That work controls load. It has no effect on whether a stage runs twice. Shorter timeouts shrink the window. Nothing closes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not two-phase commit
&lt;/h2&gt;

&lt;p&gt;XA works. Oracle supports it, and Spring can drive it through a JTA transaction manager such as Narayana. The price is extra round trips and forced log writes on every transaction, plus a coordinator that is a failure domain of its own. If the coordinator dies after prepare, participants hold in-doubt locks until someone resolves them. On a database shared by 30 services, those locks block other teams. Most teams decline XA for these reasons, and they are right to.&lt;/p&gt;

&lt;p&gt;This system has a more basic problem. A stage is an HTTP call, and HTTP has no prepare phase. The core service cannot join a transaction owned by the orchestrator. The third-party APIs behind it cannot either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exactly-once is not on offer
&lt;/h2&gt;

&lt;p&gt;At-most-once means possible loss. At-least-once means possible duplication. Exactly-once across a network boundary does not exist, because a sender cannot tell a lost request from a lost reply. Kafka's exactly-once guarantee holds inside Kafka and ends at the first external side effect. What a system can build is effectively-once: at-least-once delivery plus a receiver that makes repeats harmless.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Semantics&lt;/th&gt;
&lt;th&gt;Guarantee&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Choose it when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;At-most-once&lt;/td&gt;
&lt;td&gt;No duplicates. Loss is possible.&lt;/td&gt;
&lt;td&gt;Almost none. No retry, no durable state.&lt;/td&gt;
&lt;td&gt;Occasional loss is acceptable, as with metrics.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;At-least-once&lt;/td&gt;
&lt;td&gt;No loss. Duplicates are possible.&lt;/td&gt;
&lt;td&gt;Retries and a durable record of pending work.&lt;/td&gt;
&lt;td&gt;The receiver is already idempotent.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effectively-once&lt;/td&gt;
&lt;td&gt;Each effect lands once.&lt;/td&gt;
&lt;td&gt;At-least-once plus a dedup store or idempotent writes, with retention and cleanup.&lt;/td&gt;
&lt;td&gt;A duplicate costs money or breaks correctness.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The workflow above is at-least-once by construction. Every stage has to be effectively-once at the receiver.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making a stage idempotent
&lt;/h2&gt;

&lt;p&gt;The cheapest option is a natural key already in the data, and it is the one the team uses. Every stage endpoint is idempotent on &lt;code&gt;GROUP_ID&lt;/code&gt; and &lt;code&gt;QUOTE_ID&lt;/code&gt;, the identifiers that key the work row. That is the only reason the trace above ends safely.&lt;/p&gt;

&lt;p&gt;With a unique constraint, a second insert fails with &lt;code&gt;ORA-00001&lt;/code&gt;, which Spring raises as &lt;code&gt;DuplicateKeyException&lt;/code&gt;. The endpoint has to catch it and return the original result as a success. An error sends the orchestrator into retries until the row goes non-resumable.&lt;/p&gt;

&lt;p&gt;Endpoint idempotency covers the database writes keyed on those identifiers. External calls need more. If a stage composes several calls with &lt;code&gt;CompletableFuture&lt;/code&gt; and one fails, a retry repeats the calls that succeeded, unless the stage checks for a stored result before each one.&lt;/p&gt;

&lt;p&gt;Without a natural key, the caller can send an explicit idempotency key. It must be stable across retries, so it is derived from the row identifiers and the stage. A fresh UUID per attempt would defeat it. The receiver stores each key with its response, which costs an extra write per call, an index and a cleanup job.&lt;/p&gt;

&lt;p&gt;Some operations are idempotent by nature. A &lt;code&gt;MERGE&lt;/code&gt; to a known value is safe to repeat, and so is &lt;code&gt;SET STATUS = 'PRICED'&lt;/code&gt;. An increment such as &lt;code&gt;SET TOTAL = TOTAL + ?&lt;/code&gt; is not, and neither is an unkeyed insert into a history table. Rewriting increments as absolute sets often fixes a stage with no new infrastructure.&lt;/p&gt;

&lt;p&gt;A dedup table generalises the key store, and its hard question is retention. Keys must outlive every path by which a request can be replayed, including a manual resume of a non-resumable row weeks later. Keys kept for 24 hours protect nothing against that.&lt;/p&gt;

&lt;p&gt;The hard case is a third-party API that is not idempotent and takes no key. The caller writes an intent row before the call and an outcome after it. An intent with no outcome is unknown, and a blind retry may repeat the effect. Unknowns go to reconciliation through a status lookup or the provider's reports. A confirmed duplicate needs a compensating action, such as a cancel. At an illustrative rate of 1 ambiguous call in 10,000, 120 calls per submission leaves about 1.2% of submissions with at least one unknown.&lt;/p&gt;

&lt;h2&gt;
  
  
  The outbox, when both sides are owned
&lt;/h2&gt;

&lt;p&gt;The outbox pattern closes the gap when the team owns both sides. The service writes its business change and an event row in one local transaction. A publisher sends unpublished rows afterwards.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;OUTBOX_EVENT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;ID&lt;/span&gt;            &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="k"&gt;GENERATED&lt;/span&gt; &lt;span class="n"&gt;ALWAYS&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="k"&gt;IDENTITY&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;AGGREGATE_KEY&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;EVENT_TYPE&lt;/span&gt;    &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;PAYLOAD&lt;/span&gt;       &lt;span class="k"&gt;CLOB&lt;/span&gt;          &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;CREATED_AT&lt;/span&gt;    &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;SYSTIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;PUBLISHED_AT&lt;/span&gt;  &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;OUTBOX_PENDING_IX&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;OUTBOX_EVENT&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PUBLISHED_AT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The publisher still sends first and marks second. That is the same dual-write, moved to a place where a retry is safe.&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;fixedDelay&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="nd"&gt;@SchedulerLock&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;"outboxPublisher"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lockAtMostFor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"PT30S"&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;publishPending&lt;/span&gt;&lt;span class="o"&gt;()&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;OutboxEvent&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jdbc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"SELECT * FROM OUTBOX_EVENT WHERE PUBLISHED_AT IS NULL "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="s"&gt;"ORDER BY ID FETCH FIRST 100 ROWS ONLY"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outboxMapper&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="nc"&gt;OutboxEvent&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;send&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;aggregateKey&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;payload&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// a crash here causes a resend&lt;/span&gt;
        &lt;span class="n"&gt;jdbc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"UPDATE OUTBOX_EVENT SET PUBLISHED_AT = SYSTIMESTAMP WHERE ID = ?"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The outbox buys atomicity between a state change and its message. It costs a table, a publisher and constant polling against a database that 30 services already share. Delivery is still at-least-once, so consumers dedupe on &lt;code&gt;ID&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A narrower version fits the database-only stages. The core service already writes &lt;code&gt;PENDING_WORK&lt;/code&gt;. If it advanced &lt;code&gt;STAGE&lt;/code&gt; in the same transaction as its business writes, those stages would have no gap. That needs one datasource inside one &lt;code&gt;@Transactional&lt;/code&gt; method. Two datasources are two transactions, even on one Oracle instance. The cost is that stage transitions would live in two services, and the orchestrator would have to read an already-advanced row as success. External API stages gain nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fencing for concurrent work
&lt;/h2&gt;

&lt;p&gt;Idempotency handles repeated work. Fencing handles concurrent work, and this system needs both. ShedLock grants a lease with an expiry. If the holder stalls in a long GC pause, or a run outlives &lt;code&gt;lockAtMostFor&lt;/code&gt;, another pod takes the lock while the first is still running. The row also has two writers by design, the orchestrator and the core service.&lt;/p&gt;

&lt;p&gt;The team already has part of a fence. An &lt;code&gt;ATTEMPT_COUNT&lt;/code&gt; column is updated on every execution of a stage. The orchestrator stops triggering a stage once the count reaches a configurable threshold. That bounds how often a stage runs. It does not stop two workers running it at once. If the count is read, checked and then incremented, two workers that both read 2 can both start attempt 3.&lt;/p&gt;

&lt;p&gt;The same column becomes a fencing token once every write is conditional on it.&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="c1"&gt;// Claim the attempt. Only one worker can move the count from n to n + 1.&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;claimed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jdbc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"UPDATE PENDING_WORK SET ATTEMPT_COUNT = ATTEMPT_COUNT + 1 "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s"&gt;"WHERE GROUP_ID = ? AND QUOTE_ID = ? AND STAGE = ? AND ATTEMPT_COUNT = ?"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;groupId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quoteId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;readCount&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;claimed&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&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="c1"&gt;// another worker owns this attempt&lt;/span&gt;

&lt;span class="c1"&gt;// ...call the stage endpoint...&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jdbc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"UPDATE PENDING_WORK SET STAGE = ?, STAGE_STATE = ? "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="s"&gt;"WHERE GROUP_ID = ? AND QUOTE_ID = ? AND STAGE = ? AND ATTEMPT_COUNT = ?"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;nextStage&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nextState&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;groupId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quoteId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;readCount&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="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StaleCheckpointException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;groupId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quoteId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A stale worker's checkpoint now matches zero rows. Without the condition, it could move a row back to a stage already passed. The stale call to the endpoint still happens, and idempotency makes it harmless. The conditional claim also makes the threshold exact. The core service's writes to the row need the same condition.&lt;/p&gt;

&lt;h2&gt;
  
  
  A decision guide
&lt;/h2&gt;

&lt;p&gt;For each stage, the questions go in this order.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Does running it twice leave the same state? Call it twice with the same input and compare the rows. If it does, the stage is already idempotent.&lt;/li&gt;
&lt;li&gt;Is there a natural key, or can the orchestrator send a stable key that the receiver stores? Then idempotency is cheap. The cost is storage and a retention rule that covers manual resumes.&lt;/li&gt;
&lt;li&gt;Is the effect owned by a third party that accepts no key? Then it needs an intent record, reconciliation and compensation. It also belongs in its own stage, so retries of other work do not repeat it.&lt;/li&gt;
&lt;li&gt;If none of these hold and a duplicate is unacceptable, the architecture has to change. Timeouts and retry counts are the wrong tools.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Isn't this what Temporal and Step Functions are for?
&lt;/h2&gt;

&lt;p&gt;Yes. Both solve durable execution properly. Either one would replace the work table, the ShedLock scheduler and the hand-written retry logic.&lt;/p&gt;

&lt;p&gt;Neither removes the need for idempotency. A Temporal activity or a Step Functions task is still a call that can succeed and lose its reply. Temporal's own documentation recommends idempotent activities. The dual-write moves to the activity boundary and stays there.&lt;/p&gt;

&lt;p&gt;The counterweights are specific. Step Functions Standard workflows bill per state transition, and a 120-way fan-out multiplies the transitions in every submission. Workflow state lives in the engine's store, outside Oracle, so it is not transactional with business data. That is two systems without a shared transaction, again. A new platform also brings new on-call knowledge to a team that already runs its own database. With either tool in place, the team would still be writing idempotent stages.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>Threads Are Not Connections: A Spring Boot Concurrency Incident</title>
      <dc:creator>Rohit Babu Gaddeti</dc:creator>
      <pubDate>Fri, 16 Oct 2026 10:17:40 +0000</pubDate>
      <link>https://dev.to/rohitbabugaddeti/threads-are-not-connections-a-spring-boot-concurrency-incident-27lb</link>
      <guid>https://dev.to/rohitbabugaddeti/threads-are-not-connections-a-spring-boot-concurrency-incident-27lb</guid>
      <description>&lt;h2&gt;
  
  
  Description:
&lt;/h2&gt;

&lt;p&gt;A Spring Boot service slowed down under load because API calls and database calls shared one thread pool. How the team found it, fixed it, and why virtual threads would not have helped.&lt;/p&gt;

&lt;p&gt;Load went up, and the service slowed down with it. p99 latency climbed from 400 ms to 18 s. CPU sat around 20%. Connection timeout errors started showing up in the logs.&lt;/p&gt;

&lt;p&gt;A slow service usually has something working too hard. This one did not. Requests were piling up, yet very little real work was getting done. Most of the time was going to waiting, and the engineers had to find out what for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the service was doing
&lt;/h2&gt;

&lt;p&gt;Each unit of work was a short chain of blocking calls. Some went to external APIs. Some went to the database. A single unit could touch both, more than once, one after another.&lt;/p&gt;

&lt;p&gt;The service ran many units at the same time with &lt;code&gt;CompletableFuture&lt;/code&gt;. Every stage of every unit ran on one shared thread pool of 100 threads. Database access went through a Hikari connection pool with 20 connections.&lt;/p&gt;

&lt;p&gt;Both kinds of calls block the thread that makes them. A thread that sends an HTTP request sits idle until the response comes back. A thread that needs the database first asks Hikari for a connection. If none is free, it waits. Then it waits again for the query. While a thread waits, it does no useful work. It is also unavailable for anything else.&lt;/p&gt;

&lt;p&gt;So the 100 threads were one shared budget for two different kinds of waiting. When the APIs were fast and connections were free, the budget was enough. When either side slowed down, it spent the budget for both.&lt;/p&gt;

&lt;p&gt;That design was the team's own choice, and it was easy to set up. The problem was that 100 did not describe any real limit in the system. It was just a number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding the cause
&lt;/h2&gt;

&lt;p&gt;The team took thread dumps during the slowdown and lined them up with Hikari's metrics. Nearly every pool thread was blocked. Some were waiting on HTTP responses. Many more were parked inside &lt;code&gt;getConnection&lt;/code&gt;. Hikari showed all 20 connections in use and up to 60 threads waiting for one.&lt;/p&gt;

&lt;p&gt;A thread could get stuck in two ways, and each made the other worse.&lt;/p&gt;

&lt;p&gt;The first was slow API calls. Under load, external calls took longer and held their threads longer. That left fewer threads for every other stage, including database work, which sat in the executor's queue.&lt;/p&gt;

&lt;p&gt;The second was the database. With 100 threads and only 20 connections, most of the pool could end up waiting inside Hikari. A thread waiting for a connection is just as unavailable as one waiting on HTTP. API stages then queued behind them.&lt;/p&gt;

&lt;p&gt;Because both kinds of work drew from the same threads, slowness on one side became delay on the other. Every unit of work waited in the same queue, whatever resource it needed next.&lt;/p&gt;

&lt;p&gt;The root cause was a mismatch between thread demand and available connections. The thread pool and the connection pool had been sized as two separate settings. They should have been one decision. This also explained the low CPU. Blocked threads use almost none.&lt;/p&gt;

&lt;p&gt;Transaction scope made it worse. One code path opened a transaction, called an external API, and wrote the result before committing. The connection stayed checked out for the whole API call. In that path, every slow response took a connection out of the pool while no query was running.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: one executor per resource
&lt;/h2&gt;

&lt;p&gt;The team split the shared pool into two executors. One runs database work. The other runs external API calls. Each stage now runs on the executor for the resource it blocks on.&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ExecutorConfig&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 18 = Hikari maximum-pool-size (20) minus 2 for request-path queries&lt;/span&gt;
    &lt;span class="nd"&gt;@Bean&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolTaskExecutor&lt;/span&gt; &lt;span class="nf"&gt;dbExecutor&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="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"db-"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// 80 = ~200 calls/s x 0.3 s p95 latency = 60, plus headroom&lt;/span&gt;
    &lt;span class="nd"&gt;@Bean&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolTaskExecutor&lt;/span&gt; &lt;span class="nf"&gt;apiExecutor&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="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"api-"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolTaskExecutor&lt;/span&gt; &lt;span class="nf"&gt;pool&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;prefix&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;size&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;ex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolTaskExecutor&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setCorePoolSize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setMaxPoolSize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setThreadNamePrefix&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The DB executor was resized to align with the Hikari pool. A database thread without a connection can only wait, so threads beyond the connection count add waiting and nothing else. With the sizes aligned, extra database work waits in the executor queue, where it holds no thread and no connection. The executor got 18 threads rather than 20, because a few request-path queries use the same Hikari pool outside the executor.&lt;/p&gt;

&lt;p&gt;The API executor needed a different method, and Little's Law gives it. In plain words, the calls in flight equal the calls started per second multiplied by the seconds each call takes. At peak, the service handled about 100 units of work per second, each making two API calls. That is about 200 calls per second. The API's p95 latency under load was around 300 ms, so about 60 calls are in flight at any moment. The team added headroom and set the executor to 80 threads.&lt;/p&gt;

&lt;p&gt;The total thread count barely changed, from 100 to 98. What changed was which work could use which threads.&lt;/p&gt;

&lt;p&gt;Latency is a live input here. If the API gets twice as slow, the same throughput needs twice as many threads. A call with no timeout breaks the math completely, because it can hold a thread for as long as the remote side likes.&lt;/p&gt;

&lt;p&gt;The stages then hop between executors explicitly. A simplified version:&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="nc"&gt;CompletableFuture&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Job&lt;/span&gt; &lt;span class="n"&gt;job&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="nc"&gt;CompletableFuture&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;supplyAsync&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;externalApi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fetch&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;apiExecutor&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenApplyAsync&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&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;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;dbExecutor&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenApplyAsync&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;externalApi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;confirm&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt; &lt;span class="n"&gt;apiExecutor&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;thenAcceptAsync&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;-&amp;gt;&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;markDone&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&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="n"&gt;dbExecutor&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Async&lt;/code&gt; variants matter. Plain &lt;code&gt;thenApply&lt;/code&gt; can run the next stage on whatever thread completed the previous one. In this chain, that would quietly put database work back on an API thread.&lt;/p&gt;

&lt;p&gt;The split also assumes each stage touches one resource. The transactional path broke that rule, so the team moved its API call out of the transaction. The transaction now covers only the writes. They also added dashboards and alerts on pending connections and on each executor's queue depth.&lt;/p&gt;

&lt;p&gt;After the fix, p99 at peak load settled around 450 ms. Connection timeout errors stopped. Peak throughput went from about 70 units per second during the incident back to the expected 100.&lt;/p&gt;

&lt;p&gt;It worked because the two kinds of waiting stopped sharing a budget. A slow API can fill its own executor, but it cannot take threads from database work. A busy database cannot stall API stages. A slow API still lowers overall throughput, since every unit needs both. It just no longer drags the database side down with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Would virtual threads have prevented this?
&lt;/h2&gt;

&lt;p&gt;Java 21 made virtual threads a standard feature. They are cheap, and a service can run a very large number of them. When a virtual thread blocks on I/O, it usually releases the platform thread underneath it. Spring Boot 3.2 added a &lt;code&gt;spring.threads.virtual.enabled&lt;/code&gt; property that moves several of its own components onto virtual threads on Java 21 or later.&lt;/p&gt;

&lt;p&gt;The incident was a story about blocked threads, so the question is fair. The answer is no. Virtual threads make threads cheap. They do nothing about connections, and there would still be only 20 of those.&lt;/p&gt;

&lt;p&gt;Picture the same service with one virtual thread per task, using &lt;code&gt;Executors.newVirtualThreadPerTaskExecutor()&lt;/code&gt;. The 100-thread limit is gone, and every unit starts&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>concurrency</category>
      <category>performance</category>
    </item>
    <item>
      <title>Notify via email when an event occurs in a website</title>
      <dc:creator>Rohit Babu Gaddeti</dc:creator>
      <pubDate>Wed, 20 May 2020 11:22:29 +0000</pubDate>
      <link>https://dev.to/rohitbabugaddeti/notify-via-email-when-an-event-occurs-in-a-website-33jg</link>
      <guid>https://dev.to/rohitbabugaddeti/notify-via-email-when-an-event-occurs-in-a-website-33jg</guid>
      <description>&lt;p&gt;Ever wanted to know as soon as &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a product comes into stock?&lt;/li&gt;
&lt;li&gt;tickets are available for any event?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I think most of us have had those questions. After reading this, you'll know how to implement answers for the above questions/similar questions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/rohitbabugaddeti/swiggy6-notifier" rel="noopener noreferrer"&gt;This&lt;/a&gt; implementation had paved ways to find answers to the above questions.&lt;/p&gt;

&lt;p&gt;The project mentioned above was built in last summer(2019 April) during the IPL season. What is IPL? &lt;a href="https://en.wikipedia.org/wiki/Indian_Premier_League" rel="noopener noreferrer"&gt;check here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The main objective of this project is to notify the user when someone hits a SIX in the IPL match. With few modifications, you can build a system to notify yourself when an event occurs in a website.&lt;/p&gt;

&lt;p&gt;Diving into how I did it, the first and core concept I should discuss about is &lt;a href="https://en.wikipedia.org/wiki/Web_scraping" rel="noopener noreferrer"&gt;Web Scraping&lt;/a&gt;. Web Scraping allows you to get data from websites even if the website owner is not providing any API kind of thing. I used Python language in this project and will be talking about python packages throughout this post. Python has web scraping libraries like &lt;em&gt;BeautifulSoup, Scrapy&lt;/em&gt; etc. I used &lt;em&gt;BeautifulSoup&lt;/em&gt; here because it is simple and fits our need. &lt;/p&gt;

&lt;p&gt;BeautifulSoup consists of powerful parsers that can parse response objects into traversable objects. It also has several inbuilt methods to filter out the data extracted from the website. We can achieve a lot of things using them. &lt;/p&gt;

&lt;p&gt;So, you first need to make a request to the website you wanted to scrape using its URL. This can be done with packages like &lt;em&gt;urllib, urllib3, requests&lt;/em&gt; etc. You'll get a HttpResponse object from the server of the website you requested. Now you have to pass that response object to &lt;em&gt;BeautifulSoup&lt;/em&gt; class specifying which parser to use. It will give a parsed &lt;em&gt;bs4&lt;/em&gt; object accordingly.&lt;/p&gt;

&lt;p&gt;Now, you have to check for the required data from the parsed object using builtin methods or regular expressions.&lt;/p&gt;

&lt;p&gt;You'll want to notify after checking for the data. To notify the user via email, you need two packages: &lt;em&gt;smtplib, email&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;email&lt;/em&gt; is used to frame email object which we will be using in &lt;em&gt;sendmail&lt;/em&gt; method of &lt;em&gt;smtplib&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Using threads to send notifications can prevent the pausing of main activity.&lt;/p&gt;

</description>
      <category>python</category>
      <category>octograd2020</category>
    </item>
  </channel>
</rss>
