<?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: Digvijay Katoch</title>
    <description>The latest articles on DEV Community by Digvijay Katoch (@digvijay_katoch_efadc7529).</description>
    <link>https://dev.to/digvijay_katoch_efadc7529</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2583077%2Fa269be29-9ed7-4a3e-8513-079f1a412f6a.jpg</url>
      <title>DEV Community: Digvijay Katoch</title>
      <link>https://dev.to/digvijay_katoch_efadc7529</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/digvijay_katoch_efadc7529"/>
    <language>en</language>
    <item>
      <title>The Distributed Monolith Is Not a Microservices Problem. It's a Transaction Boundary Problem.</title>
      <dc:creator>Digvijay Katoch</dc:creator>
      <pubDate>Sat, 09 May 2026 06:29:08 +0000</pubDate>
      <link>https://dev.to/digvijay_katoch_efadc7529/the-distributed-monolith-is-not-a-microservices-problem-its-a-transaction-boundary-problem-5j3</link>
      <guid>https://dev.to/digvijay_katoch_efadc7529/the-distributed-monolith-is-not-a-microservices-problem-its-a-transaction-boundary-problem-5j3</guid>
      <description>&lt;p&gt;You've broken your monolith into six services. They deploy independently. They have separate repos.&lt;/p&gt;

&lt;p&gt;And yet — one "simple" business operation touches four of them synchronously, shares a database, and when service C fails, services A and B are left in an inconsistent state.&lt;/p&gt;

&lt;p&gt;Congratulations. You have a distributed monolith. It has all the operational complexity of microservices and all the coupling of a monolith. It is strictly worse than either.&lt;/p&gt;

&lt;p&gt;I've been building enterprise Java systems since 2009. This pattern has killed more modernization projects than any technical decision about frameworks or cloud providers. Here's what it actually is and how to fix it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Makes Something a Distributed Monolith
&lt;/h2&gt;

&lt;p&gt;The tell is in the transaction semantics, not the deployment topology.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom 1: Shared database, multiple services.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderService     → writes to orders table
InventoryService → reads from orders table directly
PaymentService   → joins orders + inventory in a single query
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are not three services. This is one service with three deployment units and a single point of failure at the data layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom 2: Synchronous chain calls in a single business operation.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /checkout
  → OrderService.createOrder()
    → InventoryService.reserve()       // sync HTTP
      → PaymentService.charge()        // sync HTTP
        → NotificationService.send()   // sync HTTP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The latency of this operation is the &lt;strong&gt;sum&lt;/strong&gt; of four network calls. The failure rate is approximately &lt;code&gt;1 - (0.99)^4 ≈ 3.9%&lt;/code&gt; if each service is at 99% availability. You have not improved availability by distributing. You have degraded it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom 3: Rollback logic scattered across services.&lt;/strong&gt;&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;// In OrderService&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;inventoryClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reserve&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;paymentClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;charge&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Now what? Payment went through, inventory didn't?&lt;/span&gt;
    &lt;span class="n"&gt;inventoryClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// This can also fail.&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;"Partial failure. Data is now inconsistent."&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;This is the defining failure mode. You've lost the atomicity guarantee of a single transaction without replacing it with anything.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix: Transaction Boundaries Are the Design
&lt;/h2&gt;

&lt;p&gt;You don't fix a distributed monolith by writing better retry logic. You fix it by deciding where your consistency boundaries actually are and designing around them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Define your aggregates honestly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;An aggregate is the unit of transactional consistency. If &lt;code&gt;Order&lt;/code&gt;, &lt;code&gt;OrderLineItem&lt;/code&gt;, and &lt;code&gt;ReservedInventory&lt;/code&gt; must always be consistent with each other, they are one aggregate — regardless of how many services touch them.&lt;/p&gt;

&lt;p&gt;Don't let team ownership boundaries dictate aggregate boundaries. That's the org-chart-driven architecture trap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Replace synchronous chains with the Transactional Outbox pattern.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of calling downstream services synchronously, write to an outbox table in the same local transaction as your primary state change.&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;@Transactional&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderRepository&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="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

    &lt;span class="c1"&gt;// Same transaction, same DB, same commit&lt;/span&gt;
    &lt;span class="nc"&gt;OutboxEvent&lt;/span&gt; &lt;span class="n"&gt;event&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;OutboxEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"ORDER_CREATED"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;order&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;serialize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;outboxRepository&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;event&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;order&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Committed atomically. No partial state.&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A separate poller (or CDC via Debezium) reads the outbox and publishes to your message broker.&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="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;pollAndPublish&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;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;outboxRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findUnpublished&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;event&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;messageBroker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;outboxRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;markPublished&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&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="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&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;warn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Publish failed for event {}, will retry"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&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="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;&lt;strong&gt;Step 3: Make downstream handlers idempotent.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Since events will be delivered at least once, your consumers must handle duplicates.&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;@EventHandler&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;onOrderCreated&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderCreatedEvent&lt;/span&gt; &lt;span class="n"&gt;event&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;inventoryRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isAlreadyReserved&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getOrderId&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="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Idempotent. No harm in receiving twice.&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;inventoryRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reserve&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getOrderId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getItems&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;&lt;strong&gt;Step 4: Use a Saga for multi-step compensating transactions.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated event →
  InventoryService: ReserveStock →
    StockReserved event →
      PaymentService: ChargePayment →
        PaymentCharged event →
          ShipmentService: CreateShipment

