<?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: Venkatesan Ramar</title>
    <description>The latest articles on DEV Community by Venkatesan Ramar (@morpheus-vera).</description>
    <link>https://dev.to/morpheus-vera</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%2F3936242%2F5cebb340-ec45-4f77-b185-19f2c7d7a5e8.png</url>
      <title>DEV Community: Venkatesan Ramar</title>
      <link>https://dev.to/morpheus-vera</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/morpheus-vera"/>
    <language>en</language>
    <item>
      <title>Distributed Locking in Practice: Guarantees, Failure Scenarios and Better Alternatives (4/4)</title>
      <dc:creator>Venkatesan Ramar</dc:creator>
      <pubDate>Wed, 19 Aug 2026 08:05:19 +0000</pubDate>
      <link>https://dev.to/morpheus-vera/distributed-locking-in-practice-guarantees-failure-scenarios-and-better-alternatives-44-2am1</link>
      <guid>https://dev.to/morpheus-vera/distributed-locking-in-practice-guarantees-failure-scenarios-and-better-alternatives-44-2am1</guid>
      <description>&lt;p&gt;&lt;strong&gt;22. The Best Distributed Lock Is Often No Lock at All&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By now, we've explored distributed locks, leases, fencing tokens, leader election, and consensus.&lt;/p&gt;

&lt;p&gt;Each of these mechanisms exists to solve a specific coordination problem in distributed systems, where multiple machines must agree on who is allowed to do what and when.&lt;/p&gt;

&lt;p&gt;A natural conclusion from studying all of them might be that building reliable distributed systems requires increasingly sophisticated coordination mechanisms.&lt;/p&gt;

&lt;p&gt;However, in practice, experienced engineers often arrive at the opposite conclusion after working with these systems at scale. Before introducing any form of distributed coordination, they tend to step back and ask a more fundamental question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can we re-design the system so that coordination isn't necessary in the first place?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This question is important because every coordination mechanism introduces real cost into the system.&lt;/p&gt;

&lt;p&gt;It adds additional network communication between services. It increases the number of failure scenarios that must be handled correctly. It introduces operational complexity that must be monitored, debugged, and maintained. It often increases latency, since coordination typically requires waiting for agreement or confirmation.&lt;/p&gt;

&lt;p&gt;Because of these costs, the most reliable distributed systems are not necessarily the ones with the strongest or most complex coordination mechanisms.&lt;/p&gt;

&lt;p&gt;Instead, they are often the systems that minimize coordination as much as possible while still preserving correctness.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;23. Let the Database Protect Its Own Data&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Suppose an application allows users to register with an email address.&lt;/p&gt;

&lt;p&gt;A straightforward but naive approach might introduce a distributed lock to prevent duplicate registrations. The flow would look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Acquire Distributed Lock
        |
        v
Check Email Exists
        |
        v
Insert Customer
        |
        v
Release Lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this design, the distributed lock ensures that two application instances do not attempt to create the same customer at the same time.&lt;/p&gt;

&lt;p&gt;While this approach works in principle, it is often unnecessary in practice. Relational databases already provide strong guarantees about data integrity, including uniqueness constraints. Instead of coordinating at the application level, we can express the business rule directly in the database schema.&lt;/p&gt;

&lt;p&gt;A unique constraint makes the intent explicit and enforces it reliably:&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;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;uk_customer_email&lt;/span&gt;
&lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this constraint in place, every application instance can simply attempt the insert operation without any external coordination.&lt;/p&gt;

&lt;p&gt;The flow becomes much simpler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Insert Customer
        |
        |
Unique Constraint
        |
  Success/Reject
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no distributed lock.&lt;br&gt;
There is no coordination service.&lt;br&gt;
There is no lease management.&lt;/p&gt;

&lt;p&gt;The database already owns the data, and therefore it is the most appropriate place to enforce rules about that data.&lt;/p&gt;

&lt;p&gt;Whenever correctness depends entirely on the state of a single database, delegating enforcement to the database itself is usually the simplest and most reliable solution.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;24. Optimistic Concurrency Instead of Ownership&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some coordination problems arise because multiple application instances attempt to modify the same record at the same time.&lt;/p&gt;

&lt;p&gt;At first glance, this seems to require a distributed lock to ensure exclusive access.&lt;/p&gt;

&lt;p&gt;However, that assumption is not always correct.&lt;/p&gt;

&lt;p&gt;Consider an inventory record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Product
--------
Quantity = 25
Version = 8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now imagine two application instances reading and updating this record concurrently.&lt;/p&gt;

&lt;p&gt;Both instances start from the same version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application A

Version 8
-------------------

Application B

Version 8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Application A performs an update first, and the database successfully applies the change:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Version = 9&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;When Application B attempts to apply its update, it is still working with the outdated version 8. The database detects this mismatch and rejects the update.&lt;/p&gt;

&lt;p&gt;At this point, the application can retry the operation using the latest state from the database.&lt;/p&gt;

&lt;p&gt;No distributed lock was required at any point in this process.&lt;br&gt;
No application had to wait for exclusive ownership of the record.&lt;/p&gt;

&lt;p&gt;Instead of preventing concurrent work, optimistic concurrency allows concurrent operations to proceed and resolves conflicts only when they actually occur. This approach is particularly effective when conflicts are relatively rare.&lt;/p&gt;

&lt;p&gt;It provides high throughput because operations are not blocked unnecessarily. It minimizes coordination overhead between services. It also offers a simple recovery model, where failed updates are retried with fresh state.&lt;/p&gt;

&lt;p&gt;This combination of properties is why &lt;em&gt;optimistic concurrency&lt;/em&gt; is widely used in modern backend systems.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;25. Idempotency Eliminates Many Locking Problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Consider a scenario where a payment request is received more than once.&lt;/p&gt;

&lt;p&gt;This can happen for many reasons in distributed systems.&lt;/p&gt;

&lt;p&gt;A client might retry after a timeout, even though the first request already succeeded.&lt;/p&gt;

&lt;p&gt;A message broker might deliver the same event more than once.&lt;br&gt;
A network failure might occur after the operation completes but before the response is acknowledged.&lt;/p&gt;

&lt;p&gt;One possible solution is to use a distributed lock to ensure the payment is only processed once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Acquire Lock
      |
Charge Customer
      |
Release Lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, there is another approach that avoids coordination entirely by changing the nature of the operation itself.&lt;/p&gt;

&lt;p&gt;In this design, every request includes an idempotency key that uniquely identifies the operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment Request

Idempotency Key
PAY-48291
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before processing the payment, the system checks whether this key has already been used.&lt;/p&gt;

&lt;p&gt;The flow becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Receive Request
       |
Already Processed?
       |
   +---+---+
   |       |
 Yes       No
  |         |
Return     Process
Result     Payment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the request has already been processed, the system simply returns the previous result. If not, it proceeds with the payment and records the key. This makes &lt;em&gt;duplicate requests safe by design&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Instead of coordinating ownership between multiple application instances, the system ensures that repeating the same operation does not change the outcome.&lt;/p&gt;

&lt;p&gt;This approach is often more robust than distributed locking because retries are not edge cases in distributed systems—they are expected behavior.&lt;/p&gt;

&lt;p&gt;Reliable systems are designed with retries in mind. They do not merely try to prevent them; they make them safe.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;26. Partition Ownership Instead of Global Coordination&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Consider a payment platform that processes millions of transactions every day.&lt;/p&gt;

&lt;p&gt;If every payment required a distributed lock, the system would quickly become a bottleneck due to the overhead of coordination.&lt;/p&gt;

&lt;p&gt;A more scalable approach is to partition the work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payments A-H
      |
      v
Consumer A
-------------------

Payments I-P
      |
      v
Consumer B
-------------------

Payments Q-Z
      |
      v
Consumer C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this model, each consumer is responsible for a specific subset of the workload. Each partition has a clear owner, and that ownership is determined by the partitioning strategy rather than dynamic locking.&lt;/p&gt;

&lt;p&gt;As a result, applications no longer compete for individual operations. Instead, they operate independently within their assigned partitions. This pattern is commonly used in message brokers and stream processing systems.&lt;/p&gt;

&lt;p&gt;Rather than coordinating every single operation globally, the system only coordinates partition ownership. This significantly reduces synchronization overhead and improves scalability.&lt;/p&gt;

&lt;p&gt;Partitioning is one of the most effective techniques for reducing the need for distributed coordination in large systems.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;27. Sharding Reduces Shared Ownership&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A similar idea applies to data storage systems.&lt;/p&gt;

&lt;p&gt;Instead of storing all data in a single shared database, the data can be divided into shards.&lt;/p&gt;

&lt;p&gt;For example, customer data might be partitioned alphabetically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customers A-M
       |
       v
Database A
-------------------

Customers N-Z
       |
       v
Database B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each shard is responsible for a subset of the data and operates independently.&lt;/p&gt;

&lt;p&gt;Most requests only interact with a single shard, which means there is no need for global coordination across the entire dataset. Applications only need to coordinate within the boundaries of a single shard, not across all shards.&lt;/p&gt;

&lt;p&gt;By reducing the amount of shared ownership, sharding naturally reduces the need for distributed locking and other global coordination mechanisms.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;28. Sometimes Coordination Isn't Necessary&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not all distributed problems require strict exclusivity or ownership. In some cases, concurrent operations can safely proceed without coordination at all.&lt;/p&gt;

&lt;p&gt;For example, consider a distributed counter that is updated by multiple application instances.&lt;/p&gt;

&lt;p&gt;Instead of preventing concurrent updates, the system can use a data structure designed to merge updates safely. In this model, multiple instances can update the counter at the same time without conflict.&lt;/p&gt;

&lt;p&gt;The applications are not competing for control; they are contributing to a shared result. Some distributed data structures are specifically designed to support this kind of behavior.&lt;/p&gt;

&lt;p&gt;Their goal is not to prevent concurrent updates, but to ensure that all updates eventually converge to a consistent final state. While the details of these structures are beyond the scope of this discussion, they highlight an important principle in distributed system design.&lt;/p&gt;

&lt;p&gt;Coordination should only be introduced when it is truly required for correctness.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;29. Choosing the Simplest Correct Solution&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As distributed systems grow in complexity, a clear pattern emerges.&lt;/p&gt;

&lt;p&gt;Different problems require different coordination strategies, and not all of them require distributed locking.&lt;/p&gt;

&lt;p&gt;Using a distributed lock simply because multiple machines are involved often leads to unnecessary complexity and reduced scalability.&lt;/p&gt;

&lt;p&gt;A better approach is to choose the simplest mechanism that still guarantees correctness.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engineering Problem&lt;/th&gt;
&lt;th&gt;Recommended Approach&lt;/th&gt;
&lt;th&gt;Why It's a Better Choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prevent duplicate customer registration&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Database Unique Constraint&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The database is the source of truth and can enforce uniqueness atomically without external coordination.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrent updates to the same record&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Optimistic Locking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Detects write conflicts efficiently while allowing high concurrency and minimal coordination.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate payment or API requests&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Idempotency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Makes retries safe by ensuring repeated requests produce the same outcome.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One instance should execute scheduled jobs&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Leader Election&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Elects a single coordinator while allowing automatic failover if the leader becomes unavailable.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exclusive access to a shared resource&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Lease + Fencing Tokens&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Provides temporary ownership and prevents stale owners from modifying shared state after lease expiration.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintain consistent cluster configuration or metadata&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Consensus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ensures all healthy nodes agree on a single, consistent view of shared state despite failures.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Process high-volume event streams or queues&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Partition Ownership&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Assigns exclusive ownership of partitions to consumers, eliminating the need for global locking.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Distribute ownership of large datasets&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Sharding&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reduces shared ownership by partitioning data, minimizing the need for cross-node coordination.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merge concurrent updates safely without coordination&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Conflict-Free Data Structures (CRDTs)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Allows concurrent updates to converge automatically, avoiding locks for specific classes of problems.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One important observation is that distributed locking appears only once in this table.&lt;/p&gt;

&lt;p&gt;This is intentional.&lt;/p&gt;

&lt;p&gt;Most real-world problems are better solved using mechanisms that are specifically designed for those problems, rather than relying on a general-purpose locking abstraction.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;30. Common Misconceptions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are several misconceptions that frequently appear when discussing distributed locking in system design.&lt;/p&gt;

&lt;p&gt;One common belief is that distributed locks guarantee correctness. In reality, this is not always true. Correctness depends not only on acquiring the lock, but also on how the protected resource validates ownership. Without mechanisms like fencing tokens, stale lock holders may still be able to modify shared state incorrectly.&lt;/p&gt;

&lt;p&gt;Another misconception is that lease expiration implies the original owner has stopped working. In practice, this is not guaranteed. The application may still be running but temporarily unable to renew the lease due to garbage collection pauses, CPU starvation, or network delays. Distributed systems must reason in terms of timeouts and uncertainty, not absolute state.&lt;/p&gt;

&lt;p&gt;It is also often assumed that leader election replaces consensus. This is not the case. Leader election determines which node is responsible for coordination, while consensus determines what the system agrees upon. These are fundamentally different problems that operate at different layers of the system.&lt;/p&gt;

&lt;p&gt;Finally, there is a belief that every distributed system requires distributed locking. In reality, many systems do not. Techniques such as unique constraints, optimistic concurrency, idempotent operations, and partition ownership often provide simpler and more scalable solutions.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We began this discussion with a simple question: how can multiple application instances ensure that only one of them performs a particular operation?&lt;/p&gt;

&lt;p&gt;At first, distributed locking seems like a complete solution. It enforces exclusivity across machines, giving the impression that once a lock is acquired, all other contenders are safely excluded.&lt;/p&gt;

&lt;p&gt;However, each approach introduces trade-offs that matter in real systems.&lt;/p&gt;

&lt;p&gt;Permanent locks can become stale if the holder crashes, leaving the system blocked indefinitely.&lt;/p&gt;

&lt;p&gt;Leases reduce this risk by introducing time-bounded ownership, but they also allow ownership to expire mid-operation, even when work is still valid. Even with leases, stale ownership can still cause incorrect writes unless the system can distinguish between current and outdated owners.&lt;/p&gt;

&lt;p&gt;Fencing tokens address this by enforcing monotonic ordering, ensuring only the latest owner can modify shared state and preventing delayed operations from corrupting data.&lt;/p&gt;

&lt;p&gt;At a higher level, leader election coordinates long-running responsibilities by ensuring a single active coordinator, while consensus protocols allow multiple nodes to agree on a single consistent state despite failures.&lt;/p&gt;

&lt;p&gt;Together, these mechanisms highlight a core truth about distributed systems:&lt;/p&gt;

&lt;p&gt;Distributed locking is not really about locking—it is about coordination under partial and unreliable information, where no node has a complete or up-to-date view of the system.&lt;/p&gt;

&lt;p&gt;In practice, machines pause, networks delay or drop messages, and failures often look identical to transient slowness. This makes correctness fundamentally harder to reason about.&lt;/p&gt;

&lt;p&gt;As a result, correctness is not achieved by removing uncertainty, but by designing systems that remain correct despite it.&lt;/p&gt;

&lt;p&gt;The key lesson is simple:&lt;/p&gt;

&lt;p&gt;Before introducing distributed locking, first ask whether the problem can be solved without adding coordination at all, since coordination is often the main source of complexity and fragility.&lt;/p&gt;

&lt;p&gt;The most resilient distributed systems are not those with the most advanced locking mechanisms, but those that minimize coordination while still preserving correctness under all expected failure modes.&lt;/p&gt;




&lt;p&gt;Assisted AI to paraphrase. &lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>database</category>
      <category>systemdesign</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Distributed Locking in Practice: Guarantees, Failure Scenarios and Better Alternatives (3/4)</title>
      <dc:creator>Venkatesan Ramar</dc:creator>
      <pubDate>Wed, 19 Aug 2026 07:22:08 +0000</pubDate>
      <link>https://dev.to/morpheus-vera/distributed-locking-in-practice-guarantees-failure-scenarios-and-better-alternatives-34-494m</link>
      <guid>https://dev.to/morpheus-vera/distributed-locking-in-practice-guarantees-failure-scenarios-and-better-alternatives-34-494m</guid>
      <description>&lt;p&gt;&lt;strong&gt;15. Distributed Locking Is Only One Coordination Pattern&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By this point, we've established an important progression in how distributed systems handle coordination.&lt;/p&gt;

&lt;p&gt;A distributed lock is designed to solve a very specific problem: coordinating ownership of a resource across multiple machines. It ensures that only one node can act on a shared resource at a time.&lt;/p&gt;

&lt;p&gt;As systems evolved, we introduced leases to handle a different failure mode—abandoned locks caused by crashed or unreachable nodes. Leases ensure that ownership is temporary and automatically expires if not renewed. We then added fencing tokens to address a more subtle issue: stale owners. Even if a node believes it still holds a lock, a higher-numbered token can prevent it from making unsafe updates.&lt;/p&gt;

&lt;p&gt;This naturally leads to a broader question.&lt;/p&gt;

&lt;p&gt;If distributed locking has become this sophisticated, why do modern distributed systems still rely on concepts such as leader election and consensus?&lt;/p&gt;

&lt;p&gt;The answer lies in a key realization: exclusive ownership is not always the problem we're trying to solve.&lt;/p&gt;

&lt;p&gt;In many systems, the goal is not to have multiple machines competing for the same resource. Instead, the goal is much simpler: ensure that one machine coordinates the rest.&lt;/p&gt;

&lt;p&gt;That difference shifts the entire coordination model.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;16. From Ownership to Leadership&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Consider a cluster of application instances responsible for scheduling background jobs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;          +-------------+
          | Instance A  |
          +-------------+

          +-------------+
          | Instance B  |
          +-------------+

          +-------------+
          | Instance C  |
          +-------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each instance runs the same code. Every minute, all of them check whether invoices should be generated.&lt;/p&gt;

&lt;p&gt;If all instances execute the scheduler independently, duplicate invoices become inevitable.&lt;/p&gt;

&lt;p&gt;A straightforward solution is to use a distributed lock. Before running the scheduler, each instance attempts to acquire the lock. The winner proceeds, and the others wait.&lt;/p&gt;

&lt;p&gt;This works correctly, but it introduces a repeating pattern of contention.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Minute 1

Acquire Lock
↓
Generate Jobs
↓
Release Lock
-------------------

Minute 2

Acquire Lock
↓
Generate Jobs
↓
Release Lock
-------------------

Minute 3

Acquire Lock
↓
Generate Jobs
↓
Release Lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every execution cycle requires coordination. Every minute, the system competes again for the same ownership.&lt;/p&gt;

&lt;p&gt;But the real requirement is simpler: only one instance should be responsible for scheduling. That responsibility does not change frequently.&lt;/p&gt;

&lt;p&gt;Continuously competing for it introduces unnecessary overhead.&lt;/p&gt;

&lt;p&gt;This is where leader election becomes a better fit.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Leadership Is Long-Lived Ownership&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of competing for every operation, the cluster elects a single instance to act as the coordinator.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;          +-------------+
          | Instance A  |
          +-------------+
                 │
                 │
          Leader Elected
                 │
                 ▼
          +-------------+
          | Leader      |
          | Instance A  |
          +-------------+

          +-------------+
          | Instance B  |
          +-------------+

          +-------------+
          | Instance C  |
          +-------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once elected, the leader takes responsibility for shared tasks such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scheduling background jobs&lt;/li&gt;
&lt;li&gt;coordinating cluster state&lt;/li&gt;
&lt;li&gt;assigning work to nodes&lt;/li&gt;
&lt;li&gt;monitoring cluster health&lt;/li&gt;
&lt;li&gt;managing shared configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The other instances continue serving requests, but they no longer compete for leadership unless a failure occurs.&lt;/p&gt;

&lt;p&gt;This changes the coordination model fundamentally.&lt;/p&gt;

&lt;p&gt;Instead of repeatedly asking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Who owns this operation right now?"&lt;br&gt;
the system asks:&lt;br&gt;
"Who is currently the leader of the cluster?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Leadership becomes a long-lived role rather than a short-lived lock acquisition.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;17. Leaders Can Fail Too&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Leader election does not remove failure scenarios. It simply changes how the system responds to them.&lt;/p&gt;

&lt;p&gt;Suppose Instance A is currently the leader.&lt;br&gt;
&lt;/p&gt;

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

      Instance A
           │
           │
      Coordinates Cluster
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While performing its responsibilities, Instance A crashes unexpectedly.&lt;/p&gt;

&lt;p&gt;At that moment, the cluster loses its coordinator. No other node is actively managing scheduling or coordination.&lt;/p&gt;

&lt;p&gt;To recover, the system must elect a new leader.&lt;/p&gt;

&lt;p&gt;This process is called &lt;strong&gt;leader election&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;Before Failure

Leader
Instance A
---------------------
After Failure

Leader
Instance B

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

&lt;/div&gt;



&lt;p&gt;Unlike distributed locking, leader election is not triggered for every operation. It only occurs when leadership is lost.&lt;/p&gt;

&lt;p&gt;Most of the time, the leader continues operating without interruption, making it a relatively stable coordination role.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Leadership Is Also Temporary&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A natural question arises at this point: how does the system know the leader has actually failed?&lt;/p&gt;

&lt;p&gt;The honest answer is that it does not know with certainty.&lt;/p&gt;

&lt;p&gt;The same uncertainty we saw earlier still applies.&lt;/p&gt;

&lt;p&gt;The leader may have crashed.&lt;br&gt;
The network may be partitioned.&lt;br&gt;
The process may be paused.&lt;br&gt;
The machine may be overloaded.&lt;/p&gt;

&lt;p&gt;From the perspective of other nodes, all they can observe is that communication has stopped.&lt;/p&gt;

&lt;p&gt;Because of this uncertainty, leader election relies on the same foundational mechanisms we have already discussed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;temporary ownership&lt;/li&gt;
&lt;li&gt;heartbeats&lt;/li&gt;
&lt;li&gt;leases&lt;/li&gt;
&lt;li&gt;timeouts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The problem has not disappeared. It has simply shifted from protecting a resource to protecting the leadership role itself.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;18. Why Leader Election Is Not Consensus&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Engineers often encounter leader election and consensus together, but they solve different problems.&lt;/p&gt;

&lt;p&gt;Leader election answers a narrow question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Which node should coordinate the cluster?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Consensus answers a broader one:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do multiple nodes agree on shared state despite failures?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Consider a cluster of three nodes maintaining configuration data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;          Node A

          Node B

          Node C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now imagine a configuration change is introduced. Every node must eventually agree on the same final state.&lt;/p&gt;

&lt;p&gt;If one node applies the change while another rejects it, the cluster becomes inconsistent.&lt;/p&gt;

&lt;p&gt;Leader election alone cannot solve this. It only determines who proposes or coordinates the change. It does not guarantee agreement.&lt;/p&gt;

&lt;p&gt;Consensus is what ensures that all nodes converge on the same decision.&lt;/p&gt;

&lt;p&gt;This distinction is subtle but critical.&lt;/p&gt;

&lt;p&gt;Leadership assigns responsibility.&lt;br&gt;
Consensus ensures agreement.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;19. Coordination Requires Agreement&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To make this more concrete, consider a distributed storage system with three nodes maintaining metadata.&lt;/p&gt;

&lt;p&gt;The leader decides that a new storage node should join the cluster.&lt;/p&gt;

&lt;p&gt;If the update is not applied consistently across all nodes, the system becomes fragmented.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Node A

Storage 1
Storage 2
Storage 3
------------------

Node B

Storage 1
Storage 2
Storage 3
------------------

Node C

Storage 1
Storage 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even though a leader exists, the cluster is now inconsistent. One node has a different view of membership than the others.&lt;/p&gt;