// Compensation on failure:
ShipmentFailed event →
  PaymentService: RefundPayment →
    PaymentRefunded event →
      InventoryService: ReleaseStock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each service listens for events, acts, and emits its result. No central coordinator. No distributed transaction.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Doesn't Fix
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It doesn't fix teams with unclear ownership of data domains. Organizational problems don't have technical solutions.&lt;/li&gt;
&lt;li&gt;It doesn't fix query patterns that require joining data across aggregate boundaries in real time. For that, you need read models (CQRS projections).&lt;/li&gt;
&lt;li&gt;It doesn't make your system simple. A well-designed distributed system is genuinely more complex to operate than a well-designed monolith. Choose distribution because you need the scale or team autonomy — not because microservices are fashionable.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The distributed monolith is what happens when you adopt the deployment model of microservices without adopting the data ownership model. Fix the boundaries first. The deployment follows.&lt;/p&gt;

</description>
      <category>java</category>
      <category>aws</category>
      <category>architecturedatabase</category>
    </item>
    <item>
      <title>Java 21 Virtual Threads + AI Workloads: What the Benchmarks Don't Show You (And What 16 Years Does)</title>
      <dc:creator>Digvijay Katoch</dc:creator>
      <pubDate>Sat, 02 May 2026 04:14:42 +0000</pubDate>
      <link>https://dev.to/digvijay_katoch_efadc7529/java-21-virtual-threads-ai-workloads-what-the-benchmarks-dont-show-you-and-what-16-years-does-1gb7</link>
      <guid>https://dev.to/digvijay_katoch_efadc7529/java-21-virtual-threads-ai-workloads-what-the-benchmarks-dont-show-you-and-what-16-years-does-1gb7</guid>
      <description>&lt;p&gt;I started writing Java professionally in October 2009 but had been working with C and Java since 2005 in college, on the side as well. I have watched every "this changes everything" moment in the JVM ecosystem — G1GC, lambdas, modularity, reactive streams. Each one was real, and each one had a trap the early adopters hit first.&lt;br&gt;
Java 21's Project Loom (virtual threads, GA) and its intersection with AI-augmented backend systems is the current one. Here is the practitioner's guide to what's real and what's a trap.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Virtual Threads Actually Do
&lt;/h2&gt;

&lt;p&gt;They replace OS thread-per-request with JVM-managed continuations. Blocking I/O unmounts the virtual thread from the carrier thread, freeing the carrier for other work. This is legitimate and the throughput gains under high concurrency are real.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trap: Pinning
&lt;/h2&gt;

&lt;p&gt;If a virtual thread parks (blocks) while holding a synchronized monitor, it cannot unmount. It pins to the carrier thread. Result: you're back to N:1 thread contention, but now it's invisible unless you instrument it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnostic flag:
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;java-Djdk.tracePinnedThreads=full
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this in your staging environment. Any output means you have a pinning problem.&lt;br&gt;
Where This Intersects AI Workloads&lt;br&gt;
Modern Spring Boot 3 apps calling AI inference APIs (OpenAI, Bedrock, internal model endpoints) over HTTP are excellent candidates for virtual threads. Java 21's HttpClient is Loom-aware — it unmounts cleanly on I/O wait.&lt;br&gt;
DB2 JDBC access is not a clean case. The legacy driver's internal synchronized usage causes pinning. Options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tune your HikariCP pool to your actual DB2 connection limit, not your thread concurrency target&lt;/li&gt;
&lt;li&gt;Evaluate R2DBC for DB2 if truly non-blocking I/O is required (driver maturity caveat: test heavily)&lt;/li&gt;
&lt;li&gt;Use virtual threads for the AI inference layer and keep JDBC on a bounded executor with clear separation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Java 25 on the Horizon
&lt;/h2&gt;

&lt;p&gt;Watch for: continued Valhalla (value types) progress, which will matter significantly for AI tensor/embedding workloads where you're moving large arrays of primitives. This is not hype — the memory layout implications are real.&lt;/p&gt;

&lt;h2&gt;
  
  
  The One-Sentence Takeaway
&lt;/h2&gt;

&lt;p&gt;Instrument first, architect second: -Djdk.tracePinnedThreads=full tells you more about your system's virtual thread readiness than any benchmark article.&lt;/p&gt;

</description>
      <category>java</category>
      <category>spring</category>
      <category>architecture</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