&lt;p&gt;This demonstrates an important principle: leadership alone is not enough.&lt;/p&gt;

&lt;p&gt;Consensus mechanisms exist to ensure that all healthy nodes eventually agree on the same state.&lt;/p&gt;

&lt;p&gt;The exact algorithms behind this—such as Paxos or Raft—are complex and deserve their own discussion.&lt;/p&gt;

&lt;p&gt;For now, the key architectural insight is:&lt;/p&gt;

&lt;p&gt;Leader election determines &lt;strong&gt;who coordinates&lt;/strong&gt;.&lt;br&gt;
Consensus determines &lt;strong&gt;what everyone agrees on&lt;/strong&gt;.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;20. Where Modern Coordination Systems Fit&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Modern distributed coordination systems often combine all of these concepts into a single platform.&lt;/p&gt;

&lt;p&gt;They typically provide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;temporary ownership through leases&lt;/li&gt;
&lt;li&gt;fencing via monotonic tokens&lt;/li&gt;
&lt;li&gt;leader election&lt;/li&gt;
&lt;li&gt;consensus mechanisms&lt;/li&gt;
&lt;li&gt;failure detection&lt;/li&gt;
&lt;li&gt;cluster membership management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of requiring every application to implement these primitives independently, the coordination system provides them as shared infrastructure.&lt;/p&gt;

&lt;p&gt;From an application perspective, the architecture becomes much simpler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
      │
      ▼
Coordination Platform
      │
      ├── Leases
      ├── Leader Election
      ├── Cluster Membership
      ├── Consensus
      └── Coordination Metadata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application focuses on business logic, while the platform handles correctness under failure conditions.&lt;/p&gt;

&lt;p&gt;An important observation here is that the specific technology—whether ZooKeeper, etcd, Consul, or another system—is often less important than understanding the underlying coordination problem.&lt;/p&gt;

&lt;p&gt;Choosing a tool before understanding the problem often leads to unnecessary complexity.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;21. Choosing the Right Coordination Primitive&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Throughout this article, we have explored several coordination mechanisms. While they are often discussed together, each one solves a distinct problem.&lt;/p&gt;

&lt;p&gt;Understanding these differences makes system design significantly clearer.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engineering Problem&lt;/th&gt;
&lt;th&gt;Coordination Primitive&lt;/th&gt;
&lt;th&gt;Primary Guarantee&lt;/th&gt;
&lt;th&gt;Typical Use Cases&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ensure only one node performs a critical operation&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Distributed Lock / Lease&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Temporary exclusive ownership&lt;/td&gt;
&lt;td&gt;Scheduled jobs, file processing, cache refresh, resource ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Prevent stale owners from modifying shared state&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Fencing Tokens&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rejects operations from older owners&lt;/td&gt;
&lt;td&gt;Inventory updates, payment processing, distributed storage, metadata updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ensure one active coordinator exists&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Leader Election&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Single active leader for cluster-wide coordination&lt;/td&gt;
&lt;td&gt;Job schedulers, controllers, coordinators, cluster management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keep multiple nodes in agreement&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Consensus&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Consistent cluster state despite failures&lt;/td&gt;
&lt;td&gt;Cluster membership, configuration management, metadata, distributed coordination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recover ownership automatically after failures&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Leases&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ownership expires unless renewed&lt;/td&gt;
&lt;td&gt;Long-running tasks, distributed locks, leadership management&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detect failed coordinators and trigger recovery&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Heartbeats + Timeouts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Failure detection based on liveness&lt;/td&gt;
&lt;td&gt;Leader monitoring, cluster health, node membership&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This table reflects the progression we have followed throughout the article.&lt;/p&gt;

&lt;p&gt;We began with distributed locks for shared resource coordination.&lt;br&gt;
We then addressed their limitations with leases.&lt;br&gt;
We fixed stale ownership with fencing tokens.&lt;br&gt;
We improved coordination efficiency with leader election.&lt;br&gt;
Finally, we ensured correctness across nodes with consensus.&lt;/p&gt;

&lt;p&gt;Each mechanism builds on the limitations of the previous one. None replaces the others. Instead, they form a layered toolkit for building reliable distributed systems.&lt;/p&gt;

&lt;p&gt;The final question in this series naturally follows from here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do we actually need distributed locking at all?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In many real-world systems, correctness is achieved through alternative approaches such as optimistic concurrency, idempotent operations, database constraints, partition ownership, or queue-based processing. Understanding when to use these alternatives is often more valuable than implementing a distributed lock itself.&lt;/p&gt;




</description>
      <category>distributedsystems</category>
      <category>database</category>
      <category>systemdesign</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Distributed Locking in Practice: Guarantees, Failure Scenarios and Better Alternatives (2/4)</title>
      <dc:creator>Venkatesan Ramar</dc:creator>
      <pubDate>Tue, 18 Aug 2026 09:48:00 +0000</pubDate>
      <link>https://dev.to/morpheus-vera/distributed-locking-in-practice-guarantees-failure-scenarios-and-better-alternatives-24-809</link>
      <guid>https://dev.to/morpheus-vera/distributed-locking-in-practice-guarantees-failure-scenarios-and-better-alternatives-24-809</guid>
      <description>&lt;p&gt;In this article, we'll explore the mechanisms to solve the coordination problem. &lt;/p&gt;




&lt;p&gt;&lt;strong&gt;8. Introducing Leases&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To address the problem of permanent ownership, distributed systems typically replace it with temporary ownership.&lt;/p&gt;

&lt;p&gt;This concept is known as a &lt;em&gt;lease&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Instead of granting indefinite control over a resource, the coordination service assigns ownership for a limited period of time.&lt;/p&gt;

&lt;p&gt;Rather than stating, “You own this resource until you explicitly release it,” the system instead says, “You own this resource for the next 30 seconds.”&lt;/p&gt;

&lt;p&gt;This changes the interaction model significantly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Acquire Lease
       |
       v
Execute Work
       |
       v
Renew Lease
       |
       v
Continue Processing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As long as the application remains healthy, it periodically renews the lease to maintain ownership. If the application crashes or becomes unresponsive, it can no longer renew the lease. Once the lease duration expires, ownership is automatically revoked.&lt;/p&gt;

&lt;p&gt;At that point, another application becomes eligible to acquire the lease and continue the work.&lt;/p&gt;

&lt;p&gt;Leases solve a critical problem in distributed systems: &lt;em&gt;they prevent abandoned locks from blocking progress indefinitely&lt;/em&gt;. The system can recover automatically without manual intervention.&lt;/p&gt;

&lt;p&gt;However, while leases improve availability, they also introduce a new class of subtle and more complex problems.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Leases Depend on Time&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To understand the next challenge, assume the lease duration is thirty seconds.&lt;/p&gt;

&lt;p&gt;Application A successfully acquires the lease.&lt;/p&gt;

&lt;p&gt;Lease Granted&lt;/p&gt;

&lt;p&gt;Duration = 30 seconds&lt;/p&gt;

&lt;p&gt;After twenty seconds, the JVM begins a long Full Garbage Collection cycle. This pause lasts forty seconds, significantly longer than the lease duration.&lt;/p&gt;

&lt;p&gt;The timeline now becomes problematic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lease Granted
      |
      |
Processing
      |
      |
GC Pause (40 sec)
      |
      |
Lease Expires
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While Application A is paused, the lease expires. During this time, another application requests access to the same resource.&lt;/p&gt;

&lt;p&gt;The coordination service observes that the previous lease has expired and therefore grants ownership to Application B.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application A             Application B

Lease Holder
      |
GC Pause
                           Acquire Lease
                                |
                          Lease Granted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the garbage collection completes, Application A resumes execution. From its perspective, it simply continues where it left off, still believing it owns the resource.&lt;/p&gt;

&lt;p&gt;At the same time, Application B also believes it legitimately owns the resource because it was granted a valid lease by the coordination service.&lt;/p&gt;

&lt;p&gt;Neither application is behaving incorrectly. Both are operating based on information that was valid at different points in time.&lt;/p&gt;

&lt;p&gt;However, the system now has two active owners for the same resource.&lt;/p&gt;

&lt;p&gt;This situation is significantly more dangerous than an abandoned lock because both participants are actively performing work under the assumption of exclusive ownership.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;9. Split Brain Without a Network Partition&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Many engineers associate split-brain scenarios exclusively with network partitions, where parts of the system become isolated from each other.&lt;/p&gt;

&lt;p&gt;However, as the previous example demonstrates, split-brain conditions can occur even without a network failure.&lt;/p&gt;

&lt;p&gt;Application A still believes it holds a valid lease, while Application B has legitimately acquired a newer lease from the coordination service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        Coordination Service

          Lease Expired
                |
      +---------+---------+
      |                   |
      v                   v
Application A      Application B
Believes           Believes
It Owns            It Owns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both applications continue processing independently.&lt;/p&gt;

&lt;p&gt;Importantly, the coordination service is behaving correctly. It expired the old lease and issued a new one based on its rules. Application A is also behaving correctly, because it has not yet observed that its lease has expired. Application B is also correct, because it received a valid lease.&lt;/p&gt;

&lt;p&gt;The problem is not incorrect behavior by any single component. The problem is that ownership information has become stale and inconsistent across the system.&lt;/p&gt;

&lt;p&gt;This leads to a crucial realization in distributed systems design: acquiring a lease does not guarantee continuous or permanent ownership. Ownership is not a static property; it can change while an application is temporarily unable to observe that change.&lt;/p&gt;

&lt;p&gt;Leases solve one important problem by eliminating abandoned locks, but they introduce another, more subtle challenge.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How do we ensure that an application does not continue performing work after it has lost ownership?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Answering this question leads directly to one of the most important concepts in distributed coordination: &lt;em&gt;fencing tokens&lt;/em&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;10. Why Leases Alone Cannot Protect Your Data&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The previous section ended with an uncomfortable situation.&lt;/p&gt;

&lt;p&gt;Application A acquired a lease, but the JVM paused long enough for that lease to expire. During that time, Application B legitimately acquired a new lease. When Application A eventually resumed, both applications believed they owned the same resource.&lt;/p&gt;

&lt;p&gt;This leads to a natural question: why doesn't the coordination service simply reject any further requests from Application A once its lease has expired?&lt;/p&gt;

&lt;p&gt;The answer is simple, but important. The coordination service has no control over what Application A does after it acquires the lease. Once ownership is granted, the application runs independently and continues performing work on its own.&lt;/p&gt;

&lt;p&gt;It cannot intercept every database update, file write, API call, or business operation that the application performs.&lt;/p&gt;

&lt;p&gt;To make this concrete, consider a payment processing service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;            Coordination Service
                    |
             Lease Granted
                    |
                    v
             Payment Service
                    |
                    v
             Banking System
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After the lease is granted, the payment service communicates directly with the banking system. At this point, the coordination service is no longer in the execution path.&lt;/p&gt;

&lt;p&gt;If the lease expires while the payment service is paused, nothing prevents it from continuing to submit payment requests after it resumes. The coordination service may know the lease has expired, but the banking system does not.&lt;/p&gt;

&lt;p&gt;This reveals a key insight: a distributed lock controls ownership, but it does not automatically control every operation performed by the owner.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ownership and Authority Are Different&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To understand the problem more clearly, consider a warehouse system that reserves inventory.&lt;/p&gt;

&lt;p&gt;Application A acquires a lease and begins processing. While it is still working, the lease expires. At that point, Application B becomes the new owner.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;       Lease Owner

     Application A
            ↓
     Lease Expires
            ↓
     Application B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now imagine Application A resumes execution. Even though it is no longer the owner, it still holds references to in-memory objects, still has an open database connection, and still has network access.&lt;/p&gt;

&lt;p&gt;Nothing physically prevents it from continuing to update inventory.&lt;/p&gt;

&lt;p&gt;The coordination service cannot reach back in time and undo or block operations that are already in progress.&lt;/p&gt;

&lt;p&gt;So while ownership has changed, the authority to modify the shared resource has not been automatically revoked in the running application.&lt;/p&gt;

&lt;p&gt;This is why leases alone cannot guarantee correctness in distributed systems.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;11. The Missing Piece&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let’s revisit the inventory example in more detail.&lt;/p&gt;

&lt;p&gt;The inventory contains a single remaining item. Application A acquires a lease and begins processing. A few seconds later, it pauses unexpectedly. During this pause, the lease expires.&lt;/p&gt;

&lt;p&gt;Application B then acquires a new lease and successfully reserves the inventory item.&lt;/p&gt;

&lt;p&gt;Several seconds later, Application A resumes and also attempts to reserve the same item.&lt;/p&gt;

&lt;p&gt;At this point, the inventory becomes inconsistent.&lt;/p&gt;

&lt;p&gt;What is important here is that neither application violated the lease protocol. The coordination service behaved correctly. Both applications followed the rules as designed.&lt;/p&gt;

&lt;p&gt;The problem is that the shared resource had no way to distinguish between the current owner and a previous owner that resumed late.&lt;/p&gt;

&lt;p&gt;Both requests looked valid.&lt;/p&gt;

&lt;p&gt;The missing capability is not another lock or another lease mechanism. The missing capability is a way to determine which owner is newer.&lt;/p&gt;

&lt;p&gt;This is exactly the problem that &lt;em&gt;fencing tokens&lt;/em&gt; solve.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;12. Introducing Fencing Tokens&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Instead of only granting a lease, the coordination service also issues a monotonically increasing number along with it.&lt;/p&gt;

&lt;p&gt;This number is called a &lt;strong&gt;fencing token&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Each time a new lease is granted, the token increases. Every new owner receives a strictly larger value than the previous one.&lt;/p&gt;

&lt;p&gt;The sequence might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application A
Lease Granted
Token = 101
--------------------
Application B
Lease Granted
Token = 102
--------------------
Application C
Lease Granted
Token = 103
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fencing token represents the freshness of ownership. A larger token always means a more recent owner.&lt;/p&gt;

&lt;p&gt;Unlike timestamps, fencing tokens do not depend on clock synchronization. They are simply increasing numbers generated by the coordination system.&lt;/p&gt;

&lt;p&gt;This small addition fundamentally changes how safety is enforced in distributed systems.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why Increasing Numbers Matter&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now consider what happens when Application A pauses after receiving token 101. While it is paused, Application B acquires a lease and receives token 102.&lt;br&gt;
&lt;/p&gt;

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

Application A
Acquire Lease
Token 101
      |
      |
GC Pause
      |
      |
Resume
-------------------------

Application B

Acquire Lease
Token 102
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both applications continue running independently.&lt;/p&gt;

&lt;p&gt;Without fencing tokens, both would appear equally valid when they attempt to modify the shared resource.&lt;/p&gt;

&lt;p&gt;With fencing tokens, every operation now carries proof of ownership.&lt;/p&gt;

&lt;p&gt;Application A sends a request like this:&lt;/p&gt;

&lt;p&gt;Reserve Inventory&lt;/p&gt;

&lt;p&gt;Token = 101&lt;/p&gt;

&lt;p&gt;Application B sends:&lt;/p&gt;

&lt;p&gt;Reserve Inventory&lt;/p&gt;

&lt;p&gt;Token = 102&lt;/p&gt;

&lt;p&gt;The resource can now immediately determine which request belongs to the most recent owner.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;13. The Resource Must Enforce the Token&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the point where many explanations stop, and also where many real-world systems fail.&lt;/p&gt;

&lt;p&gt;The coordination service generates fencing tokens, but it does not enforce them. The responsibility of validation belongs entirely to the protected resource.&lt;/p&gt;

&lt;p&gt;For example, imagine the inventory database keeps track of the highest token it has seen so far.&lt;/p&gt;

&lt;p&gt;Initially, the state might be:&lt;/p&gt;

&lt;p&gt;Highest Token = 101&lt;/p&gt;

&lt;p&gt;When Application B submits its request with token 102, the database compares it with the stored value. Since 102 is greater, the request is accepted, and the state is updated:&lt;/p&gt;

&lt;p&gt;Highest Token = 102&lt;/p&gt;

&lt;p&gt;Later, Application A resumes and submits a request with token 101. The database performs the same comparison:&lt;/p&gt;

&lt;p&gt;101 &amp;lt; 102&lt;/p&gt;

&lt;p&gt;Since the incoming token is older than the current highest token, the request is rejected.&lt;/p&gt;

&lt;p&gt;This prevents the stale owner from modifying the shared resource.&lt;/p&gt;

&lt;p&gt;The key insight is that correctness is enforced by the resource itself, not by the coordination service and not by the lease mechanism.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why the Coordination Service Cannot Do This&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first glance, it might seem simpler if the coordination service itself rejected stale operations.&lt;/p&gt;

&lt;p&gt;However, this is not possible in practice.&lt;/p&gt;

&lt;p&gt;The coordination service only participates during lease acquisition. After that point, all business operations happen directly between the application and the resource.&lt;/p&gt;

&lt;p&gt;Consider this flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
      |
Acquire Lease
      |
Coordination Service

Application
      |
      |
      v
Database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the lease is granted, every subsequent operation bypasses the coordination service entirely. Because of this, only the database (or the protected resource) can determine whether a request is stale.&lt;/p&gt;

&lt;p&gt;This is why fencing tokens must accompany every operation that modifies shared state.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;14. Where Fencing Tokens Are Useful&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fencing tokens are useful anywhere multiple distributed processes can modify a shared resource.&lt;/p&gt;

&lt;p&gt;Common examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;updating inventory systems&lt;/li&gt;
&lt;li&gt;processing payment batches&lt;/li&gt;
&lt;li&gt;writing to shared storage systems&lt;/li&gt;
&lt;li&gt;updating distributed metadata stores&lt;/li&gt;
&lt;li&gt;controlling scheduled or background jobs&lt;/li&gt;
&lt;li&gt;modifying cluster configuration or leadership state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The underlying technology does not matter. What matters is that multiple actors can attempt to modify the same resource, and correctness depends on ensuring only the newest owner succeeds.&lt;/p&gt;

&lt;p&gt;Whenever a system must distinguish between stale and current ownership, fencing tokens provide a reliable solution.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fencing Tokens Are Not a Replacement for Leases&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is important to understand that leases and fencing tokens solve different problems.&lt;/p&gt;

&lt;p&gt;Leases answer the question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Who currently owns the resource?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Fencing tokens answer a different question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Is this request coming from the most recent owner?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A system that uses leases without fencing tokens can still suffer from stale writes. A system that uses fencing tokens without leases has no mechanism for transferring ownership in the first place.&lt;/p&gt;

&lt;p&gt;In practice, robust distributed systems use both together.&lt;/p&gt;

&lt;p&gt;The lease establishes temporary ownership.&lt;/p&gt;

&lt;p&gt;The fencing token allows the resource to verify that ownership before accepting any operation.&lt;/p&gt;

&lt;p&gt;This separation of concerns is a key idea behind production systems like ZooKeeper and etcd, which must remain correct even in the presence of failures, pauses, and network partitions.&lt;/p&gt;




</description>
      <category>distributedsystems</category>
      <category>database</category>
      <category>systemdesign</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Distributed Locking in Practice: Guarantees, Failure Scenarios and Better Alternatives (1/4)</title>
      <dc:creator>Venkatesan Ramar</dc:creator>
      <pubDate>Mon, 17 Aug 2026 05:41:00 +0000</pubDate>
      <link>https://dev.to/morpheus-vera/distributed-locking-in-practice-guarantees-failure-scenarios-and-better-alternatives-14-3plb</link>
      <guid>https://dev.to/morpheus-vera/distributed-locking-in-practice-guarantees-failure-scenarios-and-better-alternatives-14-3plb</guid>
      <description>&lt;p&gt;Distributed systems solve many problems by dividing work across multiple machines. The same characteristic also introduces an entirely new class of problems.&lt;/p&gt;

&lt;p&gt;Machines must coordinate.&lt;/p&gt;

&lt;p&gt;Unlike threads running inside a single JVM, independent services have no shared memory, no common execution context, and no built-in synchronization mechanism. Every instance makes decisions based on the information available to it at that moment. When several instances attempt to perform the same business operation simultaneously, coordination becomes significantly more difficult than it first appears.&lt;/p&gt;

&lt;p&gt;Distributed locking is often introduced as the solution to this problem.&lt;/p&gt;

&lt;p&gt;It certainly plays an important role.&lt;br&gt;
It is also one of the most misunderstood concepts in distributed systems.&lt;/p&gt;

&lt;p&gt;Many discussions focus on implementing distributed locks using a particular technology. Much less attention is given to understanding what distributed locking actually guarantees, what it cannot guarantee, and why many production systems solve coordination problems without using distributed locks at all.&lt;/p&gt;

&lt;p&gt;This article explores distributed locking from an engineering perspective.&lt;/p&gt;

&lt;p&gt;Rather than starting with technologies, we'll begin with the coordination problem itself.&lt;/p&gt;

&lt;p&gt;As the discussion progresses, each solution will naturally expose its own limitations. Those limitations lead us toward leases, split-brain scenarios, fencing tokens, leader election, consensus, and finally the situations where a distributed lock is not the right solution.&lt;/p&gt;

&lt;p&gt;Because distributed locking is ultimately not about acquiring a lock.&lt;/p&gt;

&lt;p&gt;It is about keeping distributed systems correct while machines fail, networks become unreliable, and time itself becomes uncertain.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;1. The Coordination Problem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine an e-commerce platform during a flash sale. Only one unit of a limited-edition product remains in inventory. Two customers place an order at nearly the same time.&lt;/p&gt;

&lt;p&gt;The application is deployed across multiple instances.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                Inventory = 1

          +----------------------+
          |                      |
          v                      v
     Application A          Application B
          |                      |
          |                      |
     Reserve Item           Reserve Item
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both application instances receive the request almost simultaneously.&lt;/p&gt;

&lt;p&gt;Both read the inventory.&lt;br&gt;
Both conclude that one unit is available.&lt;br&gt;
Both attempt to reserve it.&lt;/p&gt;

&lt;p&gt;The inventory now becomes negative.&lt;/p&gt;

&lt;p&gt;No service crashed.&lt;br&gt;
No exception occurred.&lt;br&gt;
The database remained healthy.&lt;/p&gt;

&lt;p&gt;Every component behaved exactly as designed.&lt;/p&gt;

&lt;p&gt;The failure was not caused by incorrect code. It was caused by the &lt;em&gt;absence of coordination&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Both applications made independent decisions about the same business resource.&lt;/p&gt;

&lt;p&gt;This problem appears in many business systems.&lt;/p&gt;

&lt;p&gt;Examples include processing the same payment twice, generating duplicate invoices, executing the same scheduled job on multiple servers, refreshing the same cache simultaneously, and assigning the same delivery request to multiple drivers.&lt;/p&gt;

&lt;p&gt;Although these scenarios appear unrelated, they all share the same underlying problem.&lt;/p&gt;

&lt;p&gt;Multiple independent machines need to agree that only one of them should perform a particular operation. That is &lt;strong&gt;coordination problem&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Coordination Is Different From Concurrency&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Concurrency is a familiar concept for most Java developers.&lt;/p&gt;

&lt;p&gt;Two threads execute simultaneously inside the same JVM. They share memory, they share execution context, and they rely on built-in synchronization primitives provided by the language and runtime. Because of this shared environment, coordination is relatively straightforward: threads can directly block, signal, and enforce mutual exclusion using well-defined mechanisms.&lt;/p&gt;

&lt;p&gt;Distributed systems operate under entirely different conditions.&lt;/p&gt;

&lt;p&gt;Application instances do not share memory. They cannot directly observe each other's state. Communication happens over a network, which introduces latency, partial failures, and uncertainty. Messages may be delayed, reordered, or lost. Machines may fail independently without affecting others. As a result, every instance has only a partial and potentially outdated view of the overall system.&lt;/p&gt;

&lt;p&gt;Imagine three application instances running in different environments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  +-------------+    +-------------+    +-------------+
  | Application |    | Application |    | Application |
  |      A      |    |      B      |    |      C      |
  +-------------+    +-------------+    +-------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each instance executes independently, making decisions based solely on local information. None of them can infer the state of the others unless explicit communication occurs. This lack of shared state fundamentally changes how synchronization must be designed in distributed systems.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;2. Why Traditional Locks Stop Working&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most Java developers are comfortable solving concurrency problems using synchronization primitives such as synchronized blocks or explicit locks.&lt;/p&gt;

&lt;p&gt;Consider a simple example of protecting a critical section in a single JVM.&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;InventoryService&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;final&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="n"&gt;lock&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;Object&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;reserve&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;productId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

        &lt;span class="kd"&gt;synchronized&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Reserve inventory&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;Inside a single JVM, this approach works exactly as expected. Only one thread can enter the critical section at a time, and the JVM enforces mutual exclusion reliably. The lock effectively serializes access to shared in-memory state.&lt;/p&gt;

&lt;p&gt;The problem becomes apparent when the same application is deployed across multiple instances.&lt;/p&gt;

&lt;p&gt;Suppose the service runs on two different servers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;          JVM A                     JVM B

     synchronized(lock)      synchronized(lock)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each JVM maintains its own memory space. Each instance creates its own independent lock object. There is no shared state between them, and neither JVM is aware of the other’s existence. As a result, both threads can enter their respective synchronized blocks simultaneously without any coordination.&lt;/p&gt;

&lt;p&gt;The synchronization mechanism has not failed. It has simply been applied outside its intended scope. It only guarantees mutual exclusion within a single process, not across multiple processes.&lt;/p&gt;

&lt;p&gt;The same limitation applies to other in-memory primitives such as &lt;code&gt;ReentrantLock&lt;/code&gt;, &lt;code&gt;Semaphore&lt;/code&gt;, and &lt;code&gt;ReadWriteLock&lt;/code&gt;. All of them assume a shared memory model, which distributed systems fundamentally do not provide.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What About Database Locks?&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A natural question arises at this point: if multiple application instances share the same database, can the database itself act as a coordination mechanism?&lt;/p&gt;

&lt;p&gt;The answer is nuanced.&lt;/p&gt;

&lt;p&gt;In some cases, it works well. In others, it is appropriate but limited. In many cases, it is not suitable at all.&lt;/p&gt;

&lt;p&gt;Consider a scenario where two applications attempt to update the same customer record. A relational database can enforce row-level locking, ensuring that only one transaction modifies the row at a time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application A
       |
Row Locked
       |
Application B Waits
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works effectively because the resource being protected—the database row—already exists within the database’s control domain. The database is naturally responsible for managing consistency of its own data, so using its locking mechanism aligns with its design.&lt;/p&gt;

&lt;p&gt;Now consider a different scenario: a scheduled reconciliation job that must run only once per night. The job performs multiple external operations such as downloading files, calling third-party services, generating reports, and uploading results.&lt;/p&gt;

&lt;p&gt;None of these operations map cleanly to a single database row. There is no single database entity that represents the job’s execution state in a way that the database can meaningfully lock. The resource requiring coordination exists outside the database’s transactional boundary.&lt;/p&gt;

&lt;p&gt;This highlights an important distinction. Database locks are highly effective for protecting database state, but they are not a general-purpose coordination mechanism for distributed workflows that span multiple systems and external dependencies.&lt;/p&gt;

&lt;p&gt;Recognizing this boundary prevents overusing database locking as a distributed coordination strategy.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;3. What Problem Are We Actually Solving?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At this point, it is easy to assume that the solution is simply a lock that works across machines. However, this intuition only partially captures the real problem.&lt;/p&gt;

&lt;p&gt;The actual goal is &lt;strong&gt;not the lock&lt;/strong&gt; itself. The goal is ensuring that &lt;strong&gt;only one application instance performs a specific business responsibility at a given time&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This distinction is subtle but critical.&lt;/p&gt;

&lt;p&gt;Consider an application responsible for generating monthly customer statements. If three instances of the application start simultaneously, each instance may attempt to perform the same job independently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Instance A
             |
             |
        Generate Statements

        Instance B
             |
             |
        Generate Statements

        Instance C
             |
             |
        Generate Statements
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all three proceed, the system produces duplicate work and potentially inconsistent results. However, the business requirement is not about locking—it is about ensuring exclusivity of execution.&lt;/p&gt;

&lt;p&gt;Only one instance should perform the task.&lt;/p&gt;

&lt;p&gt;This requirement is fundamentally about ownership rather than synchronization. The system needs a way to assign temporary responsibility for a task to a single participant.&lt;/p&gt;

&lt;p&gt;This pattern appears repeatedly in distributed systems. One instance may need to refresh a shared cache, process a payment batch, act as a leader, execute scheduled tasks, or migrate shared data. In all cases, the requirement is consistent: a single owner must perform a specific responsibility for a defined period of time.&lt;/p&gt;

&lt;p&gt;Distributed locking is one possible mechanism to achieve this outcome, but it is not the outcome itself.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;4. The First Distributed Lock&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To address coordination across multiple machines, a shared coordination service is often introduced. Before performing a critical operation, each application instance requests permission from this service.&lt;/p&gt;

&lt;p&gt;The interaction typically follows a simple pattern.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application
      |
Acquire Lock
      |
      v
Coordination Service
      |
Lock Granted
      |
      v
Execute Work
      |
Release Lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From a conceptual standpoint, this resembles traditional synchronization. One participant acquires exclusive access, others wait or retry, and only a single instance proceeds with execution.&lt;/p&gt;

&lt;p&gt;At first glance, this appears to solve the problem effectively. The coordination service replaces shared memory, and mutual exclusion is enforced across distributed instances. The business requirement of single execution is satisfied.&lt;/p&gt;

&lt;p&gt;If distributed systems were perfectly reliable, this model would be sufficient and straightforward.&lt;/p&gt;

&lt;p&gt;However, real-world systems introduce complexity that does not exist in single-process environments. Time becomes unreliable. Machines can fail unexpectedly. Network communication can be delayed or interrupted. Processes may pause due to garbage collection or crash entirely.&lt;/p&gt;

&lt;p&gt;These factors introduce uncertainty into the system. A lock that was granted moments ago may no longer represent a valid or safe assumption about system state.&lt;/p&gt;

&lt;p&gt;As a result, the problem evolves.&lt;/p&gt;

&lt;p&gt;It is no longer sufficient to ask, “Who holds the lock?”&lt;/p&gt;

&lt;p&gt;The more important question becomes, “Is the current lock holder still alive and capable of safely completing the work?”&lt;/p&gt;

&lt;p&gt;This shift in perspective fundamentally changes how distributed locking must be designed and evaluated. It also explains why acquiring a distributed lock is only the beginning of the problem, not its solution.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;5. Failure Changes Everything&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;At the end of the previous section, we arrived at a seemingly simple solution.&lt;/p&gt;

&lt;p&gt;Every application instance requests a distributed lock before performing a critical operation.&lt;/p&gt;

&lt;p&gt;Only one instance receives the lock, while all other instances are forced to wait until it becomes available again.&lt;/p&gt;

&lt;p&gt;From a business perspective, this appears to satisfy the requirement because only one application performs the work at any given time.&lt;/p&gt;

&lt;p&gt;If distributed systems behaved like a single JVM, this would be sufficient. In a single process, synchronization primitives provide strong guarantees about mutual exclusion and ownership.&lt;/p&gt;

&lt;p&gt;However, distributed systems do not behave like a single JVM.&lt;/p&gt;

&lt;p&gt;The difficulty begins the moment we acknowledge a simple reality: machines fail.&lt;/p&gt;

&lt;p&gt;More importantly, machines sometimes appear to fail even when they are still running correctly. This distinction fundamentally changes how distributed coordination must be designed.&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;When a Machine Stops Responding&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s assume an application successfully acquires a distributed lock.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application A
      |
Acquire Lock
      |
Lock Granted
      |
Processing...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, everything appears normal. The system has successfully established exclusive access, and the application begins performing its critical work.&lt;/p&gt;

&lt;p&gt;A few seconds later, the application stops responding.&lt;/p&gt;

&lt;p&gt;This immediately raises an important question: &lt;em&gt;should another application be allowed to acquire the lock?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At first glance, the answer seems obvious. If the application is not responding, it feels reasonable that another instance should take over and continue the work.&lt;/p&gt;

&lt;p&gt;However, distributed systems rarely provide clarity about why a machine stopped responding. There are many possible explanations, and from the outside, they often look identical.&lt;/p&gt;

&lt;p&gt;The application may have crashed unexpectedly due to a runtime error or fatal exception. The operating system itself may have restarted the machine as part of maintenance or recovery. The network connection may have been interrupted, isolating the machine even though it is still running. The machine may simply be overloaded, causing extreme delays in processing requests. In some cases, the process may still be alive and functioning correctly, but temporarily unable to communicate with the rest of the system.&lt;/p&gt;

&lt;p&gt;From the perspective of other machines, all of these scenarios produce the same observable behavior: &lt;em&gt;the application becomes silent&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This silence is deceptively simple, yet extremely difficult to interpret correctly in a distributed environment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Failure Is Often Uncertainty&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of the most important differences between concurrent programming and distributed systems is that failures are rarely definitive in distributed environments.&lt;/p&gt;

&lt;p&gt;Inside a single JVM, the behavior is clear and deterministic. A thread either holds a lock or it does not. A method either completes successfully or it throws an exception. The system has complete visibility into execution state.&lt;/p&gt;

&lt;p&gt;Distributed systems, however, operate with incomplete and delayed information.&lt;/p&gt;

&lt;p&gt;Imagine three application instances interacting in a network.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;               +-------------+
               | Instance A  |
               +-------------+
                      |
          Communication Lost
                      X
        +-------------+-------------+
        |                           |
        v                           v
 +-------------+             +-------------+
 | Instance B  |             | Instance C  |
 +-------------+             +-------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this scenario, Instances B and C stop receiving messages from Instance A. From their perspective, communication has simply ceased.&lt;/p&gt;

&lt;p&gt;There are several possible explanations for this condition. Instance A may have crashed entirely. The network between the instances may have failed or become partitioned. Instance A may still be running but temporarily paused or overloaded. Alternatively, messages may simply be delayed in transit due to congestion or resource constraints.&lt;/p&gt;

&lt;p&gt;The key challenge is that the remaining instances cannot immediately distinguish between these possibilities. They only observe the absence of communication, not its cause.&lt;/p&gt;

&lt;p&gt;This uncertainty is not an edge case in distributed systems; it is the default operating condition. As a result, engineering decisions must often be made without complete or reliable information.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;6. Time Introduces New Problems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now consider a different scenario. Application A successfully acquires a distributed lock and begins processing a large batch of financial transactions.&lt;/p&gt;

&lt;p&gt;Initially, everything proceeds as expected. The system is stable, and the application is actively performing work.&lt;/p&gt;

&lt;p&gt;However, after a short period, the JVM triggers a Full Garbage Collection cycle. During this process, the application is paused while memory is being reclaimed and reorganized.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Acquire Lock
      |
      v
Begin Processing
      |
      v
Full GC Pause
      |
      |
      |
      v
Resume Processing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the perspective of the application itself, nothing unusual has occurred. Execution has simply been paused temporarily and will resume once garbage collection completes.&lt;/p&gt;

&lt;p&gt;From the perspective of the rest of the system, however, the situation looks very different. During the pause, the application is unable to respond to requests, send heartbeats, or communicate its state. For several seconds, it appears completely unresponsive.&lt;/p&gt;

&lt;p&gt;Importantly, the application is not dead. It is still running. It is simply not making progress.&lt;/p&gt;

&lt;p&gt;Distributed systems struggle with this distinction because external observers cannot easily determine whether a system is paused or permanently failed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A Pause Can Look Like a Failure&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This ambiguity often surprises engineers who are new to distributed systems.&lt;/p&gt;

&lt;p&gt;A useful analogy is waiting for a colleague to respond to an important message. If several minutes pass without a reply, multiple interpretations become possible. The colleague may be busy with other work. Their phone may be turned off or disconnected. They may have lost internet connectivity. In the worst case, they may no longer be available at all.&lt;/p&gt;

&lt;p&gt;Without additional context, all of these explanations remain equally plausible.&lt;/p&gt;

&lt;p&gt;Distributed systems face the same problem at scale. A temporary pause caused by garbage collection, heavy CPU contention, thread starvation, or resource exhaustion can appear identical to a complete system failure.&lt;/p&gt;

&lt;p&gt;Because of this ambiguity, distributed systems cannot rely on perfect failure detection. There is no reliable mechanism that can always distinguish between a slow system and a dead one.&lt;/p&gt;

&lt;p&gt;Instead, systems must make decisions based on incomplete and sometimes misleading information.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;7. Locks Cannot Last Forever&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To understand why distributed locking is fundamentally different from local locking, consider what would happen if a coordination service granted locks indefinitely.&lt;/p&gt;

&lt;p&gt;In that model, the interaction would look familiar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Acquire Lock
      |
      v
     Work
      |
      v
Release Lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This mirrors traditional synchronization inside a single JVM, where a thread holds a lock until it explicitly releases it.&lt;/p&gt;

&lt;p&gt;Now revisit a failure scenario. Application A acquires the lock and begins processing. Shortly afterward, the machine hosting the application crashes unexpectedly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application A
      |
Acquire Lock
      |
Machine Crash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the application crashes, it never reaches the step where it releases the lock.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Release Lock&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;As a result, the lock remains permanently held. No other application can acquire it, and every other instance is forced to wait indefinitely.&lt;/p&gt;

&lt;p&gt;The system effectively stops making progress.&lt;/p&gt;

&lt;p&gt;This illustrates a critical problem: in distributed systems, permanent ownership is dangerous because failures are not rare exceptions—they are expected events.&lt;/p&gt;

&lt;p&gt;To maintain availability, the system must be able to recover from abandoned locks automatically.&lt;/p&gt;




&lt;p&gt;Assisted AI to paraphrase. &lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>database</category>
      <category>systemdesign</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building Reliable Event-Driven Systems: Event Schemas, Versioning, Contract Testing and Events vs Commands (5/5)</title>
      <dc:creator>Venkatesan Ramar</dc:creator>
      <pubDate>Wed, 22 Jul 2026 08:50:00 +0000</pubDate>
      <link>https://dev.to/morpheus-vera/building-reliable-event-driven-systems-event-schemas-versioning-contract-testing-and-events-vs-47ed</link>
      <guid>https://dev.to/morpheus-vera/building-reliable-event-driven-systems-event-schemas-versioning-contract-testing-and-events-vs-47ed</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;In this final article, we're going to explore &lt;strong&gt;Production practices&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;strong&gt;22. Events Are Delivered, Not Guaranteed&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A common misconception in event-driven architecture is that publishing an event guarantees it will be processed exactly once.&lt;/p&gt;

&lt;p&gt;Production systems rarely provide that guarantee.&lt;/p&gt;

&lt;p&gt;Events may be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;delivered multiple times&lt;/li&gt;
&lt;li&gt;delayed&lt;/li&gt;
&lt;li&gt;delivered out of order&lt;/li&gt;
&lt;li&gt;temporarily unavailable&lt;/li&gt;
&lt;li&gt;replayed long after publication&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reliable systems are not built on ideal conditions. They assume these scenarios will occur.&lt;/p&gt;

&lt;p&gt;As a result, designing resilient consumers is just as important as designing reliable producers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Duplicate Delivery Is Normal&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consider the following workflow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderConfirmed
       |
       v
Inventory Service
       |
Inventory Reserved
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Inventory Service processes the event successfully. Before acknowledging completion, it crashes.&lt;/p&gt;

&lt;p&gt;After recovery, the event is delivered again.&lt;/p&gt;

&lt;p&gt;From the messaging system’s perspective, this is correct behavior. From the application’s perspective, the same business event has arrived twice.&lt;/p&gt;

&lt;p&gt;Without safeguards, inventory may be reserved twice.&lt;/p&gt;

&lt;p&gt;Duplicate delivery is not an edge case. It is expected behavior in distributed systems.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;23. Idempotency Makes Event Processing Safe&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Idempotency ensures that executing the same operation multiple times produces the same result.&lt;/p&gt;

&lt;p&gt;For event consumers, this means processing the same event repeatedly should not create duplicate business outcomes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Using Event Identifiers&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each event should include an &lt;code&gt;eventId&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Consumers track processed events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Processed Events
----------------
E-1001
E-1002
E-1003
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a new event arrives, say: eventId = E-1002&lt;/p&gt;

&lt;p&gt;The consumer checks if it has already processed it.&lt;/p&gt;

&lt;p&gt;If yes, ignore it&lt;br&gt;
If no, process it&lt;/p&gt;

&lt;p&gt;Flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Receive Event
      |
      v
Already Processed?
      |
 +----+----+
 |         |
Yes        No
 |          |
Ignore   Process Event
            |
            v
      Record eventId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach ensures correctness even with duplicate delivery.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Idempotency Belongs to Business Logic&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Infrastructure can reduce duplicates but cannot eliminate them.&lt;/p&gt;

&lt;p&gt;Idempotency must be enforced at the application level.&lt;/p&gt;

&lt;p&gt;Example: a payment service must never charge a customer twice for the same event.&lt;/p&gt;

&lt;p&gt;This guarantee belongs to business logic, not messaging infrastructure.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;24. Ordering Cannot Always Be Assumed&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Developers often assume events arrive in the order they were published.&lt;/p&gt;

&lt;p&gt;In reality, delivery order can change due to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;network latency&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;parallel processing&lt;/li&gt;
&lt;li&gt;independent consumers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Expected sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
      |
OrderConfirmed
      |
OrderShipped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A consumer might observe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderConfirmed
      |
OrderCreated
      |
OrderShipped
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The producer behaved correctly. The delivery order changed.&lt;/p&gt;

&lt;p&gt;Consumers should not rely on strict ordering unless explicitly guaranteed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Design Consumers to Tolerate Reordering&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consumers should validate state before processing.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Receive `OrderShipped`
         |
   Order Exists?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If required state is missing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retry later&lt;/li&gt;
&lt;li&gt;delay processing&lt;/li&gt;
&lt;li&gt;move the event for later handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach handles temporary inconsistencies without assuming perfect ordering.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;25. Correlation Makes Distributed Systems Understandable&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A single user action can trigger multiple events across services.&lt;/p&gt;

&lt;p&gt;Consider a sample flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create Order
      |
OrderConfirmed
      |
InventoryReserved
      |
PaymentCompleted
      |
ShipmentCreated
      |
InvoiceGenerated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without correlation, these events appear unrelated. But, with a shared identifier like &lt;em&gt;REQ-98451&lt;/em&gt; the entire workflow becomes traceable.&lt;/p&gt;

&lt;p&gt;Correlation improves observability without affecting business logic.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;26. Events Become Part of Your Audit Trail&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Events naturally capture business history.&lt;/p&gt;

&lt;p&gt;Example lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
      |
OrderConfirmed
      |
PaymentCompleted
      |
ShipmentCreated
      |
OrderDelivered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sequence provides a complete record of what happened. Because events represent completed facts, they should never be modified.&lt;/p&gt;

&lt;p&gt;History must remain immutable.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;27. Observability Should Include Events&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Traditional systems usually track:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;request latency&lt;/li&gt;
&lt;li&gt;response time&lt;/li&gt;
&lt;li&gt;database queries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Event-driven systems require additional metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;published events&lt;/li&gt;
&lt;li&gt;consumed events&lt;/li&gt;
&lt;li&gt;failed processing&lt;/li&gt;
&lt;li&gt;retry count&lt;/li&gt;
&lt;li&gt;duplicate events&lt;/li&gt;
&lt;li&gt;processing latency&lt;/li&gt;
&lt;li&gt;dead-letter events&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without these, failures may go unnoticed until business issues appear.&lt;/p&gt;

&lt;p&gt;Observability must include event flows, not just services.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tracing Event Flows&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Distributed tracing becomes more effective with correlation identifiers.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Customer Request
        |
Order Service
        |
Inventory Service
        |
Payment Service
        |
Shipping Service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tracing allows engineers to follow a complete business workflow instead of isolated logs.&lt;/p&gt;

&lt;p&gt;This significantly improves debugging and incident analysis.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;28. A Practical Checklist&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Reliable event-driven systems consistently follow these practices.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design&lt;/strong&gt;&lt;br&gt;
✓ Events represent business facts&lt;br&gt;
✓ Events expose business concepts, not internal models&lt;br&gt;
✓ Payloads contain meaningful business data&lt;br&gt;
✓ Technical metadata is separated from business data&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evolution&lt;/strong&gt;&lt;br&gt;
✓ Schemas evolve carefully&lt;br&gt;
✓ Breaking changes are avoided&lt;br&gt;
✓ Optional fields are preferred over removal&lt;br&gt;
✓ Compatibility is continuously validated&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reliability&lt;/strong&gt;&lt;br&gt;
✓ Consumers are idempotent&lt;br&gt;
✓ Duplicate delivery is expected&lt;br&gt;
✓ Ordering is not assumed&lt;br&gt;
✓ Correlation identifiers are included&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operations&lt;/strong&gt;&lt;br&gt;
✓ Contracts are validated automatically&lt;br&gt;
✓ Event processing is observable&lt;br&gt;
✓ Historical events remain immutable&lt;br&gt;
✓ Producers and consumers evolve independently&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Event-driven architecture is often described as a &lt;em&gt;messaging pattern&lt;/em&gt;. In practice, it is a &lt;em&gt;contract-driven architecture&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Messaging transports events. Contracts enable independent evolution.&lt;/p&gt;

&lt;p&gt;Reliable systems are not defined by tools or frameworks. They are defined by disciplined practices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;designing events as business contracts&lt;/li&gt;
&lt;li&gt;evolving schemas safely&lt;/li&gt;
&lt;li&gt;validating contracts continuously&lt;/li&gt;
&lt;li&gt;building resilient consumers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These principles apply regardless of technology.&lt;/p&gt;

&lt;p&gt;When applied consistently, events become more than messages. They become stable, long-lived contracts that support scalable and maintainable distributed systems.&lt;/p&gt;




&lt;p&gt;Assisted AI to paraphrase.&lt;/p&gt;

</description>
      <category>eventdriven</category>
      <category>distributedsystems</category>
      <category>systemdesign</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building Reliable Event-Driven Systems: Event Schemas, Versioning, Contract Testing and Events vs Commands (4/5)</title>
      <dc:creator>Venkatesan Ramar</dc:creator>
      <pubDate>Mon, 20 Jul 2026 09:00:00 +0000</pubDate>
      <link>https://dev.to/morpheus-vera/building-reliable-event-driven-systems-event-schemas-versioning-contract-testing-and-events-vs-7fo</link>
      <guid>https://dev.to/morpheus-vera/building-reliable-event-driven-systems-event-schemas-versioning-contract-testing-and-events-vs-7fo</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;In this article, we're going to explore &lt;strong&gt;Contract Testing&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;Contract testing&lt;/em&gt; ensures that event contracts remain stable as systems evolve. Producers receive immediate feedback when changes affect consumers, and consumers gain confidence in the data they process.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;16. Why Contract Testing Matters&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Events act as public contracts, and those contracts evolve as systems grow. The key question is how a producer can verify that a schema change does not break its consumers.&lt;/p&gt;

&lt;p&gt;Many teams rely on integration testing for this purpose. While useful, integration tests only confirm that systems can communicate. They do not guarantee that all consumers still understand the event contract.&lt;/p&gt;

&lt;p&gt;Consider an Order Service publishing an &lt;code&gt;OrderConfirmed&lt;/code&gt; event consumed by multiple services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  OrderConfirmed
                         |
      +------------------+-------------------+
      |                  |                   |
      v                  v                   v
 Inventory          Billing          Notification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the producer renames a field:&lt;br&gt;
&lt;code&gt;{&lt;br&gt;
    "customerId": "CUS-501"&lt;br&gt;
}&lt;/code&gt;&lt;br&gt;
to:&lt;br&gt;
&lt;code&gt;{&lt;br&gt;
    "accountId": "CUS-501"&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;everything may still appear correct. The code compiles, tests pass, and deployment succeeds. However, the Billing Service may &lt;strong&gt;fail at runtime&lt;/strong&gt; because it still expects &lt;code&gt;customerId&lt;/code&gt;. The producer had &lt;em&gt;no visibility into this dependency&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The issue is not deployment failure but &lt;strong&gt;contract violation&lt;/strong&gt;. Contract testing exists to detect such issues before they reach production.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Integration Tests Cannot Protect Unknown Consumers&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Integration tests typically validate interactions between known systems:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order Service
      |
      v
Inventory Service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this setup, both systems are controlled and predictable. Event-driven systems differ because consumers are loosely coupled and may not be known to the producer.&lt;/p&gt;

&lt;p&gt;A new Analytics Service might start consuming &lt;code&gt;OrderConfirmed&lt;/code&gt; months later. The producer cannot manually validate every consumer. Contract testing addresses this by validating expectations rather than communication.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;17. Consumer-Driven Contracts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Traditional API testing starts with the producer defining the contract. Consumers adapt to it. Consumer-driven contract testing reverses this approach.&lt;/p&gt;

&lt;p&gt;Consumers define their expectations, and producers verify that they continue to meet them. This model works well in event-driven systems where consumers evolve independently.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Thinking From the Consumer's Perspective&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Different consumers often require different subsets of data. For example, the Billing Service may need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CUS-501"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"totalAmount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;249.99&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Notification Service may require:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CUS-501"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"customerEmail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alice@example.com"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The producer does not need to understand each consumer’s logic. It only needs to satisfy their declared contracts. This encourages careful evolution and explicit documentation of expectations.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A Contract Is More Than Field Names&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Contracts define more than structure. They also capture meaning and constraints.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;code&gt;{&lt;br&gt;
    "orderId": "ORD-1001",&lt;br&gt;
    "totalAmount": 249.99&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;A robust contract may specify:&lt;br&gt;
&lt;code&gt;orderId&lt;/code&gt; must exist and not be empty&lt;br&gt;
&lt;code&gt;totalAmount&lt;/code&gt; must be numeric&lt;br&gt;
&lt;code&gt;totalAmount&lt;/code&gt; must be greater than zero&lt;/p&gt;

&lt;p&gt;These rules ensure data integrity and provide stronger guarantees for both producers and consumers.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;18. Contract Testing in Practice&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Consider an Order Service publishing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"eventType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OrderConfirmed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"eventVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CUS-501"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"totalAmount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;249.99&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consumers depend on different fields:&lt;br&gt;
&lt;strong&gt;Billing&lt;/strong&gt;: orderId, customerId, totalAmount&lt;br&gt;
&lt;strong&gt;Inventory&lt;/strong&gt;: orderId&lt;br&gt;
&lt;strong&gt;Notification&lt;/strong&gt;: customerId&lt;/p&gt;

&lt;p&gt;If the producer changes the schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"accountId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CUS-501"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"totalAmount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;249.99&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the system may still compile and deploy. However, &lt;em&gt;contract validation fails&lt;/em&gt; because &lt;em&gt;consumer expectations are no longer met&lt;/em&gt;. The issue is detected before release, preventing runtime failures.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Development Workflow&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Contract testing integrates into the delivery pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Consumer Defines Contract
          |
          v
Producer Validates Contract
          |
          v
   Build Succeeds
          |
          v
       Deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every schema change is validated during the build process, ensuring compatibility before deployment.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;19. Using JSON Schema to Describe Events&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;JSON Schema provides a structured way to define event contracts. It acts as an executable specification shared by producers and consumers.&lt;/p&gt;

&lt;p&gt;Consider a sample schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"totalAmount"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"totalAmount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"number"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This schema defines required fields, types, and structure. Additional constraints can include string lengths, numeric ranges, formats, and enumerations. Unlike static documentation, &lt;em&gt;schemas can be validated automatically&lt;/em&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Validation During Publishing&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Producers can validate events before publishing:&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;OrderConfirmedEvent&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="n"&gt;validator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;validate&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;eventPublisher&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Invalid events are rejected early, preventing faulty data from entering the system.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Validation During Consumption&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consumers can also validate incoming events:&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;OrderConfirmedEvent&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="n"&gt;validator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;validate&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;process&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This ensures that only valid data reaches business logic, improving reliability.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;20. Schema Registries and Centralized Contracts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As systems scale, managing schemas across multiple repositories becomes difficult. Teams may define similar events differently, leading to inconsistencies.&lt;/p&gt;

&lt;p&gt;A centralized schema repository addresses this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              Event Schemas
                    |
       +------------+------------+
       |                         |
       v                         v
 Producers                  Consumers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This repository becomes the single source of truth. Producers publish against registered schemas, and consumers validate against them.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why Centralized Schemas Help&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Centralized schema management improves consistency and visibility. Contracts become discoverable, version history is preserved, and compatibility rules can be enforced automatically.&lt;/p&gt;

&lt;p&gt;Teams spend less time interpreting payloads and more time building features. The contract becomes a shared organizational asset rather than an isolated implementation detail.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;21. Common Contract Testing Mistakes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Several common issues arise when adopting contract testing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Treating Documentation as the Contract&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Documentation often becomes outdated as implementations change. Consumers may rely on incorrect assumptions. Executable contracts remain synchronized with the system and provide reliable validation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Testing Only Happy Paths&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Contracts should cover more than valid scenarios. They should include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;missing required fields&lt;/li&gt;
&lt;li&gt;invalid data types&lt;/li&gt;
&lt;li&gt;unexpected values&lt;/li&gt;
&lt;li&gt;optional fields&lt;/li&gt;
&lt;li&gt;deprecated fields&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This ensures consumers handle both valid and invalid inputs correctly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ignoring Backward Compatibility&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Passing current contracts does not guarantee future compatibility. Schema evolution must be validated alongside contract correctness to avoid breaking existing consumers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Assuming Producers Own the Contract&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Although producers publish events, contracts are shared responsibilities. Both producers and consumers must maintain and validate them.&lt;/p&gt;




&lt;p&gt;Reliable event-driven systems depend on well-defined, continuously validated contracts. &lt;/p&gt;

&lt;p&gt;In the next part, we will explore production practices like idempotency, event ordering, duplicate handling, correlation and  observability.&lt;/p&gt;

</description>
      <category>eventdriven</category>
      <category>distributedsystems</category>
      <category>systemdesign</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building Reliable Event-Driven Systems: Event Schemas, Versioning, Contract Testing and Events vs Commands (3/5)</title>
      <dc:creator>Venkatesan Ramar</dc:creator>
      <pubDate>Fri, 17 Jul 2026 09:27:53 +0000</pubDate>
      <link>https://dev.to/morpheus-vera/building-reliable-event-driven-systems-event-schemas-versioning-contract-testing-and-events-vs-l6o</link>
      <guid>https://dev.to/morpheus-vera/building-reliable-event-driven-systems-event-schemas-versioning-contract-testing-and-events-vs-l6o</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;In this article, we're going to explore &lt;strong&gt;Event Schema evolution with Event versioning&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;strong&gt;10. Event Schemas Will Eventually Change&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No event schema stays the same forever. As businesses grow, regulations shift, products gain new features, and processes become more complex, the data shared between services must evolve as well. This evolution is not optional—it is a natural consequence of a system adapting to changing requirements.&lt;/p&gt;

&lt;p&gt;Many teams initially assume they can simply update an event whenever needed. This assumption may hold when there is only one producer and one consumer, but real-world systems rarely remain that simple. Over time, multiple consumers emerge, each with its own responsibilities and release cycles.&lt;/p&gt;

&lt;p&gt;A typical system often looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  OrderConfirmed
                         |
      +------------------+-------------------+
      |                  |                   |
      v                  v                   v
 Inventory          Billing          Notification
      |
      v
 Analytics
      |
      v
 Customer Insights
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each consumer evolves independently. Some services may deploy updates weekly, while others might release changes quarterly. In some cases, consumers may even belong to external teams with entirely different priorities and timelines. Because of this, producers cannot assume that all consumers will upgrade simultaneously.&lt;/p&gt;

&lt;p&gt;Schema evolution, therefore, is not just about modifying data structures. It is fundamentally about maintaining compatibility across independently evolving systems.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Compatibility Is More Important Than Version Numbers&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When discussing schema evolution, teams often focus immediately on versioning. While versioning is useful, compatibility is far more critical. Without compatibility, versioning alone cannot prevent system breakage.&lt;/p&gt;

&lt;p&gt;Consider the following event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CUS-501"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"totalAmount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;249.99&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now imagine a new requirement introduces currency. One approach might replace the existing field entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CUS-501"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;249.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Although the data model has improved, this change breaks every existing consumer that depends on the original structure. The producer has evolved, but the contract has not been preserved.&lt;/p&gt;

&lt;p&gt;The goal of schema evolution is to allow both producers and consumers to evolve independently without causing disruptions.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;11. Backward and Forward Compatibility&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Compatibility is often described using formal definitions, but a practical understanding is more useful when designing real systems.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Backward Compatibility&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Backward compatibility ensures that existing consumers continue to function when producers introduce newer versions of events. This is one of the most important principles in event-driven systems.&lt;/p&gt;

&lt;p&gt;Consider Version 1 of an event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CUS-501"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now Version 2 introduces an additional field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CUS-501"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Older consumers can safely ignore the new field, allowing them to continue operating without modification. This makes adding optional fields one of the safest ways to evolve a schema. In contrast, removing fields is far more risky because it can break existing consumers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Forward Compatibility&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Forward compatibility addresses the opposite scenario, where newer consumers must handle older events produced by systems that have not yet been upgraded.&lt;/p&gt;

&lt;p&gt;For example, a new consumer might expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, older producers may still emit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, consumers must be designed to handle missing fields gracefully. They might use default values, leave fields empty, or apply fallback logic. This approach ensures that consumers remain resilient even when the system evolves asynchronously.&lt;/p&gt;

&lt;p&gt;Consumers should never assume that every field will always be present, as distributed systems rarely evolve in perfect synchronization.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Compatibility Is a Team Discipline&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most compatibility issues arise not from technical limitations but from &lt;em&gt;incorrect assumptions&lt;/em&gt;. A common example is the belief that all consumers have already upgraded to the latest version.&lt;/p&gt;

&lt;p&gt;In practice, this assumption is rarely valid. Independent deployment is one of the key advantages of microservices, and compatibility is what preserves that independence. Without it, teams become tightly coupled, and deployments require coordination, defeating the purpose of a distributed architecture.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;12. Safe Schema Evolution&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Schema evolution&lt;/em&gt; is ultimately about maintaining trust between producers and consumers. Producers must continue evolving to meet business needs, while consumers must retain the freedom to upgrade on their own timelines.&lt;/p&gt;

&lt;p&gt;Not all schema changes carry the same level of risk. Some changes are generally safe and can be introduced with minimal impact, while others are inherently breaking and require careful planning.&lt;/p&gt;

&lt;p&gt;Understanding the difference between these types of changes is essential for preventing production failures.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Adding Optional Fields&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding optional fields is usually &lt;em&gt;a safe way to evolve a schema&lt;/em&gt;. It allows new functionality to be introduced without disrupting existing consumers.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CUS-501"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can evolve into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CUS-501"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Older consumers will ignore the new field, while newer consumers can take advantage of it. This approach supports gradual adoption and minimizes risk.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Removing Fields&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Removing fields is significantly &lt;em&gt;more dangerous&lt;/em&gt;. If any consumer depends on a field, its removal will cause failures.&lt;/p&gt;

&lt;p&gt;For instance, if a Billing service relies on:&lt;br&gt;
&lt;code&gt;{&lt;br&gt;
  "totalAmount": 249.99&lt;br&gt;
}&lt;/code&gt;&lt;br&gt;
and that field is removed, the service will break. Because it is often difficult to know all consumers of an event, it is safest to assume that every published field is in use somewhere.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Renaming Fields&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Renaming fields may appear harmless, but it effectively behaves like removing one field and adding another. This makes it &lt;em&gt;a breaking change&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;code&gt;{&lt;br&gt;
   "customerId": "CUS-501"&lt;br&gt;
}&lt;/code&gt;&lt;br&gt;
changing to:&lt;br&gt;
&lt;code&gt;{&lt;br&gt;
   "accountId": "CUS-501"&lt;br&gt;
}&lt;/code&gt;&lt;br&gt;
will cause existing consumers to fail because they no longer recognize the expected field. Renaming should therefore be treated with the same caution as removing fields.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Changing Data Types&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Changing the data type of a field can also introduce &lt;em&gt;subtle but serious issues&lt;/em&gt;. Even if serialization succeeds, consumers may fail when processing the data.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;code&gt;{&lt;br&gt;
   "quantity": 5&lt;br&gt;
}&lt;/code&gt;&lt;br&gt;
becoming:&lt;br&gt;
&lt;code&gt;{&lt;br&gt;
   "quantity": "5"&lt;br&gt;
}&lt;/code&gt;&lt;br&gt;
may not immediately cause errors during transmission, but it can break downstream logic that expects a numeric value. Type changes should be treated as breaking changes and handled carefully.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;13. Versioning Strategies&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There are situations where compatibility alone is not sufficient, and the contract must change in a way that cannot be made backward-compatible. In such cases, versioning becomes necessary.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Version Inside the Event&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One approach is to include version information directly within the event metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"eventType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OrderConfirmed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"eventVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows consumers to adjust their behavior based on the version while keeping the event name consistent. It provides flexibility but requires consumers to handle multiple versions within their logic.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Different Event Types&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Another approach is to define separate event types for each version:&lt;br&gt;
&lt;code&gt;OrderConfirmedV1&lt;br&gt;
OrderConfirmedV2&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In this model, consumers &lt;em&gt;subscribe only to the versions&lt;/em&gt; they support. The producer may need to &lt;em&gt;maintain multiple versions during the transition period&lt;/em&gt;, but this approach &lt;em&gt;simplifies consumer logic by avoiding conditional handling&lt;/em&gt; within a single event type.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Which Strategy Is Better?&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no universally correct choice between these strategies. Embedding versions keeps naming simpler, while separate event types make changes more explicit.&lt;/p&gt;

&lt;p&gt;The most important factor is consistency. Teams should adopt a standard approach across services to reduce confusion and operational complexity.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;14. Avoid Breaking Changes Whenever Possible&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Breaking changes introduce tight coupling between services by forcing coordinated deployments. Instead of allowing independent evolution, they create dependencies that can slow down development and increase risk.&lt;/p&gt;

&lt;p&gt;Consider the following structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Producer
    |
    +-----------------------------+
    |      |      |      |        |
    v      v      v      v        v
Service Service Service Service Service
   A       B       C       D       E
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the producer removes a field, every consumer must update before the change can be safely deployed. Deployment order becomes critical, and the independence of services is lost.&lt;/p&gt;

&lt;p&gt;Even a single breaking change can undermine the benefits of an event-driven architecture.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Deprecation Is Usually Better Than Removal&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a field becomes obsolete, it is better to deprecate it rather than remove it immediately.&lt;br&gt;
&lt;code&gt;{&lt;br&gt;
    "legacyField": "..."&lt;br&gt;
}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Keeping the field while marking it as deprecated allows consumers time to migrate at their own pace. Once all consumers have transitioned away from the field, it can be safely removed.&lt;/p&gt;

&lt;p&gt;This gradual approach &lt;em&gt;reduces disruption and maintains system stability&lt;/em&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;15. Common Versioning Mistakes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Certain mistakes appear frequently when teams manage schema evolution.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Treating Events Like Internal DTOs&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Internal data transfer objects (DTOs) often change rapidly as implementation details evolve. Public event contracts, however, should be treated with much greater care.&lt;/p&gt;

&lt;p&gt;They represent agreements between services and should not be modified casually.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Releasing Breaking Changes Without Visibility&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Producers often lack visibility into who consumes their events. Making breaking changes without understanding downstream dependencies introduces significant risk.&lt;/p&gt;

&lt;p&gt;Contract testing can help address this issue by providing insight into how events are used.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Versioning Every Small Change&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some teams create new versions for every minor change:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;OrderConfirmedV2&lt;br&gt;
OrderConfirmedV3&lt;br&gt;
OrderConfirmedV4&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In many cases, this is unnecessary. Adding optional fields often preserves compatibility without requiring a new version. Excessive versioning can make systems harder to maintain and understand.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Forgetting That Old Events Continue to Exist&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Events are often stored for long periods and may be replayed for analytics, auditing, or recovery purposes. Schema evolution must account for both new and historical events.&lt;/p&gt;

&lt;p&gt;Even if the schema changes, historical data remains unchanged. Systems must be able to handle both.&lt;/p&gt;




&lt;p&gt;In the next section, we will explore contract testing and how it helps validate these assumptions before changes reach production.&lt;/p&gt;

</description>
      <category>eventdriven</category>
      <category>distributedsystems</category>
      <category>systemdesign</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building Reliable Event-Driven Systems: Event Schemas, Versioning, Contract Testing and Events vs Commands (2/5)</title>
      <dc:creator>Venkatesan Ramar</dc:creator>
      <pubDate>Wed, 15 Jul 2026 05:41:00 +0000</pubDate>
      <link>https://dev.to/morpheus-vera/building-reliable-event-driven-systems-event-schemas-versioning-contract-testing-and-events-vs-28oa</link>
      <guid>https://dev.to/morpheus-vera/building-reliable-event-driven-systems-event-schemas-versioning-contract-testing-and-events-vs-28oa</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;In this article, we're going to explore &lt;strong&gt;&lt;em&gt;Event Schemas&lt;/em&gt;&lt;/strong&gt;. &lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;strong&gt;5. Designing Event Schemas That Age Well&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once a team adopts event-driven architecture, the event schema quickly becomes one of the most critical design artifacts in the system. Unlike an internal Java class that is used within a single codebase, an event schema is consumed by multiple independent services. These services may be developed, deployed, and maintained by different teams, often evolving at different speeds. As a result, every field included in an event effectively becomes part of a contract that consumers may rely on.&lt;/p&gt;

&lt;p&gt;Changing that contract later is rarely as simple as modifying a Java object. While internal models can evolve freely, event schemas must remain stable over time. Good schemas are designed to evolve gracefully, allowing systems to grow without breaking existing consumers. Poorly designed schemas, on the other hand, tend to accumulate compatibility issues that become increasingly difficult to manage.&lt;/p&gt;

&lt;p&gt;At the core of this challenge is a simple but powerful question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is the event describing a business fact or exposing the producer's implementation?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Events Should Represent Business Concepts&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consider an Order Service that has just confirmed an order. One way to represent this event is by focusing on the business outcome:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CUS-501"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CONFIRMED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"totalAmount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;249.99&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another approach might expose internal structures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customerEntity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderAggregate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hibernateVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Although both events may contain similar information, only the first represents a stable business contract. The second leaks internal implementation details that consumers should not depend on. Over time, such exposure creates tight coupling and makes evolution difficult.&lt;/p&gt;

&lt;p&gt;A useful guideline is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Design events for consumers, not for producers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The producer already understands its internal model. Consumers only need clear, meaningful business information.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Don't Serialize Your Domain Model&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A common mistake is publishing JPA entities directly as events. For example:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;eventPublisher.publish(orderEntity);&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This approach may seem convenient because it avoids creating additional classes. However, it tightly couples consumers to the producer’s internal structure. Any change in the domain model can unintentionally break consumers.&lt;/p&gt;

&lt;p&gt;Consider an initial model:&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt; &lt;span class="n"&gt;customer&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;Later, the model evolves:&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;CustomerAccount&lt;/span&gt; &lt;span class="n"&gt;customerAccount&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;From a business perspective, nothing has changed. However, the event payload has changed, potentially breaking consumers. This happens because the event contract was tied directly to the internal model.&lt;/p&gt;

&lt;p&gt;A better approach is to define dedicated event models:&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="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;OrderConfirmedEvent&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;orderId&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;customerId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;totalAmount&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 separation ensures that the event contract remains stable even as the internal model evolves. Both can change independently without affecting each other.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;6. Designing Event Payloads&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every event answers a specific business question. The schema should provide enough information for consumers to understand that answer clearly, without exposing unnecessary implementation details. Striking this balance is one of the most important design decisions in event-driven systems.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Include Business Information&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consider a minimal event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-1001"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While technically valid, it is not very useful. Consumers will likely need additional information, forcing them to make extra API calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderConfirmed Event
         |
         v
Inventory Service
         |
         v
GET /orders/ORD-1001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If multiple consumers follow this pattern, a single event can trigger multiple network requests. This increases system load and introduces unnecessary coupling between services.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Avoid Including Everything&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the other extreme, some events include too much information:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"order"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"payment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"inventory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"shipment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Large payloads can lead to higher network usage, increased serialization costs, and tighter coupling between services. They also make schema evolution more difficult, as changes in one part of the payload may affect multiple consumers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Design Around Business Needs&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A practical approach is to ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What information should every consumer reasonably expect to receive?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For an &lt;code&gt;OrderConfirmed&lt;/code&gt; event, a balanced payload might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CUS-501"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"orderDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-01T10:15:30Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"totalAmount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;249.99&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This provides essential business context without overwhelming consumers. Those who need additional details can fetch them independently, while others remain unaffected.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;7. Event Metadata Matters&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;While developers often focus on the payload, metadata plays an equally important role in production systems. Metadata provides critical context that helps systems understand how to process and trace events.&lt;/p&gt;

&lt;p&gt;It enables systems to determine when an event occurred, where it originated, how it should be tracked, and whether it has already been processed. Without this information, operating and debugging event-driven systems becomes significantly more challenging.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Business Data vs Technical Metadata&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Business data should reside in the payload, while technical details belong in metadata. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eventId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8c1e6d12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eventType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OrderConfirmed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eventVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"occurredAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-07-01T10:15:30Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"correlationId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REQ-98451"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CUS-501"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"totalAmount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;249.99&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation improves clarity and makes it easier to evolve both business data and technical metadata independently.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Event Identifier&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every event should include a unique identifier: &lt;code&gt;eventId&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This identifier is essential for de-duplication, ensuring idempotent processing, enabling tracing, and supporting auditing. It also simplifies handling scenarios where events may be delivered multiple times.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Correlation Identifier&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In distributed systems, workflows often span multiple services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create Order
      |
Reserve Inventory
      |
Process Payment
      |
Create Shipment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step may produce additional events. A correlation identifier links these events together:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Correlation ID&lt;br&gt;
REQ-98451&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This makes it much easier to trace workflows and debug issues in production environments.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Event Timestamp&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Events should record when the business action occurred, not when the event was received. These timestamps can differ due to network delays, retries, or temporary failures. Keeping business time separate from delivery time ensures accurate interpretation of events.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;8. Naming Events Consistently&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Naming may seem like a minor detail, but it becomes increasingly important as systems grow. Large organizations may produce hundreds of event types, and consistency helps maintain clarity and usability across teams.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Prefer Past-Tense Business Events&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Effective event names describe completed business actions:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CustomerRegistered&lt;br&gt;
OrderConfirmed&lt;br&gt;
InventoryReserved&lt;br&gt;
PaymentCompleted&lt;br&gt;
ShipmentCreated&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;These names clearly communicate what has happened, making them easy for consumers to understand.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Avoid CRUD-Oriented Events&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Generic names such as:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;OrderUpdated&lt;br&gt;
CustomerModified&lt;br&gt;
ProductChanged&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;lack clarity. They do not explain what changed or why, forcing consumers to inspect the payload for meaning. Event names should convey intent directly.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Keep Names Business-Oriented&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Avoid technical or implementation-focused names:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;DatabaseUpdated&lt;br&gt;
RowInserted&lt;br&gt;
EntitySaved&lt;br&gt;
JpaEntityUpdated&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;These describe internal processes rather than business outcomes. Consumers care about what happened in the business domain, not how it was implemented.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;9. Common Schema Design Mistakes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Despite differences in technology, many event-driven systems encounter similar design issues. Recognizing these common mistakes can help teams avoid long-term problems.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Publishing Internal Objects&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Internal models change frequently, while public contracts should remain stable. Mixing the two leads to fragile systems. Keeping them separate ensures better maintainability.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Making Events Too Generic&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An event like: &lt;code&gt;OrderUpdated&lt;/code&gt; can represent many different actions, making it difficult for consumers to interpret. More specific events provide clearer intent:&lt;br&gt;
&lt;code&gt;OrderConfirmed&lt;br&gt;
OrderCancelled&lt;br&gt;
OrderRefunded&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This clarity simplifies consumer logic and improves overall system understanding.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Missing Metadata&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without essential metadata such as event identifiers, timestamps, and correlation IDs, troubleshooting becomes significantly harder. Operational concerns should be considered from the beginning of event design.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Designing for Today's Consumers&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many producers design events based only on current consumers, overlooking future needs. A better approach is:&lt;/p&gt;

&lt;p&gt;Design events as though the next consumer has not been written yet.&lt;/p&gt;

&lt;p&gt;This mindset encourages more flexible and future-proof designs.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Practical Rule of Thumb&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Well-designed schemas remain clear and understandable long after they are introduced. They focus on business facts rather than implementation details, separate metadata from payload data, and provide sufficient context without unnecessary complexity.&lt;/p&gt;




&lt;p&gt;In the next part, we will explore schema evolution, event versioning, backward compatibility, and strategies that allow producers and consumers to evolve independently without disrupting production systems.&lt;/p&gt;

&lt;p&gt;Assisted AI to paraphrase the content. &lt;/p&gt;

</description>
      <category>eventdriven</category>
      <category>distributedsystems</category>
      <category>systemdesign</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building Reliable Event-Driven Systems: Event Schemas, Versioning, Contract Testing and Events vs Commands (1/5)</title>
      <dc:creator>Venkatesan Ramar</dc:creator>
      <pubDate>Tue, 14 Jul 2026 05:00:00 +0000</pubDate>
      <link>https://dev.to/morpheus-vera/building-reliable-event-driven-systems-event-schemas-versioning-contract-testing-and-events-vs-37eg</link>
      <guid>https://dev.to/morpheus-vera/building-reliable-event-driven-systems-event-schemas-versioning-contract-testing-and-events-vs-37eg</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;As part-1 of a multi-part series, in this article we'll explore why and where event-driven systems fail and foundational concepts. &lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;Distributed systems have become considerably easier to build than they were a decade ago. Modern frameworks allow us to publish events with only a few lines of code, cloud platforms provide fully managed messaging services, and frameworks like Spring Boot makes asynchronous communication feel almost effortless. Because of this, many teams successfully adopt event-driven architectures. Unfortunately, publishing events is usually the easiest part of the journey. &lt;/p&gt;

&lt;p&gt;Designing events that remain reliable for years is considerably more difficult. Most production problems in event-driven systems are rarely caused by the messaging infrastructure itself. Instead, they originate from architectural questions such as: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;What information an event should contain?&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Whether an event schema can evolve without breaking existing consumers?&lt;/em&gt; &lt;br&gt;
&lt;em&gt;How new services safely consume old events?&lt;/em&gt; &lt;br&gt;
&lt;em&gt;When a service should publish an event instead of sending a command?&lt;/em&gt; &lt;br&gt;
&lt;em&gt;How producers can know they haven't broken downstream consumers?&lt;/em&gt; &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These questions determine whether an event-driven system remains maintainable as more services, teams, and business capabilities are added. &lt;/p&gt;

&lt;p&gt;This article explores the practices that make event-driven systems resilient over time. We focus on the contracts that services exchange with each other. In event-driven architecture, events become public APIs, and unlike REST APIs, those APIs are usually consumed by systems the producer does not directly control. It makes compatibility one of the most important design concerns.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;1. Event-Driven Architecture Is Really About Contracts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Many developers describe event-driven architecture as services communicating through events. That description is correct, but it is also incomplete. Events are more than messages moving between services—every published event represents a contract.&lt;/p&gt;

&lt;p&gt;Suppose an Order Service publishes the following event.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"customerId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CUS-501"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CONFIRMED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"totalAmount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;249.99&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Several services subscribe to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 OrderConfirmed
                        |
        +---------------+---------------+
        |               |               |
        v               v               v
 Inventory Service  Billing Service  Notification Service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The producer may know about three consumers today. Six months later, another team builds additional services such as Analytics, Recommendation, and Customer Loyalty. The producer does not need to change; the consumers simply subscribe. This loose coupling is one of the biggest strengths of event-driven systems, but it is also one of their biggest challenges.&lt;/p&gt;

&lt;p&gt;The producer no longer knows who depends on the event, which makes changing the event significantly more complicated than changing an internal Java object.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Events Are Public APIs&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most teams treat REST APIs very carefully. Before removing a field, they consider existing clients, API versions, backward compatibility, and migration plans. Events deserve exactly the same level of discipline.&lt;/p&gt;

&lt;p&gt;Once an event is published, it becomes part of the public interface of the service. Removing a field from an event can break downstream systems just as easily as removing a field from a REST response. The difference is that consumers are often invisible. A REST API usually has documented clients, while an event may have consumers owned by completely different teams, some of which may not even exist when the producer is originally developed.&lt;/p&gt;

&lt;p&gt;Thinking of events as contracts fundamentally changes how they should be designed.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;2. Why Event Design Matters More Than Event Publishing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Publishing an event is a technical task, but designing an event is an architectural task. Many event-driven projects begin with something like this:&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&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;OrderItem&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Address&lt;/span&gt; &lt;span class="n"&gt;shippingAddress&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 easiest approach is to serialize the entire object and publish it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;eventPublisher.publish(order);&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;It works—until the domain model changes. A field gets renamed, an object is restructured, or a new relationship is introduced. Every consumer now receives a different payload. The producer evolved, but the contract changed accidentally.&lt;/p&gt;

&lt;p&gt;This is one of the most common mistakes in event-driven systems. Events should not expose internal domain models; they should communicate business facts. Those are two very different things.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;An Event Describes Something That Already Happened&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful mental model is that a command asks for something to happen, while an event states that something already happened. For example:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;OrderConfirmed&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is a business fact. It cannot be rejected because it has already occurred. Similarly:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PaymentCompleted&lt;br&gt;
InventoryReserved&lt;br&gt;
ShipmentCreated&lt;br&gt;
CustomerRegistered&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;All describe completed business actions. &lt;/p&gt;

&lt;p&gt;Consumers should be able to trust that these events represent facts. This makes event names extremely important. Good names communicate completed business outcomes, while poor names often expose implementation details. Compare the following examples:&lt;/p&gt;

&lt;p&gt;Good: &lt;code&gt;OrderConfirmed&lt;/code&gt;&lt;br&gt;
Poor: &lt;code&gt;UpdateOrderStatus&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The first describes something that happened, while the second sounds like an &lt;em&gt;internal method call&lt;/em&gt;. This distinction becomes increasingly important as systems grow.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;3. Events Are Immutable&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One characteristic separates events from many other forms of communication: events are immutable. Once published, an event represents history, and history cannot be rewritten.&lt;/p&gt;

&lt;p&gt;Imagine the following event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CONFIRMED"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tomorrow, the customer cancels the order. The producer should not update the previous event. Instead, it publishes a new one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CANCELLED"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The event stream now tells a complete story:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OrderCreated
      |
OrderConfirmed
      |
OrderCancelled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consumers joining later can reconstruct what happened. This is one of the reasons event-driven systems are valuable—events become an immutable business history. Changing previously published events destroys that history.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Events Represent Facts, Not State&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Another common misunderstanding is treating events as snapshots of current state. Consider this event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ORD-1001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PROCESSING"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Does it mean the order is currently processing, or that the order entered processing&lt;/em&gt;? These are different meanings.&lt;/p&gt;

&lt;p&gt;A better event would be: &lt;code&gt;OrderProcessingStarted&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The name clearly communicates a business fact. Consumers no longer need to interpret the payload because the event itself explains what happened. As event catalogs grow, this principle becomes increasingly important. Well-designed event names reduce ambiguity, while poorly named events force consumers to infer business meaning.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;4. Events and Commands Are Not the Same&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is one of the most misunderstood topics in event-driven architecture. Many teams use commands and events interchangeably, which creates tightly coupled systems. Understanding the difference changes how services communicate.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Commands Express Intent&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A command represents a request where the sender expects another service to perform an action. &lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;code&gt;ReserveInventory&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The sender is asking, “Please reserve inventory.” The receiving service can accept it, reject it, validate it, or return an error. &lt;/p&gt;

&lt;p&gt;Commands imply responsibility—someone is expected to do something.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Events Express Facts&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An event communicates that something has already happened. &lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;code&gt;InventoryReserved&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The reservation has already completed. Consumers cannot reject it; they simply react. This difference may appear subtle, but architecturally it is enormous. Commands influence behavior, while events communicate history.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Visualizing the Difference&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Command&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order Service
      |
Reserve Inventory
      |
      v
Inventory Service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Order Service knows exactly who should process the request, making the communication directed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Event&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;InventoryReserved
        |
   +----+----+---------+
   |         |         |
   v         v         v
Billing   Shipping   Analytics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Inventory Service simply publishes a business fact. It does not know who reacts, and it does not need to. That is the essence of loose coupling.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;A Practical Rule of Thumb&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One guideline that helps during system design reviews is simple: use a &lt;strong&gt;command&lt;/strong&gt; when &lt;em&gt;one specific service is responsible for performing an action&lt;/em&gt;, and use an &lt;strong&gt;event&lt;/strong&gt; when &lt;em&gt;informing any interested service that a business fact has already occurred&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This distinction keeps responsibilities clear and prevents event-driven systems from gradually turning into distributed RPC systems disguised as messaging.&lt;/p&gt;




&lt;p&gt;In the next part, we will build on these foundations by designing event schemas that survive years of system evolution.&lt;/p&gt;

&lt;p&gt;Assisted AI to paraphrase. &lt;/p&gt;

</description>
      <category>eventdriven</category>
      <category>distributedsystems</category>
      <category>systemdesign</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>JVM Internals for Microservices: Classloading, Memory, and GC in Containers</title>
      <dc:creator>Venkatesan Ramar</dc:creator>
      <pubDate>Wed, 08 Jul 2026 23:34:52 +0000</pubDate>
      <link>https://dev.to/morpheus-vera/jvm-internals-for-microservices-classloading-memory-and-gc-in-containers-222h</link>
      <guid>https://dev.to/morpheus-vera/jvm-internals-for-microservices-classloading-memory-and-gc-in-containers-222h</guid>
      <description>&lt;p&gt;A few years ago, an iPaaS platform I worked started experiencing intermittent pod restarts in Kubernetes. The issue initially appeared unrelated to the integration workloads themselves. Customer integrations were processing messages successfully, API response times remained stable, and the platform showed no obvious signs of distress. Yet several integration runtime pods were being restarted multiple times a day.&lt;/p&gt;

&lt;p&gt;At first, the investigation focused on application-level concerns. Since the platform handled large volumes of transformation logic, message routing, and connector execution, the assumption was that some integration flow was creating excessive object allocations or causing a memory leak. Heap utilization, however, remained well below the configured limits. Garbage collection logs looked healthy, and profiling revealed no significant retention issues.&lt;/p&gt;

&lt;p&gt;The actual problem was hidden outside the heap. The JVM was consuming memory from multiple sources that were not visible in the standard application dashboards. Metaspace grew as connectors, SDKs, and framework components loaded thousands of classes. Thread stacks accumulated because integration runtimes maintained pools for message processing, scheduling, and external system communication. Direct buffers allocated by networking libraries consumed native memory. Garbage collector metadata and JVM internal structures added additional overhead.&lt;/p&gt;

&lt;p&gt;Individually, none of these memory consumers appeared problematic. Together, they pushed the process beyond the container's memory limit.&lt;/p&gt;

&lt;p&gt;The JVM monitoring tools showed healthy heap usage. Kubernetes, however, cared only about the total memory consumed by the process. Once the runtime exceeded its container limit, the Linux kernel terminated it, and Kubernetes restarted the pod.&lt;/p&gt;

&lt;p&gt;That incident highlighted an important reality of modern B2B integration platforms. Many production issues are not caused by transformation logic, connector implementations, or external system latency. Instead, they originate from misunderstandings about how the JVM behaves inside containers.&lt;/p&gt;

&lt;p&gt;Developers building integration services often focus on workflows, mappings, APIs, and connectivity while treating the JVM as a black box. In traditional deployment environments, that approach was often sufficient. Modern cloud-native integration platforms operate under very different constraints. Memory limits are tighter, startup times affect autoscaling behavior, workloads are highly dynamic, and infrastructure efficiency directly impacts operating costs.&lt;/p&gt;

&lt;p&gt;Modern Java runtimes have improved dramatically over the last decade. Java 21 running inside Kubernetes behaves very differently from Java 8 running on dedicated virtual machines. Nevertheless, understanding a few key JVM internals remains essential for building efficient and reliable integration services.&lt;/p&gt;

&lt;p&gt;This article focuses on the JVM concepts that matter most in containerized environments: memory layout, classloading, and the relationship between JVM behavior and container resource limits.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;1. Why JVM Internals Matter in Microservices&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Many JVM concepts were easy to ignore in traditional monolithic deployments because infrastructure resources were abundant. A typical enterprise application might run as a single JVM process with access to large amounts of memory and CPU resources.&lt;/p&gt;

&lt;p&gt;1 JVM&lt;br&gt;
16 GB RAM&lt;br&gt;
32 CPUs&lt;/p&gt;

&lt;p&gt;In such environments, inefficiencies often remained hidden. An application consuming an extra few hundred megabytes of memory rarely caused operational problems. Startup times were less important because deployments happened infrequently. Thread counts could grow significantly without immediately impacting system stability.&lt;/p&gt;

&lt;p&gt;The cloud-native world operates under a completely different set of assumptions.&lt;/p&gt;

&lt;p&gt;50 Services&lt;br&gt;
512 MB each&lt;/p&gt;

&lt;p&gt;Instead of one large application, organizations often deploy dozens or hundreds of smaller services. Each service operates within strict resource boundaries. Every deployment has memory limits, CPU quotas, startup requirements, autoscaling behavior, and infrastructure costs associated with it.&lt;/p&gt;

&lt;p&gt;As a result, inefficiencies that were previously insignificant become highly visible.&lt;/p&gt;

&lt;p&gt;A service wasting 100 MB of memory may not seem problematic in isolation. However, when that same inefficiency exists across one hundred services, the organization is effectively allocating an additional 10 GB of memory simply to support overhead.&lt;/p&gt;

&lt;p&gt;10 GB of unnecessary memory allocation&lt;/p&gt;

&lt;p&gt;At scale, these inefficiencies translate directly into infrastructure costs, operational complexity, and reduced platform efficiency.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cloud Cost Becomes a JVM Problem&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Organizations often invest heavily in optimizing databases, networking infrastructure, and cloud architecture. Surprisingly, the JVM itself is frequently overlooked despite being one of the largest consumers of compute resources in many enterprise environments.&lt;/p&gt;

&lt;p&gt;Consider two services that deliver identical throughput and latency. One requires 2 GB of memory while the other requires only 1 GB.&lt;/p&gt;

&lt;p&gt;2 GB memory&lt;/p&gt;

&lt;p&gt;versus&lt;/p&gt;

&lt;p&gt;1 GB memory&lt;/p&gt;

&lt;p&gt;From a business perspective, the second service is significantly more efficient. Across dozens of deployments, the difference can represent substantial infrastructure savings.&lt;/p&gt;

&lt;p&gt;This means JVM tuning is no longer merely a performance concern. It becomes an architectural and financial concern as well.&lt;/p&gt;

&lt;p&gt;Memory efficiency directly influences cloud spending.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scaling Magnifies JVM Decisions&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many JVM-related decisions appear harmless during development because developers typically run a single service on a powerful workstation.&lt;/p&gt;

&lt;p&gt;1 Service&lt;/p&gt;

&lt;p&gt;Production environments tell a different story.&lt;/p&gt;

&lt;p&gt;50+ Services&lt;/p&gt;

&lt;p&gt;Every JVM incurs overhead. Every service loads classes. Every service allocates memory for threads. Every service performs garbage collection.&lt;/p&gt;

&lt;p&gt;When multiplied across an entire platform, these costs become significant.&lt;/p&gt;

&lt;p&gt;Classloading overhead scales.&lt;br&gt;
Memory overhead scales.&lt;br&gt;
Thread overhead scales.&lt;br&gt;
Garbage collection overhead scales.&lt;/p&gt;

&lt;p&gt;Understanding JVM internals becomes increasingly important as systems grow.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;2. The JVM Memory Model Most Developers Never See&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When developers think about JVM memory, they usually think about one thing: &lt;code&gt;Heap&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The heap is certainly important because it stores application objects and is the primary target of garbage collection. However, the heap represents only one portion of the JVM's memory footprint.&lt;/p&gt;

&lt;p&gt;This distinction becomes critical in containerized environments because Kubernetes and operating systems measure total process memory consumption rather than heap usage alone.&lt;/p&gt;

&lt;p&gt;A service can have perfectly healthy heap utilization and still be terminated due to memory pressure.&lt;/p&gt;

&lt;p&gt;Understanding where memory is allocated inside the JVM helps explain why this happens.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Heap Memory&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The heap stores the majority of application objects created during execution.&lt;/p&gt;

&lt;p&gt;Examples include:&lt;/p&gt;

&lt;p&gt;Order order = new Order();&lt;br&gt;
List customers = ...&lt;br&gt;
Map cache = ...&lt;/p&gt;

&lt;p&gt;Whenever objects are instantiated, memory is typically allocated within the heap.&lt;/p&gt;

&lt;p&gt;Modern garbage collectors organize the heap into multiple regions or generations. Although implementation details vary between collectors, the general concepts remain familiar.&lt;/p&gt;

&lt;p&gt;Common concepts are:&lt;br&gt;
Young Generation and Old Generation&lt;/p&gt;

&lt;p&gt;Most objects are short-lived. They are created, used briefly, and then discarded. These objects typically remain in the young generation.&lt;/p&gt;

&lt;p&gt;Objects that survive multiple garbage collection cycles are eventually promoted into the old generation, where they remain for longer periods.&lt;/p&gt;

&lt;p&gt;Heap memory is usually the easiest JVM memory area to monitor because most observability tools expose heap metrics by default.&lt;/p&gt;

&lt;p&gt;Unfortunately, this visibility often creates the misconception that heap memory represents total JVM memory consumption.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Metaspace&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Starting with Java 8, Metaspace replaced the older Permanent Generation (PermGen).&lt;/p&gt;

&lt;p&gt;Metaspace stores information about loaded classes, methods, fields, and other runtime metadata required by the JVM.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
class metadata&lt;br&gt;
method metadata&lt;br&gt;
runtime class information&lt;/p&gt;

&lt;p&gt;Modern Spring applications load thousands of classes during startup.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
Spring Framework&lt;br&gt;
Spring Boot&lt;br&gt;
Hibernate&lt;br&gt;
Jackson&lt;br&gt;
Logging libraries&lt;br&gt;
Database drivers&lt;/p&gt;

&lt;p&gt;In addition to framework classes, many frameworks generate classes dynamically at runtime.&lt;/p&gt;

&lt;p&gt;For instance:&lt;br&gt;
Spring proxies&lt;br&gt;
AOP proxies&lt;br&gt;
Hibernate proxies&lt;br&gt;
Bytecode-enhanced entities&lt;/p&gt;

&lt;p&gt;Every loaded class consumes Metaspace.&lt;/p&gt;

&lt;p&gt;Large enterprise applications can easily load several thousand classes before processing their first request. As a result, Metaspace can become a meaningful contributor to overall memory consumption.&lt;/p&gt;

&lt;p&gt;Unlike heap memory, Metaspace often receives little attention until it becomes a problem.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Thread Stacks&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every platform thread created by the JVM receives its own stack.&lt;/p&gt;

&lt;p&gt;Typical stack sizes range between 256 KB – 1 MB depending on operating system and JVM configuration.&lt;/p&gt;

&lt;p&gt;This memory is allocated independently of the heap.&lt;/p&gt;

&lt;p&gt;The impact becomes significant when applications create large numbers of threads.&lt;/p&gt;

&lt;p&gt;Consider a service configured with &lt;em&gt;500 Threads&lt;/em&gt; and a stack size of &lt;em&gt;1 MB&lt;/em&gt;. Thread stacks alone may consume &lt;em&gt;500 MB&lt;/em&gt; before accounting for application objects, caches, or framework overhead.&lt;/p&gt;

&lt;p&gt;This is one reason thread-heavy applications often consume substantially more memory than expected.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Native Memory&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Native memory is one of the least understood areas of JVM memory management.&lt;/p&gt;

&lt;p&gt;The JVM frequently allocates memory outside the heap for performance reasons.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
NIO buffers&lt;br&gt;
JNI libraries&lt;br&gt;
Compression libraries&lt;br&gt;
GC metadata&lt;br&gt;
JVM internal structures&lt;/p&gt;

&lt;p&gt;These allocations do not appear in heap metrics.&lt;/p&gt;

&lt;p&gt;From the perspective of many monitoring dashboards, this memory is effectively invisible.&lt;/p&gt;

&lt;p&gt;However, the operating system and Kubernetes still count it toward the process memory limit.&lt;/p&gt;

&lt;p&gt;As a result, applications can experience memory-related failures even when heap utilization appears completely normal.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Memory Layout That Kubernetes Sees&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kubernetes does not distinguish between heap memory, Metaspace, thread stacks, or native allocations.&lt;/p&gt;

&lt;p&gt;It sees a single process consuming memory.&lt;/p&gt;

&lt;p&gt;A simplified representation looks like this:&lt;/p&gt;

&lt;p&gt;JVM Process Memory&lt;br&gt;
|&lt;br&gt;
|-- Heap&lt;br&gt;
|-- Metaspace&lt;br&gt;
|-- Thread Stacks&lt;br&gt;
|-- Direct Buffers&lt;br&gt;
|-- GC Structures&lt;br&gt;
|-- Native Memory&lt;/p&gt;

&lt;p&gt;The container limit applies to the entire process, not just the heap.&lt;/p&gt;

&lt;p&gt;This distinction explains many seemingly mysterious OOMKill incidents.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;3. Classloading: The Invisible Startup Cost&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Classloading is one of the most fundamental JVM mechanisms, yet most developers rarely think about it.&lt;/p&gt;

&lt;p&gt;Every Java application depends on classloading. Every framework depends on classloading. Every object created by the application ultimately relies on classes being loaded into memory.&lt;/p&gt;

&lt;p&gt;Despite its importance, classloading remains largely invisible during day-to-day development. Its effects, however, are highly visible.&lt;/p&gt;

&lt;p&gt;Startup time, memory consumption, deployment speed, and auto-scaling responsiveness are all influenced by classloading behavior.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What Happens During Startup&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a Spring Boot application starts, the JVM performs a series of operations before the application becomes available.&lt;/p&gt;

&lt;p&gt;A simplified view looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Class Loading
     |
Verification
     |
Initialization
     |
Bean Creation
     |
Application Ready
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thousands of classes may be loaded and initialized during this process. Each class must be located, verified, linked, and prepared for execution. The larger the application and dependency graph, the more work the JVM must perform before serving traffic.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why Spring Applications Load So Many Classes&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Modern Spring applications rely heavily on framework capabilities such as dependency injection, reflection, annotations, auto-configuration, and proxy generation.&lt;/p&gt;

&lt;p&gt;These features provide tremendous developer productivity but introduce startup overhead.&lt;/p&gt;

&lt;p&gt;Common contributors include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dependency injection&lt;/li&gt;
&lt;li&gt;reflection&lt;/li&gt;
&lt;li&gt;annotations&lt;/li&gt;
&lt;li&gt;auto-configuration&lt;/li&gt;
&lt;li&gt;proxy generation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even a seemingly simple dependency can trigger substantial framework activity.&lt;/p&gt;

&lt;p&gt;For example, &lt;em&gt;spring-boot-starter-web&lt;/em&gt; does far more than provide an embedded web server.&lt;/p&gt;

&lt;p&gt;Spring performs extensive classpath scanning, conditional configuration evaluation, bean registration, and framework initialization.&lt;/p&gt;

&lt;p&gt;As applications grow, startup complexity grows as well.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Classloader Hierarchy&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The JVM organizes classloading through a hierarchy of classloaders.&lt;/p&gt;

&lt;p&gt;Most applications rely on three primary classloaders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bootstrap ClassLoader
          |
Platform ClassLoader
          |
Application ClassLoader
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Bootstrap ClassLoader loads core JDK classes such as &lt;code&gt;java.lang.String, java.util.List&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The Application ClassLoader loads application-specific dependencies and business code like: &lt;/p&gt;

&lt;p&gt;Spring&lt;br&gt;
Hibernate&lt;br&gt;
Business code&lt;/p&gt;

&lt;p&gt;Understanding this hierarchy becomes important when diagnosing dependency conflicts, startup failures, or class visibility issues. Many difficult startup problems ultimately originate from classloading behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why Classloading Matters in Containers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Classloading directly affects startup performance.&lt;/p&gt;

&lt;p&gt;In traditional environments, startup time might not matter significantly because applications remained running for weeks or months.&lt;/p&gt;

&lt;p&gt;Containerized environments are different.&lt;/p&gt;

&lt;p&gt;Auto-scaling events, rolling deployments, and node failures can trigger frequent application startups.&lt;/p&gt;

&lt;p&gt;Consider a deployment scaling from: &lt;em&gt;5 Pods to 50 Pods&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every new pod must complete startup before it can serve traffic.&lt;br&gt;
Classloading delays become deployment delays.&lt;/p&gt;

&lt;p&gt;Large applications may spend a surprising amount of time loading classes and initializing frameworks before becoming operational. For highly dynamic environments, startup efficiency becomes an important operational characteristic.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;4. Memory in Containers: Where Reality Gets Interesting&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Many JVM misconceptions become apparent only after applications move into containers.&lt;/p&gt;

&lt;p&gt;Historically, the JVM was designed for physical servers and virtual machines where resource boundaries were relatively straightforward.&lt;/p&gt;

&lt;p&gt;Containers introduced a new abstraction layer that changed how resources are allocated and enforced.&lt;/p&gt;

&lt;p&gt;These changes exposed assumptions that were previously hidden.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The JVM Was Not Originally Container-Aware&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Older JVM versions viewed the environment primarily through the perspective of the host machine.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;64 GB Host&lt;/p&gt;

&lt;p&gt;The JVM assumed those resources were available.&lt;/p&gt;

&lt;p&gt;Containers introduced a different reality:&lt;/p&gt;

&lt;p&gt;64 GB Host&lt;br&gt;
|&lt;br&gt;
512 MB Container&lt;/p&gt;

&lt;p&gt;The application could access only a small fraction of the host's resources.&lt;/p&gt;

&lt;p&gt;Early JVM versions frequently sized memory pools based on host capacity rather than container limits.&lt;/p&gt;

&lt;p&gt;This behavior caused numerous production issues in Kubernetes environments.&lt;/p&gt;

&lt;p&gt;Modern JVMs have become significantly more container-aware, but understanding the historical context helps explain many configuration recommendations.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Heap Is Not Total Memory&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of the most common mistakes in containerized Java deployments is allocating the entire container budget to the heap.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;Container Limit = 512 MB&lt;br&gt;
Heap = 512 MB&lt;/p&gt;

&lt;p&gt;This configuration leaves no room for any other JVM memory consumers.&lt;/p&gt;

&lt;p&gt;The JVM still requires memory for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Metaspace&lt;/li&gt;
&lt;li&gt;Thread stacks&lt;/li&gt;
&lt;li&gt;Direct memory&lt;/li&gt;
&lt;li&gt;Native allocations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eventually, total process memory exceeds the container limit.&lt;/p&gt;

&lt;p&gt;A healthier configuration might look like:&lt;/p&gt;

&lt;p&gt;Container Limit = 512 MB&lt;/p&gt;

&lt;p&gt;Heap = 300 MB&lt;br&gt;
Remaining Memory = JVM Overhead&lt;/p&gt;

&lt;p&gt;The exact allocation depends on workload characteristics, but the principle remains universal.&lt;/p&gt;

&lt;p&gt;The heap cannot consume the entire memory budget.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why Pods Get OOMKilled&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many production incidents follow a similar pattern.&lt;/p&gt;

&lt;p&gt;Consider the following memory breakdown:&lt;/p&gt;

&lt;p&gt;Container Limit = 512 MB&lt;/p&gt;

&lt;p&gt;Heap = 320 MB&lt;br&gt;
Metaspace = 80 MB&lt;br&gt;
Native = 90 MB&lt;br&gt;
Thread Stacks = 50 MB&lt;/p&gt;

&lt;p&gt;Total = 540 MB&lt;/p&gt;

&lt;p&gt;From the JVM's perspective, &lt;br&gt;
heap utilization may appear healthy.&lt;br&gt;
Garbage collection may appear healthy.&lt;br&gt;
Application latency may appear healthy.&lt;/p&gt;

&lt;p&gt;Yet, Kubernetes observes only one fact:&lt;/p&gt;

&lt;p&gt;540 MB &amp;gt; 512 MB&lt;/p&gt;

&lt;p&gt;The process exceeds its memory limit. The operating system terminates it.&lt;/p&gt;

&lt;p&gt;Kubernetes restarts the pod.&lt;/p&gt;

&lt;p&gt;This behavior often surprises some because traditional JVM monitoring focuses heavily on heap metrics while ignoring other memory consumers.&lt;/p&gt;

&lt;p&gt;In many real-world incidents, understanding total JVM memory consumption provides far more value than tuning garbage collection parameters.&lt;/p&gt;

&lt;p&gt;Before optimizing GC, it is often worth ensuring that the JVM's complete memory footprint actually fits within the container budget.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;5. Garbage Collection Choices That Matter&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Garbage Collection discussions often become overly theoretical. Many articles spend significant time explaining concepts such as mark-and-sweep algorithms, generational memory management, compaction strategies, and collector internals. While these topics are important for understanding how the JVM works, most backend teams are usually trying to answer more practical questions. They want to know which garbage collector they should use, what problems it solves, when it makes sense to move away from the default configuration, and how to determine whether garbage collection is actually responsible for a performance issue.&lt;/p&gt;

&lt;p&gt;Modern Java has already made many sensible decisions on behalf of developers. For most microservices, the default collector is an excellent starting point. The real challenge is understanding when the default stops being sufficient and what trade-offs alternative collectors introduce. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What Garbage Collection Is Optimizing&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every garbage collector attempts to balance three competing goals: throughput, latency, and memory efficiency. Improving one of these dimensions often comes at the expense of another.&lt;/p&gt;

&lt;p&gt;A collector optimized for throughput may allow longer stop-the-world pauses because it prioritizes maximizing the amount of useful application work completed over time. This approach can be highly efficient for batch workloads but may introduce noticeable pauses that affect user-facing applications.&lt;/p&gt;

&lt;p&gt;A latency-focused collector takes a different approach. Instead of maximizing throughput, it attempts to minimize pause times by performing more work concurrently with application threads. This keeps applications responsive but consumes additional CPU resources because the collector remains active while the application is running.&lt;/p&gt;

&lt;p&gt;Memory efficiency introduces yet another dimension. Some collectors require additional metadata structures, forwarding information, or concurrent processing overhead to achieve lower pause times. As a result, reducing latency may increase memory consumption or CPU utilization.&lt;/p&gt;

&lt;p&gt;Conceptually, the trade-off looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 Throughput
                      ▲
                      │
Memory Efficiency ◄───┼───► Latency
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Moving closer to one corner generally means moving farther away from another. There is no universally optimal collector because every &lt;em&gt;workload has different priorities&lt;/em&gt;. The best choice depends on the characteristics of the application being deployed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Understanding Generational Collection&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before discussing specific collectors, it is useful to understand why modern JVMs organize memory into generations. This design is based on a simple observation: most objects in Java die young.&lt;/p&gt;

&lt;p&gt;Objects such as HTTP request wrappers, DTOs, temporary collections, serialization buffers, and JSON parsing structures are often created and discarded within milliseconds. A single request may allocate thousands of short-lived objects that become unreachable immediately after the response is returned.&lt;/p&gt;

&lt;p&gt;Because of this behavior, the JVM separates memory into regions optimized for different object lifetimes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Young Generation
      |
Most Objects Die
      |
      v
Old Generation
      |
Long-Lived Objects
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The young generation is collected frequently because reclaiming memory there is usually inexpensive. Objects that survive multiple collection cycles are promoted into the old generation, where they are assumed to have longer lifetimes.&lt;/p&gt;

&lt;p&gt;Many memory-related problems begin when allocation rates become excessive, objects survive longer than expected, or old-generation growth becomes continuous. In practice, understanding object lifetime patterns is often more valuable than switching collectors because many performance issues originate from application behavior rather than collector choice.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Class Data Sharing (CDS)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Class Data Sharing is one of the least discussed JVM optimizations despite its practical value.&lt;/p&gt;

&lt;p&gt;Normally, every JVM process loads and processes core JDK classes independently. CDS allows pre-processed class metadata to be stored in a shared archive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JDK Classes
      |
Create Archive
      |
Shared By JVMs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reduces startup time, lowers memory consumption, and improves class-loading efficiency. Multiple JVM processes can reuse the same archive, making CDS particularly valuable in containerized environments where many identical workloads run simultaneously.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Application CDS&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Application CDS extends the same concept beyond JDK classes.&lt;/p&gt;

&lt;p&gt;Instead of sharing only core platform classes, applications can include framework classes, third-party libraries, and application-specific code within the archive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Application Classes
        |
Generate Archive
        |
Reuse During Startup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As application size grows, the benefits become increasingly noticeable. Large Spring Boot applications can often achieve meaningful startup improvements through Application CDS.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AOT and Native Images&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Spring and GraalVM introduced another strategy: moving work from runtime to build time.&lt;/p&gt;

&lt;p&gt;Instead of performing extensive runtime analysis, applications can be compiled ahead of time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build Time Analysis
        |
Generate Native Binary
        |
Fast Startup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach delivers faster startup times, lower memory footprints, and reduced runtime initialization overhead.&lt;/p&gt;

&lt;p&gt;The trade-offs include longer build times, reduced JVM dynamism, reflection constraints, and additional operational complexity. Native images solve a different set of problems than traditional JVM tuning, and many organizations achieve acceptable startup performance without leaving the JVM ecosystem.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;6. Observability: What To Monitor&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Many JVM tuning efforts fail because teams focus on the wrong metrics. Heap utilization alone rarely tells the complete story.&lt;/p&gt;

&lt;p&gt;Effective JVM observability requires visibility into memory behavior, allocation patterns, garbage collection activity, and runtime characteristics.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Heap Metrics&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Teams should monitor heap used, heap committed, and heap maximum values. These metrics help answer important questions.&lt;/p&gt;

&lt;p&gt;Is heap usage growing continuously? Are objects surviving longer than expected? Are caches oversized? Does memory return to normal levels after traffic decreases?&lt;/p&gt;

&lt;p&gt;A healthy pattern often resembles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Heap Usage
    /\
   /  \
  /    \
 /      \
----------
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Memory grows and shrinks as collections occur.&lt;/p&gt;

&lt;p&gt;A problematic pattern looks more like:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Continuous growth may indicate memory leaks, retained references, or unbounded caches. Heap metrics are often the fastest way to identify memory-related issues.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GC Metrics&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Garbage collection metrics deserve equal attention. Teams should monitor pause duration, collection frequency, allocation rate, and promotion rate.&lt;/p&gt;

&lt;p&gt;Questions worth asking include whether pauses are affecting latency, whether allocation rates are unusually high, whether old-generation occupancy is growing continuously, and whether promotion rates are increasing unexpectedly.&lt;/p&gt;

&lt;p&gt;Allocation rate is particularly important because a service allocating several gigabytes per second may experience GC pressure even when heap utilization appears healthy. Tuning decisions should be based on these metrics rather than assumptions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Container Metrics&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many JVM dashboards stop at heap metrics, but container platforms don't. Teams should monitor RSS memory, container memory usage, memory limits, and OOMKill events.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Container Memory
       |
+------+------+------+
| Heap | Native | OS |
+------+------+------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JVM controls only part of total process memory consumption. Container-level metrics frequently expose issues that remain invisible when observing heap metrics alone.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Startup Metrics&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Startup metrics are increasingly important in Kubernetes, serverless environments, and autoscaling workloads.&lt;/p&gt;

&lt;p&gt;Useful measurements include startup duration, loaded class count, and bean initialization time.&lt;/p&gt;

&lt;p&gt;A startup breakdown often looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Startup Time
      |
      +-- Class Loading
      |
      +-- Spring Initialization
      |
      +-- Bean Creation
      |
      +-- Application Ready
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Startup performance should be treated as production performance because it directly affects deployment and recovery behavior.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;7. Common JVM Mistakes in Microservices&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some JVM-related mistakes appear repeatedly across engineering teams.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Setting Heap Equal To Container Memory&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A common mistake is configuring heap size equal to the container memory limit.&lt;/p&gt;

&lt;p&gt;Container Limit = 1 GB&lt;br&gt;
Heap = 1 GB&lt;/p&gt;

&lt;p&gt;This leaves no room for Metaspace, thread stacks, native memory, direct buffers, or JIT compiler structures.&lt;/p&gt;

&lt;p&gt;Actual process memory consumption includes:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Heap + Metaspace + Native Memory + Thread Stacks&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The result is often container OOMKills. A safer approach reserves sufficient memory for non-heap consumers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ignoring Native Memory&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many teams monitor heap usage while ignoring total process memory.&lt;/p&gt;

&lt;p&gt;Native memory includes thread stacks, direct byte buffers, JNI allocations, GC metadata, and JIT compiler structures. These components can consume substantial memory and frequently explain crashes where heap utilization appears normal.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Premature GC Tuning&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A common troubleshooting pattern looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The application slows down.&lt;/li&gt;
&lt;li&gt;Garbage collection becomes the primary suspect.&lt;/li&gt;
&lt;li&gt;The collector is changed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In many cases, the actual root cause is inefficient queries, memory leaks, oversized caches, or excessive serialization overhead.&lt;/p&gt;

&lt;p&gt;GC tuning should always be evidence-driven. Teams should validate a clear correlation between latency issues and GC pauses before changing collectors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Latency
     |
GC Pause Correlation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Treating Every Service The Same&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Different workloads have different requirements.&lt;/p&gt;

&lt;p&gt;An API gateway is typically latency-sensitive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API Gateway
     |
Latency Sensitive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A batch processor is usually throughput-sensitive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Batch Job
   |
Throughput Sensitive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The gateway may benefit from lower pause times, while the batch processor may prioritize throughput and memory efficiency. JVM decisions should reflect workload characteristics rather than organizational defaults.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Practical Recommendations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A few practical guidelines consistently work well across many environments.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Small Spring Boot Services (&amp;lt; 2 GB)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For smaller services, Java 21, G1GC, and container-aware JVM defaults are usually sufficient.&lt;/p&gt;

&lt;p&gt;Teams should monitor heap usage, RSS memory, and GC pauses while avoiding aggressive tuning. Modern JVM defaults are already highly optimized.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;High-Traffic APIs&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For high-traffic APIs, focus on allocation rate, latency distribution, GC pause behavior, and tail latency.&lt;/p&gt;

&lt;p&gt;A useful investigation flow looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Latency Increase
       |
Check GC Pauses
       |
Check Allocation Rate
       |
Evaluate Collector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If latency requirements justify it, ZGC may be worth evaluating.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Startup-Sensitive Services&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Services that scale frequently should focus on class loading, startup configuration, CDS archives, and bean initialization performance.&lt;/p&gt;

&lt;p&gt;Startup should be measured continuously, and regressions should be treated with the same seriousness as runtime performance regressions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Memory-Constrained Containers&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When operating in memory-constrained environments, reserve capacity for Metaspace, thread stacks, direct buffers, and other native allocations.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Container Limit
       |
       +-- Heap
       |
       +-- Metaspace
       |
       +-- Native Memory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Avoid allocating the entire container budget to the heap. Sufficient headroom is essential for stable operation.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;9. Final Thoughts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Understanding JVM internals is not about memorizing garbage collector algorithms or collecting obscure JVM flags.&lt;/p&gt;

&lt;p&gt;Microservices introduce operational constraints that make certain JVM concepts impossible to ignore. Class loading influences startup behavior. Memory extends far beyond the heap. Garbage collection affects latency, throughput, and resource utilization. Container limits apply to the entire JVM process, not just the heap.&lt;/p&gt;

&lt;p&gt;Most production incidents involving Java services are not caused by obscure JVM bugs. They are usually the result of incorrect assumptions about how the JVM behaves inside modern cloud environments.&lt;/p&gt;

&lt;p&gt;The JVM has become remarkably efficient. Modern collectors such as G1GC and ZGC solve problems that previously required extensive manual tuning. The responsibility of engineering teams is understanding the relatively small set of JVM concepts that directly affect production systems.&lt;/p&gt;

&lt;p&gt;These concepts influence cloud costs, deployment speed, application stability, and operational reliability far more than any collection of JVM tuning flags ever will.&lt;/p&gt;

</description>
      <category>java</category>
      <category>microservices</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Data Consistency Under Contention: Optimistic vs Pessimistic Locking</title>
      <dc:creator>Venkatesan Ramar</dc:creator>
      <pubDate>Wed, 24 Jun 2026 06:35:00 +0000</pubDate>
      <link>https://dev.to/morpheus-vera/data-consistency-under-contention-optimistic-vs-pessimistic-locking-1k0d</link>
      <guid>https://dev.to/morpheus-vera/data-consistency-under-contention-optimistic-vs-pessimistic-locking-1k0d</guid>
      <description>&lt;p&gt;A few years ago, I investigated a production issue where customers occasionally reported incorrect inventory counts. The application was healthy. The database was healthy. No errors appeared in the logs.&lt;/p&gt;

&lt;p&gt;The problem turned out to be concurrent updates. Multiple requests were modifying the same inventory record at nearly the same time, and one update silently overwrote another. The database did exactly what it was asked to do. The application failed to co-ordinate concurrent modifications to shared data.&lt;/p&gt;

&lt;p&gt;This is a common consistency problem. Whenever multiple users, services, or processes attempt to modify the same data simultaneously, contention appears. &lt;/p&gt;

&lt;p&gt;To manage that contention, systems typically rely on two approaches &lt;em&gt;Optimistic locking&lt;/em&gt; and &lt;em&gt;Pessimistic locking&lt;/em&gt;. Both aim to preserve data consistency, but they make very different assumptions about how conflicts occur. Those assumptions directly affect performance, scalability, and user experience.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;1. Why Locking Exists&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Databases are excellent at storing and retrieving data, but they do not inherently understand business intent. They execute operations exactly as instructed. This becomes problematic when multiple users interact with the same piece of data at the same time.&lt;/p&gt;

&lt;p&gt;Consider an inventory record:&lt;/p&gt;

&lt;p&gt;Product A&lt;br&gt;
Inventory = 10&lt;/p&gt;

&lt;p&gt;Now imagine two users accessing the system simultaneously. Both users read the same inventory value:&lt;/p&gt;

&lt;p&gt;Inventory = 10&lt;/p&gt;

&lt;p&gt;User A purchases one item.&lt;br&gt;
User B purchases two items.&lt;/p&gt;

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

&lt;p&gt;User A reads 10&lt;br&gt;
User B reads 10&lt;/p&gt;

&lt;p&gt;User A writes 9&lt;br&gt;
User B writes 8&lt;/p&gt;

&lt;p&gt;Both transactions succeed from the database's perspective. No errors occur, and both updates are accepted. However, one update effectively overwrites the other. &lt;/p&gt;

&lt;p&gt;This scenario is known as a &lt;strong&gt;lost update&lt;/strong&gt;. Both users started with the same information, but because their updates were not co-ordinated, one user's changes disappeared. Locking mechanisms exist primarily to prevent such situations.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Concurrency Is Usually A Business Problem&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Concurrency issues rarely present themselves as obvious technical failures. Systems continue running, databases remain available, and monitoring dashboards look healthy. The real impact appears in business outcomes.&lt;/p&gt;

&lt;p&gt;Customers do not care whether the root cause involves MVCC, transaction isolation levels, or a particular locking strategy. They only see incorrect results. For that reason, concurrency control is not merely a database concern—it is a business requirement that directly affects customer trust and operational correctness.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Contention Changes Everything&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many applications operate flawlessly until contention increases. A user profile system may rarely experience concurrent updates because different users modify different records. In contrast, a payment platform may process thousands of updates against the same accounts every second. Similarly, a seat reservation system may have thousands of users competing for a very small number of records.&lt;/p&gt;

&lt;p&gt;The frequency of contention is one of the most important factors when choosing a concurrency strategy. Systems with frequent conflicts require a different approach than systems where conflicts are rare. This distinction forms the foundation of the optimistic versus pessimistic locking debate.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;2. Pessimistic Locking: Assume Conflict Will Happen&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pessimistic locking starts with a conservative assumption:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Someone else will probably try to modify this data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because conflicts are expected, the system prevents them from occurring by restricting access immediately. The first transaction acquires a lock on the data, and any subsequent transaction attempting to modify the same data must wait until the lock is released.&lt;/p&gt;

&lt;p&gt;This approach prioritizes correctness by ensuring that only one transaction can modify a resource at a time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Bank Account Example&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Imagine two transactions attempting to modify the same account balance.&lt;/p&gt;

&lt;p&gt;Transaction A begins and acquires a lock:&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;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The row becomes locked, preventing other transactions from modifying it.&lt;/p&gt;

&lt;p&gt;Now Transaction B attempts the same operation:&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;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because Transaction A already holds the lock, Transaction B cannot proceed. It must wait until Transaction A completes and releases the lock. This guarantees that updates occur sequentially rather than concurrently.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What Happens Under Contention&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxm412dd3qjg16i7uci18.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%2Fxm412dd3qjg16i7uci18.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Notice that the second transaction does not fail. Instead, it pauses until the lock becomes available. This behavior makes correctness easier to reason about because the database itself enforces exclusive access to the data. Developers do not need to detect conflicts later because the database prevents them from occurring in the first place.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why Financial Systems Like Pessimistic Locking&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Certain domains prioritize correctness above all else. Examples include payment processing systems, banking platforms, trading applications, and inventory reservation systems.&lt;/p&gt;

&lt;p&gt;In these environments, &lt;em&gt;waiting is preferable to risking inconsistent data&lt;/em&gt;. Consider two users attempting to reserve the last available airline seat. Allowing both requests to proceed simultaneously could result in overselling the seat, creating operational and customer-service problems. A short delay is usually a much smaller cost than correcting inconsistent business data later.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Cost Of Waiting&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While pessimistic locking provides strong protection against conflicting updates, it introduces a different challenge: reduced concurrency.&lt;/p&gt;

&lt;p&gt;As contention increases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;response times increase&lt;/li&gt;
&lt;li&gt;throughput decreases&lt;/li&gt;
&lt;li&gt;blocked transactions accumulate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under heavy load, lock contention can become a significant bottleneck. Instead of processing business operations, the database spends more time coordinating access to shared resources. This trade-off becomes increasingly visible in high-traffic systems where many users compete for the same records.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;3. Optimistic Locking: Assume Conflict Is Rare&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Optimistic locking takes the opposite approach.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Most transactions will not conflict.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Instead of preventing concurrent access, the system allows multiple users to work with the same data simultaneously. Rather than blocking access upfront, conflicts are detected later when an update is attempted.&lt;/p&gt;

&lt;p&gt;This approach assumes that contention is relatively uncommon and that most operations can proceed without interference.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Core Idea&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Optimistic locking typically relies on a version number stored alongside each record.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Account
--------
Id = 100
Balance = 1000
Version = 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suppose two users read the same row. Both receive:&lt;/p&gt;

&lt;p&gt;Version = 5&lt;/p&gt;

&lt;p&gt;User A updates the record first:&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
               &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The update succeeds because the version matches the expected value. The record now becomes:&lt;/p&gt;

&lt;p&gt;Version = 6&lt;/p&gt;

&lt;p&gt;Later, User B attempts an update:&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;
               &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This update affects zero rows because the version is no longer 5. The database detects that another transaction modified the record first, and the update fails.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Conflict Becomes Explicit&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Unlike pessimistic locking, optimistic locking does not force transactions to wait. Instead, conflicting updates fail immediately.&lt;/p&gt;

&lt;p&gt;The application must then decide how to respond. Common options include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retry&lt;/li&gt;
&lt;li&gt;refresh data&lt;/li&gt;
&lt;li&gt;reject the operation&lt;/li&gt;
&lt;li&gt;ask the user to resolve the conflict&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This approach makes conflicts visible rather than hiding them behind waiting transactions. The responsibility for handling those conflicts shifts from the database to the application.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why Modern Applications Prefer Optimistic Locking&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many business applications experience relatively low contention. Examples include customer profiles, employee records, product catalogs, and content management systems. Most users interact with different records, making simultaneous updates uncommon.&lt;/p&gt;

&lt;p&gt;In these environments, blocking every update would introduce unnecessary overhead. Optimistic locking allows the system to maximize concurrency while still detecting the occasional conflict. As a result, applications achieve better scalability and responsiveness.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Cost Of Retrying&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Optimistic locking reduces database contention but introduces complexity elsewhere. Because conflicts are detected after they occur, applications must implement strategies for handling failures.&lt;/p&gt;

&lt;p&gt;Retries may sound straightforward, but production systems require additional considerations such as exponential back-off, &lt;br&gt;
user experience, duplicate submissions and retry storms. &lt;/p&gt;

&lt;p&gt;As a result, conflict resolution becomes an important part of application design rather than a purely database-level concern.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;4. How Modern RDBMS Actually Handle Concurrency&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Many engineers imagine databases constantly locking rows and blocking transactions. Modern relational databases are far more sophisticated.&lt;/p&gt;

&lt;p&gt;Systems such as PostgreSQL and MySQL rely heavily on a technique called &lt;strong&gt;Multi-Version Concurrency Control (MVCC)&lt;/strong&gt;. Understanding MVCC helps explain why modern databases can support high levels of concurrency without excessive blocking. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Multiple Versions Of Data&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of immediately replacing existing data, MVCC creates new versions of rows whenever updates occur.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────┐
│ Row Version 1 │
└───────────────┘
         ↓
┌───────────────┐
│ Row Version 2 │
└───────────────┘
         ↓
┌───────────────┐
│ Row Version 3 │
└───────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Older versions remain available for active transactions that still need them. This allows readers to continue accessing a consistent view of the data while updates occur in parallel.&lt;/p&gt;

&lt;p&gt;The result is significantly less blocking and much higher concurrency.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why Reads Usually Don't Block Writes&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of the most common &lt;em&gt;misconceptions&lt;/em&gt; about databases is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Every update blocks every read.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In MVCC-based databases, this is not true. Readers can access a consistent snapshot of the data while writers create newer versions in the background.&lt;/p&gt;

&lt;p&gt;This capability allows databases to support large numbers of concurrent users without forcing readers and writers to constantly wait for one another. It is one of the primary reasons modern relational databases scale far better than many developers initially expect.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Isolation Levels Matter&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Locking strategies are only one part of the consistency story. Isolation levels determine what data a transaction can see while other transactions are running.&lt;/p&gt;

&lt;p&gt;Common isolation levels include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read Committed&lt;/li&gt;
&lt;li&gt;Repeatable Read&lt;/li&gt;
&lt;li&gt;Serializable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each level provides different guarantees and trade-offs. Higher isolation levels generally offer stronger consistency but require additional coordination and overhead.&lt;/p&gt;

&lt;p&gt;Choosing a locking strategy without understanding transaction isolation can lead to incorrect assumptions about application behavior. In practice, consistency emerges from the combination of locking mechanisms, MVCC behavior, and transaction isolation working together.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;5. Deadlocks: The Hidden Cost of Pessimistic Locking&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Pessimistic locking guarantees exclusive access to data by preventing multiple transactions from modifying the same resource simultaneously. While this approach is highly effective at preserving consistency, it introduces a different class of concurrency problems: deadlocks.&lt;/p&gt;

&lt;p&gt;Deadlocks typically do not appear during initial development or testing because contention levels are low and transaction flows are relatively simple. As systems grow, however, more users, background processes, and business workflows begin interacting with the same data concurrently. Under these conditions, transactions may start waiting on each other in ways that create circular dependencies.&lt;/p&gt;

&lt;p&gt;When that happens, transactions that previously completed successfully begin failing unexpectedly, without any changes to the underlying business logic.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A Classic Deadlock Scenario&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consider a money transfer workflow involving two accounts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Transaction A                     Transaction B
─────────────                     ─────────────
Lock Account A                    Lock Account B
       │                                 │
       ▼                                 ▼
Update Account A                  Update Account B
       │                                 │
       ▼                                 ▼
Lock Account B ◄──────────────► Lock Account A
                  DEADLOCK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Transaction A holds a lock on Account A and waits for Account B. Meanwhile, Transaction B holds a lock on Account B and waits for Account A.&lt;/p&gt;

&lt;p&gt;Neither transaction can proceed because each will be waiting for a resource currently held by the other. Neither transaction can release its lock because it has not yet completed.&lt;/p&gt;

&lt;p&gt;The database detects this circular wait condition and identifies it as a deadlock.&lt;/p&gt;

&lt;p&gt;Deadlocks are &lt;em&gt;not limited&lt;/em&gt; to two rows or two transactions. In complex systems, deadlocks may involve multiple tables, indexes, and transactions, making them difficult to diagnose without proper monitoring and logging.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;How Databases Resolve Deadlocks&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Modern relational databases continuously analyze lock dependencies between active transactions. When a deadlock is detected, the database must break the cycle to allow progress.&lt;/p&gt;

&lt;p&gt;A simplified flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────┐
│ Transaction A │
└───────────────┘
         ↓
┌───────────────┐
│   Deadlock    │
└───────────────┘
         ↓
┌──────────────────────────┐
│ Database Chooses Victim  │
└──────────────────────────┘
         ↓
┌───────────────┐
│   Rollback    │
└───────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database selects one transaction as the deadlock victim and rolls it back. The other transaction is allowed to continue and eventually commit.&lt;/p&gt;

&lt;p&gt;The victim selection process varies by database implementation. Factors such as transaction age, resource consumption, and rollback cost may influence which transaction is terminated.&lt;/p&gt;

&lt;p&gt;From the application's perspective, this usually appears as an exception indicating that the transaction failed due to a deadlock. The application must be prepared to retry the operation because deadlocks are considered transient failures rather than permanent errors.&lt;/p&gt;

&lt;p&gt;Importantly, deadlocks are not database bugs. They are an expected consequence of concurrent transactions acquiring locks in different orders.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Deadlocks Become Operational Problems&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deadlocks are difficult to reproduce in development environments because concurrency levels are significantly lower than in production.&lt;/p&gt;

&lt;p&gt;Real-world systems contain many independent actors operating simultaneously like concurrent users, background jobs, asynchronous consumers and scheduled tasks. &lt;/p&gt;

&lt;p&gt;Each of these components may access shared resources using different execution paths.&lt;/p&gt;

&lt;p&gt;A deadlock occurring once every few weeks may have little operational impact. However, when contention increases and deadlocks begin occurring hundreds or thousands of times per hour, they can significantly affect throughput, latency, and user experience.&lt;/p&gt;

&lt;p&gt;For this reason, high-scale systems attempt to minimize lock durations, enforce consistent lock acquisition ordering, or adopt optimistic concurrency strategies when contention remains relatively low.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;6. Optimistic Locking in Spring and JPA&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Optimistic locking is one of the most commonly used concurrency control mechanisms in enterprise Java applications. Frameworks such as JPA and Hibernate provide built-in support, making implementation straightforward while still offering strong protection against lost updates.&lt;/p&gt;

&lt;p&gt;Unlike pessimistic locking, optimistic locking does not prevent concurrent access. Instead, it detects whether another transaction modified the data between the time it was read and the time it was updated.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The &lt;code&gt;@Version&lt;/code&gt; Annotation&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A typical entity might look like:&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;@Entity&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;Account&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Id&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@Version&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;version&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;@Version&lt;/code&gt; field acts as a concurrency token. Every successful update increments the version number automatically.&lt;/p&gt;

&lt;p&gt;When Hibernate generates update statements, it includes the current version value in the &lt;code&gt;WHERE&lt;/code&gt; clause. This ensures that updates only succeed if the record has not been modified since it was originally read.&lt;/p&gt;

&lt;p&gt;This mechanism allows multiple users to read the same data concurrently while still preventing silent overwrites.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What Actually Happens&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Suppose two users load the same entity.&lt;/p&gt;

&lt;p&gt;Both receive:&lt;br&gt;
Version = 10&lt;/p&gt;

&lt;p&gt;User A updates first.&lt;/p&gt;

&lt;p&gt;The version becomes:&lt;br&gt;
Version = 11&lt;/p&gt;

&lt;p&gt;User B attempts an update.&lt;/p&gt;

&lt;p&gt;Hibernate generates an update statement similar to:&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;UPDATE&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;balance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;
               &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="k"&gt;version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the row now contains version 11 instead of version 10, the WHERE condition no longer matches.&lt;/p&gt;

&lt;p&gt;As a result, no rows are updated.&lt;/p&gt;

&lt;p&gt;Hibernate detects this condition and throws &lt;code&gt;OptimisticLockException&lt;/code&gt;. This exception indicates that another transaction modified the entity after it was originally loaded.&lt;/p&gt;

&lt;p&gt;Rather than silently overwriting data, the application is forced to acknowledge and handle the conflict.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Handling Optimistic Lock Failures&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding &lt;code&gt;@Version&lt;/code&gt; annotation is only the first step.&lt;/p&gt;

&lt;p&gt;The more important challenge is deciding how the application should respond when conflicts occur.&lt;/p&gt;

&lt;p&gt;Possible strategies include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retry automatically&lt;/li&gt;
&lt;li&gt;reject the operation&lt;/li&gt;
&lt;li&gt;reload and merge&lt;/li&gt;
&lt;li&gt;notify the user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The appropriate choice depends heavily on business requirements.&lt;/p&gt;

&lt;p&gt;For example, &lt;em&gt;inventory/reservation systems retry automatically&lt;/em&gt; because conflicts are expected and transient. &lt;em&gt;Collaborative editing systems&lt;/em&gt; may present users with &lt;em&gt;merge&lt;/em&gt; options. &lt;em&gt;Financial applications&lt;/em&gt; frequently &lt;em&gt;reload&lt;/em&gt; the latest state and &lt;em&gt;re-validate&lt;/em&gt; business rules before attempting another update.&lt;/p&gt;

&lt;p&gt;Optimistic locking provides conflict detection. It does not provide conflict resolution. Designing an effective resolution strategy is a critical part of building reliable systems.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;7. Locking in NoSQL Databases&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A common misconception is that NoSQL databases eliminate concurrency concerns.&lt;/p&gt;

&lt;p&gt;In reality, concurrent modification problems still exist. The difference lies in how databases expose consistency guarantees and concurrency control mechanisms.&lt;/p&gt;

&lt;p&gt;Most NoSQL platforms provide some form of optimistic concurrency control rather than traditional row-level locking.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MongoDB&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MongoDB provides atomic operations at the document level. Updates to a single document are isolated and executed atomically.&lt;/p&gt;

&lt;p&gt;For concurrency control, many applications implement version-based optimistic locking.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="na"&gt;_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="na"&gt;$set&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SHIPPED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
     &lt;span class="p"&gt;},&lt;/span&gt;
     &lt;span class="na"&gt;$inc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
     &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The update succeeds only if the document still contains version 5.&lt;/p&gt;

&lt;p&gt;If another process updates the document first, the query condition no longer matches:&lt;/p&gt;

&lt;p&gt;Matched Documents = 0&lt;/p&gt;

&lt;p&gt;The application can then detect the conflict and decide whether to retry or reject the operation.&lt;/p&gt;

&lt;p&gt;Conceptually, this is very similar to optimistic locking in relational databases.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Redis&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Redis is generally viewed as a simple in-memory cache, but it is also frequently used as a primary data store, coordination mechanism, and distributed locking platform.&lt;/p&gt;

&lt;p&gt;Because Redis executes commands sequentially within a single-threaded event loop, individual commands are atomic. However, concurrency challenges still arise when multiple clients perform read-modify-write operations.&lt;/p&gt;

&lt;p&gt;One approach is to use optimistic concurrency control through the WATCH command.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;WATCH account:100
GET account:100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The client reads the value and prepares an update.&lt;/p&gt;

&lt;p&gt;When the transaction executes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MULTI
SET account:100 900
EXEC
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Redis verifies that the watched key has not changed since it was read.&lt;/p&gt;

&lt;p&gt;If another client modifies the key before &lt;code&gt;EXEC&lt;/code&gt;, the transaction is aborted: &lt;em&gt;&lt;strong&gt;Transaction Failed&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The application can then retry using the latest value.&lt;/p&gt;

&lt;p&gt;Redis is also widely used for distributed locking through commands such as:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SET resource-lock unique-id NX PX 30000&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This creates a lock only if the key does not already exist and automatically expires it after a specified timeout.&lt;/p&gt;

&lt;p&gt;While distributed locks can co-ordinate access across multiple application instances, they should be used carefully. Improper lock expiration settings, network partitions, and process failures can introduce subtle consistency issues.&lt;/p&gt;

&lt;p&gt;For this reason, many Redis-based systems prefer optimistic concurrency patterns or idempotent operations whenever possible, reserving distributed locks for workflows that truly require exclusive access.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DynamoDB&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;DynamoDB provides optimistic concurrency control through &lt;em&gt;conditional writes&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;A write operation can specify a condition that must evaluate to true before the update is applied.&lt;/p&gt;

&lt;p&gt;The following example performs an &lt;code&gt;UpdateItem&lt;/code&gt; operation. It tries to reduce the &lt;code&gt;Price&lt;/code&gt; of a product by 75—but the condition expression prevents the update if the current &lt;code&gt;Price&lt;/code&gt; is less than or equal to 500.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws dynamodb update-item \
    --table-name ProductCatalog \
    --key '{"Id": {"N": "456"}}' \
    --update-expression "SET Price = Price - 75" \
    --condition-expression "Price &amp;gt; 500"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the starting Price is 650, the &lt;code&gt;UpdateItem&lt;/code&gt; operation reduces the &lt;code&gt;Price&lt;/code&gt; to 575. If you run the &lt;code&gt;UpdateItem&lt;/code&gt; operation again, the &lt;code&gt;Price&lt;/code&gt; is reduced to 500. If you run it a third time, the condition expression evaluates to false, and the update fails.&lt;/p&gt;

&lt;p&gt;This approach allows DynamoDB to maintain high scalability while still preventing lost updates. Because &lt;strong&gt;conditional writes&lt;/strong&gt; are implemented directly by the &lt;em&gt;storage engine&lt;/em&gt;, applications can enforce concurrency guarantees &lt;em&gt;without&lt;/em&gt; introducing &lt;em&gt;explicit locking mechanisms&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Many large-scale AWS systems rely heavily on this pattern.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;8. Distributed Systems Change Everything&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Many engineers discover an uncomfortable reality when transitioning from monolithic applications to microservices:&lt;/p&gt;

&lt;p&gt;Database locking does not extend beyond a single database.&lt;/p&gt;

&lt;p&gt;Traditional locking mechanisms work extremely well within a single transactional boundary. Once data and business processes span multiple services, those guarantees disappear.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Locks Cannot Cross Services&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Order Service
      |
Database A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Inventory Service
      |
Database B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A lock acquired in Database A has no effect on Database B.&lt;/p&gt;

&lt;p&gt;Even if both services participate in the same business workflow, neither database has visibility into the other's locks or transactions.&lt;/p&gt;

&lt;p&gt;As a result, traditional database locking cannot guarantee consistency across service boundaries.&lt;/p&gt;

&lt;p&gt;This limitation fundamentally changes how distributed systems are designed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why SAGAs Exist&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Microservices frequently execute workflows that span multiple services and databases.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create Order
      │
      ▼
Reserve Inventory
      │
      ▼
Process Payment
      │
      ▼
Create Shipment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No single ACID transaction can encompass the entire workflow.&lt;/p&gt;

&lt;p&gt;Instead, systems rely on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;compensating transactions&lt;/li&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;eventual consistency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the problem Saga patterns address.&lt;/p&gt;

&lt;p&gt;Rather than locking resources across services, Sagas coordinate a sequence of local transactions and define recovery actions when failures occur.&lt;/p&gt;

&lt;p&gt;The goal is not immediate consistency but reliable business outcomes despite partial failures.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why Outbox Doesn't Require Locks&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Transactional Outbox pattern solves a different challenge.&lt;/p&gt;

&lt;p&gt;It guarantees &lt;em&gt;Database Commit + Event Publication&lt;/em&gt; without requiring distributed transactions.&lt;/p&gt;

&lt;p&gt;The application writes both business data and an outbound event record within the same local transaction. A separate process later publishes the event.&lt;/p&gt;

&lt;p&gt;This approach relies on transactional guarantees within a single database.&lt;/p&gt;

&lt;p&gt;Not pessimistic locking.&lt;/p&gt;

&lt;p&gt;Understanding this distinction is important because many distributed systems problems are fundamentally reliability and coordination problems rather than concurrency-control problems.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Idempotency Beats Locking&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many distributed systems avoid locking altogether.&lt;/p&gt;

&lt;p&gt;Instead, they make operations idempotent, meaning the same operation can be executed multiple times without changing the final outcome.&lt;/p&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;p&gt;Process Payment Event&lt;/p&gt;

&lt;p&gt;The consumer records &lt;em&gt;Payment Already Processed&lt;/em&gt; and ignores duplicates.&lt;/p&gt;

&lt;p&gt;This strategy allows systems to safely retry operations without introducing global locks or distributed coordination.&lt;/p&gt;

&lt;p&gt;Modern event-driven architectures frequently prefer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retries&lt;/li&gt;
&lt;li&gt;idempotency&lt;/li&gt;
&lt;li&gt;eventual consistency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;over distributed locking because these approaches scale more effectively and remain resilient during failures.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;9. Choosing Between Optimistic and Pessimistic Locking&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Neither optimistic nor pessimistic locking is universally superior.&lt;/p&gt;

&lt;p&gt;The correct choice depends on workload characteristics, contention frequency, consistency requirements, and performance goals.&lt;/p&gt;

&lt;p&gt;Understanding how often conflicts occur is usually more important than understanding the locking mechanism itself.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Choose Pessimistic Locking When&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pessimistic locking is most appropriate when conflicts are common and the cost of inconsistency is high.&lt;/p&gt;

&lt;p&gt;Scenarios like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;seat reservation systems&lt;/li&gt;
&lt;li&gt;inventory allocation&lt;/li&gt;
&lt;li&gt;financial transactions&lt;/li&gt;
&lt;li&gt;account balance updates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In these scenarios, allowing concurrent modifications may create unacceptable business outcomes. Waiting for access is preferable to resolving conflicts after they occur.&lt;/p&gt;

&lt;p&gt;Correctness takes priority over throughput.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Choose Optimistic Locking When&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Optimistic locking works best when conflicts are relatively rare.&lt;/p&gt;

&lt;p&gt;Scenarios like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;customer profiles&lt;/li&gt;
&lt;li&gt;product catalogs&lt;/li&gt;
&lt;li&gt;employee records&lt;/li&gt;
&lt;li&gt;content management systems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most transactions complete successfully without interference from other users. Because contention is low, avoiding locks improves concurrency and reduces database overhead.&lt;/p&gt;

&lt;p&gt;The occasional conflict can be handled through &lt;em&gt;retries or user intervention&lt;/em&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Measure Contention First&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many teams choose a locking strategy based on assumptions rather than evidence.&lt;/p&gt;

&lt;p&gt;A better approach is to measure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lock wait time&lt;/li&gt;
&lt;li&gt;retry rates&lt;/li&gt;
&lt;li&gt;update conflicts&lt;/li&gt;
&lt;li&gt;transaction latency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Production metrics reveal surprising patterns.&lt;/p&gt;

&lt;p&gt;A workflow that appears highly contentious may rarely experience conflicts, while seemingly independent operations may compete heavily for shared resources.&lt;/p&gt;

&lt;p&gt;Data should drive concurrency decisions whenever possible.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;10. Common Mistakes Teams Make&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Concurrency control is generally misunderstood because systems behave correctly under low load and fail only when contention increases.&lt;/p&gt;

&lt;p&gt;Several mistakes appear repeatedly across production systems.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Using Pessimistic Locking Everywhere&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Applying pessimistic locking indiscriminately can severely limit scalability.&lt;/p&gt;

&lt;p&gt;The application remains correct, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;throughput decreases&lt;/li&gt;
&lt;li&gt;latency increases&lt;/li&gt;
&lt;li&gt;lock contention grows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As traffic increases, the database spends more time coordinating access than executing business logic.&lt;/p&gt;

&lt;p&gt;Correctness is essential, but excessive locking can become a significant performance bottleneck.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ignoring Retry Logic&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Optimistic locking assumes conflicts will occasionally occur. Without retry mechanisms, users may experience unnecessary failures even when a simple retry would succeed immediately.&lt;/p&gt;

&lt;p&gt;Applications should treat optimistic lock exceptions as expected outcomes rather than exceptional situations. Proper retry policies are as important as the locking strategy itself.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Long Transactions&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Locks held for extended periods dramatically increase contention. Transactions should perform only the work necessary to maintain consistency.&lt;/p&gt;

&lt;p&gt;External API calls, file processing, and lengthy computations should generally occur outside transactional boundaries whenever possible.&lt;/p&gt;

&lt;p&gt;Short transactions reduce lock duration and improve overall system throughput.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Confusing Isolation Levels with Locking&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many developers assume &lt;code&gt;Serializable&lt;/code&gt; automatically solves every concurrency problem.&lt;/p&gt;

&lt;p&gt;In reality, isolation levels define &lt;em&gt;visibility rules&lt;/em&gt; between transactions, while locking strategies define how concurrent modifications are co-ordinated.&lt;/p&gt;

&lt;p&gt;Both influence consistency.&lt;br&gt;
Neither replaces the other.&lt;/p&gt;

&lt;p&gt;Understanding the distinction is critical when diagnosing concurrency issues.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;11. Final Thoughts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Concurrency control is fundamentally the discipline of managing contention while preserving correctness.&lt;/p&gt;

&lt;p&gt;Optimistic and pessimistic locking approach this challenge from different perspectives.&lt;/p&gt;

&lt;p&gt;The correct choice depends on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;contention patterns&lt;/li&gt;
&lt;li&gt;consistency requirements&lt;/li&gt;
&lt;li&gt;throughput goals&lt;/li&gt;
&lt;li&gt;operational behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many production systems use both approaches simultaneously. Critical workflows may require strict exclusivity, while less contentious operations benefit from maximum concurrency.&lt;/p&gt;

&lt;p&gt;The most effective engineers understand the trade-offs behind each strategy and apply them deliberately based on business requirements and real-world traffic patterns. Because concurrency problems rarely appear when systems are idle. They appear when traffic grows, users increase, and contention finally arrives.&lt;/p&gt;




&lt;p&gt;Assisted AI to generate charts and diagrams. &lt;/p&gt;

</description>
      <category>database</category>
      <category>programming</category>
      <category>discuss</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Build vs Buy: The Expensive Engineering Decision Less Talked About</title>
      <dc:creator>Venkatesan Ramar</dc:creator>
      <pubDate>Mon, 15 Jun 2026 08:59:00 +0000</pubDate>
      <link>https://dev.to/morpheus-vera/build-vs-buy-the-expensive-engineering-decision-less-talked-about-4k7i</link>
      <guid>https://dev.to/morpheus-vera/build-vs-buy-the-expensive-engineering-decision-less-talked-about-4k7i</guid>
      <description>&lt;p&gt;Back in 2015, I joined a product company whose platform had been evolving since late 90's. Coming from a startup background, I was overwhelmed by the number of in-house tools, and platforms that existed alongside the core product.&lt;/p&gt;

&lt;p&gt;Over time and after leaving the organization in 2023 — I began to appreciate the trade-offs behind those build decisions. Some became strategic assets, while some introduced years of ownership and maintenance overhead.&lt;/p&gt;

&lt;p&gt;This article shares some of the lessons I learned about one of the most important engineering decisions teams make: build or buy.&lt;/p&gt;




&lt;p&gt;Over the years, I've come to believe that some of the most expensive engineering mistakes have very little to do with technology itself.&lt;/p&gt;

&lt;p&gt;They start with a much simpler question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Should we build this ourselves?&lt;br&gt;
Or should we buy it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At first glance, the answer often feels obvious. A team identifies a need.&lt;br&gt;
Maybe it's:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authentication,&lt;/li&gt;
&lt;li&gt;workflow orchestration,&lt;/li&gt;
&lt;li&gt;internal developer portals,&lt;/li&gt;
&lt;li&gt;database migration tool, or &lt;/li&gt;
&lt;li&gt;some internal framework.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Someone says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"We can build this in a few weeks."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Often, they're right. The first version usually isn't that difficult. The real challenge comes later. Because every build decision eventually becomes an ownership decision.&lt;/p&gt;

&lt;p&gt;And ownership tends to last much longer than implementation.&lt;/p&gt;

&lt;p&gt;Over the years, I've seen teams successfully build internal platforms that became strategic assets. I've also seen teams accidentally become software vendors to themselves. &lt;/p&gt;

&lt;p&gt;The interesting question isn't whether we can build something. Modern engineering teams can build almost anything.&lt;/p&gt;

&lt;p&gt;The more important question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do we want to own it for the next five years?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;strong&gt;1. Why This Decision Matters&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A decade ago, many engineering teams had fewer choices. But today, the situation is completely different.&lt;/p&gt;

&lt;p&gt;Almost every technical capability has mature products available.&lt;/p&gt;

&lt;p&gt;Say, you need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;authentication?&lt;/li&gt;
&lt;li&gt;observability?&lt;/li&gt;
&lt;li&gt;workflow orchestration?&lt;/li&gt;
&lt;li&gt;developer portals?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is probably a vendor already solving that problem, that's what makes the decision difficult.&lt;/p&gt;

&lt;p&gt;Because modern engineering teams are no longer choosing between having a capability, or not having one.&lt;/p&gt;

&lt;p&gt;They're choosing between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;building it,&lt;/li&gt;
&lt;li&gt;extending it,&lt;/li&gt;
&lt;li&gt;buying it, or&lt;/li&gt;
&lt;li&gt;integrating it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The number of options has increased. At the same time, engineering capacity remains limited. Teams hit decision paralysis. &lt;/p&gt;

&lt;p&gt;Every sprint spent building internal tooling is a sprint not spent building customer-facing capabilities.&lt;/p&gt;

&lt;p&gt;This trade-off becomes increasingly important as organizations grow. Especially when platform investments start competing with product investments.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;2. The Hidden Cost Teams Ignore&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One pattern I've noticed is that teams are usually good at estimating development effort. They're much less effective at estimating ownership effort.&lt;/p&gt;

&lt;p&gt;A discussion might sound like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This looks straightforward. We can probably build it in three weeks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Probably they're right. The problem is that the three-week estimate usually covers only Version 1.&lt;/p&gt;

&lt;p&gt;It rarely includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;upgrades,&lt;/li&gt;
&lt;li&gt;support,&lt;/li&gt;
&lt;li&gt;operational maintenance,&lt;/li&gt;
&lt;li&gt;bug fixes,&lt;/li&gt;
&lt;li&gt;security reviews,&lt;/li&gt;
&lt;li&gt;documentation,&lt;/li&gt;
&lt;li&gt;on-boarding,&lt;/li&gt;
&lt;li&gt;scalability improvements, and &lt;/li&gt;
&lt;li&gt;future requirements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those costs appear gradually which makes them easy to underestimate.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Building Is Easy. Owning Is Hard&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many internal systems begin life as small engineering utilities. Gradually adoption grows. Soon other teams depend on them.&lt;/p&gt;

&lt;p&gt;Now expectations change.&lt;/p&gt;

&lt;p&gt;The platform suddenly needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;up-time guarantees,&lt;/li&gt;
&lt;li&gt;backward compatibility,&lt;/li&gt;
&lt;li&gt;support processes, and &lt;/li&gt;
&lt;li&gt;clear ownership.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What started as an engineering project slowly becomes a product except now the customers are internal teams.&lt;/p&gt;

&lt;p&gt;I've seen this happen with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;internal frameworks,&lt;/li&gt;
&lt;li&gt;workflow engines,&lt;/li&gt;
&lt;li&gt;authentication services, and &lt;/li&gt;
&lt;li&gt;developer portals.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The implementation wasn't the difficult part but the long-term ownership was.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Internal SaaS Trap&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of the most interesting things about platform engineering is that organizations sometimes become software vendors without realizing it.&lt;/p&gt;

&lt;p&gt;Imagine a team builds an internal feature flag platform.&lt;/p&gt;

&lt;p&gt;Version 1.0 supports &lt;em&gt;simple enable/disable toggles&lt;/em&gt;. Seems pretty straightforward.&lt;/p&gt;

&lt;p&gt;Then adopted teams raise feature requests like percentage roll-outs, audit logs, experimentation, approval workflows.&lt;/p&gt;

&lt;p&gt;Now the platform team is effectively running a software product. Except instead of external customers, they're supporting internal engineering teams.&lt;/p&gt;

&lt;p&gt;The complexity didn't disappear. It simply became your responsibility.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Opportunity Cost Is Real&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is probably the most overlooked factor in build-versus-buy discussions.&lt;/p&gt;

&lt;p&gt;Suppose five engineers spend six months building an internal platform.&lt;/p&gt;

&lt;p&gt;The direct cost is obvious but another question is often ignored:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What didn't get built during those six months?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Perhaps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;product features were delayed,&lt;/li&gt;
&lt;li&gt;customer requests remained unresolved,&lt;/li&gt;
&lt;li&gt;roadmap commitments slipped.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These costs rarely appear in dashboards. Yet they often have a larger business impact than infrastructure costs.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;3. Why Engineering Teams Choose To Build&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Despite the risks, engineering teams continue to build internal solutions. There are good reasons for that, not every build decision is a mistake.&lt;/p&gt;

&lt;p&gt;Some become enormous competitive advantages.&lt;/p&gt;

&lt;p&gt;The challenge is understanding why we are choosing to build in the first place.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Engineers Like Solving Problems&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This shouldn't surprise anyone. &lt;/p&gt;

&lt;p&gt;Most engineers enjoy creating systems, and building software feels productive and empowering. It provides a level of control that third-party products cannot. When requirements are unique, building can absolutely make sense.&lt;/p&gt;

&lt;p&gt;The problem appears when technical enthusiasm replaces strategic evaluation.&lt;/p&gt;

&lt;p&gt;Just because something is technically interesting doesn't automatically mean it should be owned long-term.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Vendor Skepticism&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many teams have legitimate concerns about vendors.&lt;/p&gt;

&lt;p&gt;Questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What if pricing changes?&lt;/li&gt;
&lt;li&gt;What if the company gets acquired?&lt;/li&gt;
&lt;li&gt;What if we're locked in?&lt;/li&gt;
&lt;li&gt;What if customization becomes difficult?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These concerns are real. Sometimes they justify building.&lt;/p&gt;

&lt;p&gt;But I've also seen teams dramatically overestimate vendor risks while underestimating ownership risks.&lt;/p&gt;

&lt;p&gt;Both sides deserve equal scrutiny.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The "It Looks Simple" Fallacy&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some capabilities appear deceptively simple.&lt;/p&gt;

&lt;p&gt;Authentication is a classic example.&lt;/p&gt;

&lt;p&gt;At first glance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;users log in,&lt;/li&gt;
&lt;li&gt;users log out,&lt;/li&gt;
&lt;li&gt;passwords are stored.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simple, until requirements expand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OAuth&lt;/li&gt;
&lt;li&gt;SAML&lt;/li&gt;
&lt;li&gt;MFA&lt;/li&gt;
&lt;li&gt;SSO&lt;/li&gt;
&lt;li&gt;compliance&lt;/li&gt;
&lt;li&gt;account recovery&lt;/li&gt;
&lt;li&gt;security reviews&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Suddenly the original problem looks very different.&lt;/p&gt;

&lt;p&gt;Version 1 is usually easy. Version 10 is where the complexity appears. &lt;/p&gt;




&lt;p&gt;&lt;strong&gt;4. Where Companies Successfully Build&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It might sound like buying is always the safer option, No. Many of the most successful technology companies built substantial internal platforms.&lt;/p&gt;

&lt;p&gt;The difference is that they usually built capabilities closely tied to their competitive advantage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build What Makes You Different&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One repeated pattern among successful engineering organizations is that they build things that are core to their business. Not things that are merely useful.&lt;/p&gt;

&lt;p&gt;This distinction matters.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Netflix Didn't Win By Building Authentication&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Netflix became successful because of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;streaming infrastructure,&lt;/li&gt;
&lt;li&gt;recommendation systems,&lt;/li&gt;
&lt;li&gt;content delivery,&lt;/li&gt;
&lt;li&gt;personalization.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those capabilities directly influenced the business.&lt;/p&gt;

&lt;p&gt;Investing heavily in them made strategic sense. Authentication was necessary. Recommendation systems were differentiating.&lt;/p&gt;

&lt;p&gt;The difference is important.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Uber Didn't Buy Dispatching&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dispatching is central to Uber's business.&lt;/p&gt;

&lt;p&gt;The way drivers and riders are matched directly affects customer experience, efficiency, profitability.&lt;/p&gt;

&lt;p&gt;That capability is core business logic.&lt;/p&gt;

&lt;p&gt;Owning it provides competitive advantage. Buying it would have limited differentiation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LinkedIn Built Kafka&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kafka began as an internal project at LinkedIn to solve large-scale event streaming challenges. At the time, existing messaging systems struggled to handle the volume, durability, and scalability requirements of LinkedIn's growing platform.&lt;/p&gt;

&lt;p&gt;Building Kafka made sense because reliable event streaming was becoming a foundational capability for the business. What started as an internal solution eventually evolved into one of the most widely adopted distributed systems in the industry.&lt;/p&gt;

&lt;p&gt;The lesson isn't that every company should build its own messaging platform. The lesson is that LinkedIn built a capability that directly addressed a strategic problem at its scale.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Google Built Kubernetes&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kubernetes originated from Google's experience running massive distributed systems over many years. Google had already developed internal container orchestration platforms and operational practices long before containers became mainstream.&lt;/p&gt;

&lt;p&gt;Rather than adapting existing solutions, Google built Kubernetes based on lessons learned from operating infrastructure at enormous scale.&lt;/p&gt;

&lt;p&gt;For most organizations, building a container orchestration platform would be a terrible investment. For Google, infrastructure management was a core competency and strategic advantage.&lt;/p&gt;

&lt;p&gt;The takeaway is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build when the capability is closely tied to your unique scale, business model, or competitive advantage.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;strong&gt;The Common Pattern&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The most successful build decisions usually share a characteristic:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The capability directly contributes to competitive advantage.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When that's true, ownership often makes sense. When it doesn't, the equation changes dramatically.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;5. The Platform Engineering Perspective&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Over the last few years, platform engineering has become a major focus area for many organizations. The goal is to make developers more productive.&lt;/p&gt;

&lt;p&gt;Provide:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;self-service capabilities,&lt;/li&gt;
&lt;li&gt;deployment automation,&lt;/li&gt;
&lt;li&gt;observability,&lt;/li&gt;
&lt;li&gt;infrastructure provisioning, and &lt;/li&gt;
&lt;li&gt;standardized workflows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The challenge is deciding how much of that platform should be built internally.&lt;/p&gt;

&lt;p&gt;This is where build-versus-buy decisions become particularly interesting.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Internal Developer Platform Dilemma&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Imagine a growing engineering organization.&lt;/p&gt;

&lt;p&gt;Developers complain about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inconsistent environments,&lt;/li&gt;
&lt;li&gt;deployment complexity,&lt;/li&gt;
&lt;li&gt;on-boarding difficulties.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The organization decides to build an Internal Developer Platform. The initial vision sounds reasonable.&lt;/p&gt;

&lt;p&gt;A central portal where developers can create services, access documentation, provision resources, monitor deployments. But soon new requirements emerge:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RBAC&lt;/li&gt;
&lt;li&gt;audit logs&lt;/li&gt;
&lt;li&gt;integrations&lt;/li&gt;
&lt;li&gt;workflow automation&lt;/li&gt;
&lt;li&gt;plugin ecosystems&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Before long, the platform itself becomes a product. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Backstage Lesson&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many organizations faced this exact challenge.&lt;/p&gt;

&lt;p&gt;Instead of building an entire developer portal from scratch, they adopted existing platforms and customized them.&lt;/p&gt;

&lt;p&gt;This approach is interesting because it reflects a broader engineering principle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Buy the foundation. Build the differentiation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The organization still owns the developer experience. It avoids spending years recreating foundational capabilities.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Build The Last 20%&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of the most useful heuristics I've encountered is this:&lt;/p&gt;

&lt;p&gt;Buy the first 80%. Build the last 20%.&lt;/p&gt;

&lt;p&gt;The first 80% usually consists of commodity functionality. The last 20% often contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;business-specific workflows,&lt;/li&gt;
&lt;li&gt;domain integrations,&lt;/li&gt;
&lt;li&gt;unique operational requirements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That final layer is often where competitive advantage exists. It's usually a better place to invest engineering effort.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;6. A Practical Decision Framework&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Over time, I've found that build-versus-buy discussions become much easier when evaluated through a consistent framework.&lt;/p&gt;

&lt;p&gt;Rather than debating technologies, the conversation shifts toward business and ownership.&lt;/p&gt;

&lt;p&gt;Here are some of the questions help to make decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question 1: Does This Differentiate The Business?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is one of the most important question.&lt;/p&gt;

&lt;p&gt;If the capability disappeared tomorrow, would customers notice?&lt;br&gt;
Would it impact the company's competitive position?&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;Building recommendation engines, pricing algorithms, matching engines, or domain-specific workflows often creates differentiation.&lt;/p&gt;

&lt;p&gt;The closer a capability is to &lt;em&gt;competitive advantage&lt;/em&gt;, the stronger the case for building.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question 2: Do We Want To Own This In Three Years?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most build decisions focus on implementation. &lt;br&gt;
Few focus on ownership.&lt;/p&gt;

&lt;p&gt;A better question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Will we still want to maintain this three years from now?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ownership includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;upgrades,&lt;/li&gt;
&lt;li&gt;security,&lt;/li&gt;
&lt;li&gt;operational support,&lt;/li&gt;
&lt;li&gt;bug fixes,&lt;/li&gt;
&lt;li&gt;documentation,&lt;/li&gt;
&lt;li&gt;compliance,&lt;/li&gt;
&lt;li&gt;training.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the answer feels uncomfortable, that is valuable information.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question 3: Can We Support It Operationally?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every system eventually enters production and production changes everything.&lt;/p&gt;

&lt;p&gt;A build decision also means committing to on-call support, incident response, monitoring, maintenance, and/or disaster recovery.&lt;/p&gt;

&lt;p&gt;The engineering effort doesn't end when the code is deployed. In many cases, that's where the real work begins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question 4: Is The Market Mature?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sometimes buying is difficult because the market is immature. The available products may not solve the problem adequately.&lt;/p&gt;

&lt;p&gt;But in mature categories observability, authentication, feature management and workflow orchestration vendors have often spent years refining their solutions.&lt;/p&gt;

&lt;p&gt;Ignoring that accumulated expertise can be expensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Question 5: What Is The Opportunity Cost?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This question is frequently overlooked.&lt;/p&gt;

&lt;p&gt;Suppose a team spends six engineers, six months, building an internal capability.&lt;/p&gt;

&lt;p&gt;The direct cost is obvious but opportunity cost is harder to measure.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What customer-facing work was delayed?&lt;br&gt;
What revenue-generating features were postponed?&lt;br&gt;
What strategic initiatives slowed down?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sometimes the most expensive cost is the one that never appears in a budget report.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;7. The Hybrid Model Usually Wins&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One thing I've noticed is that the engineering organizations rarely choose a pure build or pure buy strategy.&lt;/p&gt;

&lt;p&gt;Instead, they combine both.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Buy The Foundation&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Commodity capabilities are often purchased or adopted. These tools solve common problems that many organizations face.&lt;/p&gt;

&lt;p&gt;Rebuilding them rarely creates differentiation.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Build Business-Specific Layers&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The organization's engineering effort is then focused on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;business workflows,&lt;/li&gt;
&lt;li&gt;domain models,&lt;/li&gt;
&lt;li&gt;operational processes,&lt;/li&gt;
&lt;li&gt;customer-facing capabilities,&lt;/li&gt;
&lt;li&gt;proprietary integrations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where engineering investment usually generates the highest return.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Why This Works&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The hybrid model captures the advantages of both approaches.&lt;/p&gt;

&lt;p&gt;Organizations avoid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rebuilding mature capabilities,&lt;/li&gt;
&lt;li&gt;unnecessary ownership burden,&lt;/li&gt;
&lt;li&gt;platform reinvention.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the same time, they retain flexibility where it matters most.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;8. Common Mistakes Teams Make&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most build-versus-buy failures follow surprisingly similar patterns. The technology might change but the mistakes rarely do. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Underestimating Maintenance&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Teams usually estimate '&lt;em&gt;Initial Build Cost&lt;/em&gt;' while forgetting support, upgrades, security and operations. Over a multi-year horizon, ownership often exceeds implementation cost.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Rebuilding Commodity Software&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is probably the most common mistake.&lt;/p&gt;

&lt;p&gt;Engineering teams are highly capable. Given enough time, they can rebuild almost anything. The question is whether they should.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Optimizing For Engineering Preference&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a subtle trap.&lt;/p&gt;

&lt;p&gt;Engineers naturally enjoy building systems. But engineering satisfaction and business value are not always aligned.&lt;/p&gt;

&lt;p&gt;A technically elegant solution can still be a poor investment. The best technical decision is not always the best business decision.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Assuming Vendors Never Improve&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many build decisions are based on current vendor limitations but products evolve. Markets mature gradually and capabilities improve. A solution that looked inadequate two years ago may look very different today.&lt;/p&gt;

&lt;p&gt;Periodic re-evaluation is important.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;9. AI Is Changing The Economics&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's impossible to discuss build-versus-buy decisions today without mentioning AI. AI-assisted development has significantly reduced implementation effort. Many teams can now prototype internal tools faster than ever.&lt;/p&gt;

&lt;p&gt;Capabilities that once required months of development can sometimes be assembled in days.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Building Is Cheaper Than Before&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI helps with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scaffolding&lt;/li&gt;
&lt;li&gt;code generation&lt;/li&gt;
&lt;li&gt;testing&lt;/li&gt;
&lt;li&gt;documentation&lt;/li&gt;
&lt;li&gt;integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The barrier to building has unquestionably decreased.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ownership Has Not Become Cheaper&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the important distinction.&lt;/p&gt;

&lt;p&gt;AI can help create software. It does not eliminate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;operational support&lt;/li&gt;
&lt;li&gt;on-call responsibility&lt;/li&gt;
&lt;li&gt;compliance&lt;/li&gt;
&lt;li&gt;security&lt;/li&gt;
&lt;li&gt;upgrades&lt;/li&gt;
&lt;li&gt;platform ownership&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cost of creation is decreasing. The cost of ownership remains surprisingly stable. That means ownership becomes even more important in future build-versus-buy discussions.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;10. Final Thoughts&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One of the biggest lessons I've learned is that build-versus-buy decisions are rarely technology decisions.&lt;/p&gt;

&lt;p&gt;They're ownership decisions.&lt;/p&gt;

&lt;p&gt;Modern engineering teams can build almost anything. Open-source ecosystems are thriving. Cloud platforms provide powerful building blocks.&lt;/p&gt;

&lt;p&gt;AI accelerates development even further.&lt;/p&gt;

&lt;p&gt;The question is no longer: &lt;em&gt;Can we build it?&lt;/em&gt;&lt;br&gt;
The more important question is: &lt;em&gt;Do we want to own it?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Because every build decision creates a long-term commitment. A commitment to maintenance, operations, support, upgrades, and continuous evolution.&lt;/p&gt;

&lt;p&gt;Sometimes that commitment is absolutely worth making especially when the capability creates competitive advantage. Other times, the smarter decision is to leverage what already exists and focus engineering effort where it matters most. &lt;/p&gt;

&lt;p&gt;In the end, the most successful engineering organizations aren't the ones that build everything. They're the ones that understand what is truly worth owning.&lt;/p&gt;




&lt;p&gt;Assisted ChatGPT to rephrase. &lt;/p&gt;

</description>
      <category>discuss</category>
      <category>softwareengineering</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
