<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged distributedsystems</title>
<link>https://coderlegion.com/tag/distributedsystems</link>
<description>Powered by Question2Answer</description>
<item>
<title>Pub/Sub Isn't Event-Driven Architecture: Here's What Most Teams Miss</title>
<link>https://coderlegion.com/32197/pub-sub-isnt-event-driven-architecture-heres-what-most-teams-miss</link>
<description>&lt;p&gt;Pub/Sub is a messaging pattern. Event-Driven Architecture is an architectural discipline. Every EDA implementation may use Pub/Sub, but not every Pub/Sub implementation is truly Event-Driven Architecture.&lt;/p&gt;
&lt;p&gt;For many years, I would have agreed with the statement that Pub/Sub equals Event-Driven Architecture (EDA).&lt;/p&gt;
&lt;p&gt;Why? Because early discussions around EDA focused primarily on three characteristics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Real-time communication &lt;/li&gt;
&lt;li&gt;Publish/Subscribe messaging &lt;/li&gt;
&lt;li&gt;Asynchronous processing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At that time, implementing a Pub/Sub broker was often considered synonymous with implementing EDA.&lt;/p&gt;
&lt;p&gt;Today, however, the answer is no.&lt;/p&gt;
&lt;p&gt;As organizations have adopted EDA at scale, the discipline has evolved from a messaging concept into a comprehensive architectural practice. Modern EDA encompasses far more than simply publishing and subscribing to messages.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;📡EDA Is More Than Messaging&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A mature Event-Driven Architecture consists of multiple foundational elements:&lt;br&gt;
&lt;strong&gt;📝Event Definition&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Events must be clearly defined and categorized.&lt;/p&gt;
&lt;p&gt;Examples include:&lt;br&gt;
Business Events: CustomerRegistered, OrderPlaced, PaymentCompleted&lt;br&gt;
System Events: FileUploaded, ServiceStarted, DatabaseUpdated, EOD&lt;/p&gt;
&lt;p&gt;Without a consistent event model, organizations quickly end up with thousands of loosely defined messages that are difficult to understand and maintain.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;👤Event Ownership&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Just like data, events are business assets.&lt;/p&gt;
&lt;p&gt;Every event should have a clearly identified owner responsible for:&lt;br&gt;
Event definition&lt;br&gt;
Schema management&lt;br&gt;
Versioning&lt;br&gt;
Lifecycle governance&lt;/p&gt;
&lt;p&gt;When nobody owns an event, everyone assumes somebody else does.&lt;br&gt;
&lt;strong&gt;📏Event and Queue Standards&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Topic (event) and queue naming conventions form the foundation of a scalable EDA platform.&lt;br&gt;
A well-defined naming structure enables:&lt;br&gt;
Event discoverability&lt;br&gt;
Consistent routing&lt;br&gt;
Easier troubleshooting&lt;br&gt;
Better governance&lt;br&gt;
Without standards, the event landscape quickly becomes chaotic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;🔀Event Flow Design&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;EDA is not merely about sending messages.&lt;/p&gt;
&lt;p&gt;It is about how events collaborate to achieve business outcomes.&lt;/p&gt;
&lt;p&gt;Modern architectures require a clear understanding of:&lt;br&gt;
Event relationships&lt;br&gt;
Event choreography&lt;br&gt;
Event dependencies&lt;br&gt;
Business process visibility&lt;/p&gt;
&lt;p&gt;While services remain loosely coupled, the overall event flow must still be intentionally designed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;🛡️Event Governance&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Governance is the difference between a sustainable platform and an uncontrolled messaging ecosystem.&lt;/p&gt;
&lt;p&gt;Effective governance covers:&lt;br&gt;
Schema management&lt;br&gt;
Data classification&lt;br&gt;
Security controls&lt;br&gt;
Access management&lt;br&gt;
Event lifecycle management&lt;br&gt;
Compliance and auditing&lt;/p&gt;
&lt;p&gt;Governance should be embedded throughout the entire lifecycle, from design through implementation and operations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;📬Pub/Sub Is Only One Pattern Within EDA&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Pub/Sub is an important communication pattern, but it is only one building block of a broader architecture.&lt;br&gt;
A Pub/Sub platform without governance, ownership, standards, and event design is simply a collection of disconnected messages.&lt;/p&gt;
&lt;p&gt;In such environments, organizations commonly encounter problems such as:&lt;br&gt;
&lt;strong&gt;⚠️Consumer Failures&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Consumers may crash when:&lt;br&gt;
Unexpected event types are received&lt;br&gt;
Event schemas change without coordination&lt;br&gt;
Required fields are removed or modified&lt;br&gt;
Data Exposure Risks&lt;/p&gt;
&lt;p&gt;Sensitive information, including Personally Identifiable Information (PII), may be unintentionally delivered to systems or teams that should not have access to it.&lt;br&gt;
Message Loss or Misrouting&lt;/p&gt;
&lt;p&gt;Poorly governed subscriptions can result in:&lt;br&gt;
Events being consumed by unintended systems&lt;br&gt;
Important messages being ignored&lt;br&gt;
Business processes becoming unreliable&lt;br&gt;
Rising Infrastructure Costs&lt;/p&gt;
&lt;p&gt;Unnecessary event distribution increases:&lt;br&gt;
Network traffic&lt;br&gt;
Broker workload&lt;br&gt;
Infrastructure consumption&lt;br&gt;
Operational costs&lt;br&gt;
What begins as architectural flexibility can quickly become architectural sprawl.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;💡 Final Thoughts&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Many organizations begin their event-driven journey by deploying a message broker and enabling Pub/Sub communication. While this is an important first step, it is only the foundation.&lt;br&gt;
The real challenge is not moving events between systems. The challenge is managing events as long-lived business assets.&lt;/p&gt;
&lt;p&gt;Without clear ownership, governance, standards, and lifecycle management, organizations often end up with an event ecosystem that is difficult to understand, secure, and evolve. Teams gain asynchronous communication, but lose visibility, consistency, and control.&lt;/p&gt;
&lt;p&gt;Successful Event-Driven Architecture is not measured by how many events are produced each day. It is measured by how effectively those events enable business capabilities while remaining discoverable, governed, and trusted across the enterprise.&lt;/p&gt;
&lt;p&gt;In other words:&lt;br&gt;
A message broker can make your systems event-enabled.&lt;br&gt;
Governance, ownership, and standards make your organization truly event-driven.&lt;/p&gt;
&lt;p&gt;❓ What do you think? Have you seen organizations mistake Pub/Sub implementation for Event-Driven Architecture? What governance challenges emerged as event adoption grew?&lt;/p&gt;
&lt;p&gt;This typically drives significantly more comments and discussion from architects, developers, and integration professionals.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32197/pub-sub-isnt-event-driven-architecture-heres-what-most-teams-miss</guid>
<pubDate>Fri, 23 Oct 2026 14:38:21 +0000</pubDate>
</item>
<item>
<title>🚦 EDA Without Governance: A Fast Track to Enterprise Chaos</title>
<link>https://coderlegion.com/31224/eda-without-governance-a-fast-track-to-enterprise-chaos</link>
<description>&lt;p&gt;In today's digital enterprise, Event-Driven Architecture (EDA) has become a preferred architectural pattern for building scalable, loosely coupled, and real-time systems. By enabling applications to publish and subscribe to events independently, EDA provides the flexibility and agility required to support modern business demands.&lt;/p&gt;
&lt;p&gt;However, there is a critical truth that many organizations discover only after their event ecosystem starts to grow:&lt;/p&gt;
&lt;p&gt;⚠️ The success of EDA depends less on technology and more on governance.&lt;/p&gt;
&lt;p&gt;Without proper governance, an event bus can quickly evolve into a chaotic integration landscape where dependencies are unclear, ownership becomes fragmented, and changes introduce widespread downstream impacts.&lt;/p&gt;
&lt;p&gt;🌐 EDA Is More Than Event Distribution&lt;/p&gt;
&lt;p&gt;EDA is fundamentally a distribution design that allows applications to publish and consume events across the enterprise.&lt;/p&gt;
&lt;p&gt;✅ New publishers can share information with multiple consumers.&lt;br&gt;
✅ New consumers can subscribe to existing event streams.&lt;br&gt;
✅ Applications remain loosely coupled and independently deployable.&lt;/p&gt;
&lt;p&gt;But this flexibility comes with responsibility.&lt;/p&gt;
&lt;p&gt;An event published today may be consumed by numerous applications, either directly or indirectly. As adoption grows, every design decision becomes part of a shared enterprise contract.&lt;/p&gt;
&lt;p&gt;Without governance, what starts as flexibility can quickly become complexity.&lt;/p&gt;
&lt;p&gt;⚠️ The Hidden Risk of Uncontrolled Growth&lt;/p&gt;
&lt;p&gt;When onboarding new applications into an event-driven ecosystem, governance standards and processes should be established before implementation begins.&lt;/p&gt;
&lt;p&gt;Without these controls:&lt;/p&gt;
&lt;p&gt;🔴 Topic structures become inconsistent.&lt;br&gt;
🔴 Applications subscribe to unnecessary events.&lt;br&gt;
🔴 Event routing becomes difficult to understand and maintain.&lt;br&gt;
🔴 Sensitive data may be exposed to unauthorized consumers.&lt;br&gt;
🔴 Small changes trigger large-scale application rework.&lt;/p&gt;
&lt;p&gt;The impact is not limited to event broker configurations.&lt;/p&gt;
&lt;p&gt;A seemingly simple change to an event topic, payload structure, or routing pattern may require modifications across multiple consuming applications, testing cycles, operational procedures, and business processes.&lt;/p&gt;
&lt;p&gt;In large-scale enterprises, a single event redesign can affect dozens of interconnected systems.&lt;/p&gt;
&lt;p&gt;🔄 Governance Must Cover the Entire Lifecycle&lt;/p&gt;
&lt;p&gt;EDA governance is not a one-time architecture review.&lt;/p&gt;
&lt;p&gt;It should span the entire delivery lifecycle:&lt;/p&gt;
&lt;p&gt;📋 Design&lt;br&gt;
👨💻 Development&lt;br&gt;
🧪 Testing&lt;br&gt;
🚀 Deployment&lt;br&gt;
⚙️ Production Operations&lt;/p&gt;
&lt;p&gt;Any governance gap can result in:&lt;/p&gt;
&lt;p&gt;❌ Increased technical debt&lt;br&gt;
❌ Integration failures&lt;br&gt;
❌ Security and compliance risks&lt;br&gt;
❌ Operational complexity&lt;br&gt;
❌ Higher maintenance costs&lt;/p&gt;
&lt;p&gt;The most successful event-driven organizations balance architectural flexibility with disciplined governance.&lt;/p&gt;
&lt;p&gt;🎯 Key Areas of EDA Governance&lt;/p&gt;
&lt;p&gt;🏷️ 1. Topic Naming Standards&lt;/p&gt;
&lt;p&gt;Topics are the foundation of event discoverability and maintainability.&lt;/p&gt;
&lt;p&gt;Every topic should follow a mandatory naming standard that clearly identifies:&lt;/p&gt;
&lt;p&gt;✅ Business domain&lt;br&gt;
✅ Business capability&lt;br&gt;
✅ Event type&lt;br&gt;
✅ Environment&lt;/p&gt;
&lt;p&gt;A consistent naming convention helps architects, developers, and support teams quickly understand event purpose and ownership.&lt;/p&gt;
&lt;p&gt;Good governance starts with naming things correctly.&lt;/p&gt;
&lt;p&gt;📬 2. Queue Subscription Governance&lt;/p&gt;
&lt;p&gt;Applications should subscribe only to events that are required for their business functions.&lt;/p&gt;
&lt;p&gt;Governance should ensure:&lt;/p&gt;
&lt;p&gt;✅ Controlled subscription approval&lt;br&gt;
✅ Consumer registration and ownership&lt;br&gt;
✅ Dependency visibility&lt;br&gt;
✅ Periodic subscription reviews&lt;/p&gt;
&lt;p&gt;This prevents unnecessary event consumption and reduces future change impacts.&lt;/p&gt;
&lt;p&gt;Not every application needs every event.&lt;/p&gt;
&lt;p&gt;🔀 3. Event Flow Design&lt;/p&gt;
&lt;p&gt;A well-designed event flow enables systems to evolve without disrupting the broader ecosystem.&lt;/p&gt;
&lt;p&gt;Governance should enforce:&lt;/p&gt;
&lt;p&gt;✅ Standard routing principles&lt;br&gt;
✅ Clear producer and consumer responsibilities&lt;br&gt;
✅ Traceable event paths&lt;br&gt;
✅ Flexible addition and removal of components&lt;/p&gt;
&lt;p&gt;A properly governed event flow allows organizations to grow their architecture while maintaining control and predictability.&lt;/p&gt;
&lt;p&gt;Flexibility should be intentional, not accidental.&lt;/p&gt;
&lt;p&gt;🔐 4. Data Governance and Access Control&lt;/p&gt;
&lt;p&gt;Not every consumer should have access to every event.&lt;/p&gt;
&lt;p&gt;Organizations must ensure applications can subscribe only to authorized events, especially when they contain:&lt;/p&gt;
&lt;p&gt;🔒 Personally Identifiable Information (PII)&lt;br&gt;
🔒 Customer information&lt;br&gt;
🔒 Financial transactions&lt;br&gt;
🔒 Business-sensitive data&lt;/p&gt;
&lt;p&gt;Strong governance ensures:&lt;/p&gt;
&lt;p&gt;✅ Data privacy&lt;br&gt;
✅ Regulatory compliance&lt;br&gt;
✅ Security controls&lt;br&gt;
✅ Consumer authorization&lt;/p&gt;
&lt;p&gt;The principle should be simple:&lt;/p&gt;
&lt;p&gt;🔑 Subscribe only to the data you are authorized to consume.&lt;/p&gt;
&lt;p&gt;🏆 Governance Determines EDA Success&lt;/p&gt;
&lt;p&gt;Many organizations focus heavily on event brokers, event meshes, streaming platforms, and messaging technologies.&lt;/p&gt;
&lt;p&gt;While these technologies are important, they are not what ultimately determines the success of an Event-Driven Architecture.&lt;/p&gt;
&lt;p&gt;The true differentiator is governance.&lt;/p&gt;
&lt;p&gt;Well-defined standards, ownership models, review processes, security controls, and lifecycle management enable organizations to scale their event ecosystem confidently and sustainably.&lt;/p&gt;
&lt;p&gt;Without governance, flexibility becomes chaos.&lt;/p&gt;
&lt;p&gt;With governance, EDA becomes a strategic enabler of enterprise agility, innovation, and business growth.&lt;/p&gt;
&lt;p&gt;💡 Final Thought&lt;/p&gt;
&lt;p&gt;🚦 EDA without governance is simply message transportation.&lt;/p&gt;
&lt;p&gt;🚀 EDA with governance becomes sustainable enterprise architecture.&lt;/p&gt;
&lt;p&gt;The question is not whether your organization should adopt Event-Driven Architecture.&lt;/p&gt;
&lt;p&gt;The real question is:&lt;/p&gt;
&lt;p&gt;Do you have the governance needed to keep it from becoming enterprise chaos?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31224/eda-without-governance-a-fast-track-to-enterprise-chaos</guid>
<pubDate>Thu, 15 Oct 2026 16:03:25 +0000</pubDate>
</item>
<item>
<title>Keeping a Legacy Cache Consistent During an Incremental Migration</title>
<link>https://coderlegion.com/31009/keeping-a-legacy-cache-consistent-during-an-incremental-migration</link>
<description>&lt;p&gt;During an incremental system migration, I ran into a subtle consistency problem: our new service could successfully update the database while the legacy service continued reading stale data from its own in-memory cache.&lt;/p&gt;
&lt;p&gt;The obvious solutions such as distributed locks, cache invalidation, or adding another piece of infrastructure didn't fit our constraints.&lt;/p&gt;
&lt;p&gt;The solution ended up being much simpler: &lt;strong&gt;the legacy system already had a lease mechanism that we could reuse.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is the story of how I found it, how we used it, and how we handled the failure cases.&lt;/p&gt;
&lt;h3 id=&quot;architecture&quot;&gt;Architecture&lt;/h3&gt;
&lt;p&gt;Our system currently has three main components:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Main Service:&lt;/strong&gt; The client-facing service that sends business requests.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Legacy Service:&lt;/strong&gt; An older, high-performance engine that maintains an embedded in-memory cache. It loads a subset of database tables into its own memory for fast access.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;New Service:&lt;/strong&gt; A modern service that is gradually replacing the Legacy Service using the Strangler Fig pattern.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The New Service currently acts as a proxy between the Main Service and the Legacy Service.&lt;/p&gt;
&lt;p&gt;If an endpoint has already been migrated, the New Service handles the request directly. Otherwise, it forwards the request to the Legacy Service.&lt;/p&gt;
&lt;p&gt;The architecture looks roughly like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/04m0jg5bf6d3qlo9ermn.png&quot; alt=&quot;System architecture diagram&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-problem&quot;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;The problem appeared when the New Service handled a migrated request.&lt;/p&gt;
&lt;p&gt;The New Service writes updates directly to the database. However, the Legacy Service has its own in-memory copy of the data and has no way of knowing that the database changed.&lt;/p&gt;
&lt;p&gt;That creates a dangerous sequence:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The Legacy Service caches &lt;code&gt;status = pending&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A request is migrated to the New Service.&lt;/li&gt;
&lt;li&gt;The New Service updates the database to &lt;code&gt;status = completed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A later request reaches the Legacy Service.&lt;/li&gt;
&lt;li&gt;The Legacy Service reads &lt;code&gt;status = pending&lt;/code&gt; from its local cache.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The database is correct, but the Legacy Service is operating on stale state.&lt;/p&gt;
&lt;p&gt;We discovered these issues during QA testing. As more endpoints moved to the New Service, the problem would become increasingly common.&lt;/p&gt;
&lt;p&gt;So we needed a way to guarantee that the Legacy Service could not continue operating on stale data while a migrated endpoint was modifying the same resource.&lt;/p&gt;
&lt;h3 id=&quot;the-constraints&quot;&gt;The Constraints&lt;/h3&gt;
&lt;p&gt;The textbook solutions weren't a good fit for this system.&lt;/p&gt;
&lt;p&gt;We had several constraints:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The Legacy and Main Services should not be modified.&lt;/strong&gt; The Legacy Service is largely a black box, so modifying its behavior is risky.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Legacy Service could not temporarily operate on stale data.&lt;/strong&gt; For these operations, we needed to prevent conflicting legacy operations rather than accept eventual consistency.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Legacy Service had limited cache synchronization capabilities.&lt;/strong&gt; It automatically reloaded its cache only periodically, or through a manual refresh operation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;We wanted to avoid introducing new infrastructure.&lt;/strong&gt; Adding something like Redis purely for distributed locking would introduce another dependency to provision, operate, and maintain.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These constraints ruled out several conventional approaches.&lt;/p&gt;
&lt;h3 id=&quot;the-naive-solution&quot;&gt;The Naive Solution&lt;/h3&gt;
&lt;p&gt;My first thought was:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;What if the New Service acquires a distributed lock, performs the database update, and then triggers a cache refresh on the Legacy Service?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That sounds reasonable, but there were two problems.&lt;/p&gt;
&lt;h4 id=&quot;1-the-legacy-service-wouldn-t-know-about-the-lock&quot;&gt;1. The Legacy Service wouldn't know about the lock&lt;/h4&gt;
&lt;p&gt;An external distributed lock only coordinates operations if the participating systems can use it.&lt;/p&gt;
&lt;p&gt;The Legacy Service was a black box and wouldn't check our new lock before accessing its local cache.&lt;/p&gt;
&lt;p&gt;We could add that behavior, but that violated one of our main constraints.&lt;/p&gt;
&lt;h4 id=&quot;2-refreshing-the-entire-cache-was-too-expensive&quot;&gt;2. Refreshing the entire cache was too expensive&lt;/h4&gt;
&lt;p&gt;The Legacy Service's manual refresh operation reloaded a large amount of data.&lt;/p&gt;
&lt;p&gt;Doing that after every migrated request would introduce significant latency and unnecessary work.&lt;/p&gt;
&lt;p&gt;We needed something more targeted.&lt;/p&gt;
&lt;h3 id=&quot;the-solution-reusing-an-existing-lease&quot;&gt;The Solution: Reusing an Existing Lease&lt;/h3&gt;
&lt;p&gt;While investigating the Legacy Service's existing APIs, I found something useful: it already had a built-in &lt;strong&gt;lease mechanism&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The Legacy Service exposed APIs to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Acquire a lease for a specific resource.&lt;/li&gt;
&lt;li&gt;Release the lease when the operation was complete.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;More importantly, releasing the lease had an existing side effect: &lt;strong&gt;the Legacy Service refreshed the cached data associated with that resource.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The lease mechanism was already part of how the Legacy Service protected its own critical sections.&lt;/p&gt;
&lt;p&gt;This changed the problem completely.&lt;/p&gt;
&lt;p&gt;Instead of introducing a new distributed lock, we could use the same coordination mechanism the Legacy Service already understood.&lt;/p&gt;
&lt;p&gt;The New Service could wrap a migrated operation like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Acquire lease for the resource
2. Update the database
3. Perform business logic
4. Release the lease
5. Legacy Service refreshes that resource's cached data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important part is that the lease wasn't merely an arbitrary lock exposed by the Legacy Service. The Legacy Service's own operations already align with the same lease mechanism.&lt;/p&gt;
&lt;p&gt;That gave us two properties we needed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Conflicting Legacy Service operations could not proceed while the resource was leased.&lt;/li&gt;
&lt;li&gt;Releasing the lease caused the Legacy Service to refresh the affected resource rather than performing a full cache reload.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So instead of adding Redis, Kafka, or another synchronization system, I reused a primitive that already existed in the system.&lt;/p&gt;
&lt;h3 id=&quot;what-if-releasing-the-lease-fails-&quot;&gt;What If Releasing the Lease Fails?&lt;/h3&gt;
&lt;p&gt;The happy path was straightforward.&lt;/p&gt;
&lt;p&gt;But distributed systems rarely fail only on the happy path.&lt;/p&gt;
&lt;p&gt;Consider this sequence:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;New Service
    |
    | acquire lease
    v
Legacy Service
    |
    | lease acquired
    |
    v
New Service
    |
    | update database
    |
    X---- network failure ----&amp;gt; release request never arrives
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The database update succeeded, but the release request never reached the Legacy Service.&lt;/p&gt;
&lt;p&gt;If leases could remain indefinitely, the resource could become permanently locked.&lt;/p&gt;
&lt;p&gt;So I looked at how the Legacy Service handled abandoned leases.&lt;/p&gt;
&lt;p&gt;It already had two mechanisms:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TTL (Time-To-Live):&lt;/strong&gt; Every lease automatically expires after a configured duration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Background reaper:&lt;/strong&gt; A background process periodically detects expired leases and cleans them up.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even better, lease cleanup also triggered the same cache refresh behavior.&lt;/p&gt;
&lt;p&gt;That meant the failure path became:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Acquire lease
     |
     v
Update database
     |
     X  release request lost
     |
     v
Lease expires
     |
     v
Background reaper
     |
     v
Lease cleared + cache refreshed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We therefore didn't need to build a separate retry system just to recover from a lost release request.&lt;/p&gt;
&lt;p&gt;The existing lease lifecycle already provided a recovery mechanism.&lt;/p&gt;
&lt;h4 id=&quot;an-important-invariant&quot;&gt;An Important Invariant&lt;/h4&gt;
&lt;p&gt;The key correctness property here is that the resource remains protected by the lease until the Legacy Service releases or expires it.&lt;/p&gt;
&lt;p&gt;As long as the database update completes before the lease is released or expires, the Legacy Service cannot resume normal access to the resource using its old cached state during that protected window.&lt;/p&gt;
&lt;p&gt;When the lease is eventually cleaned up, the cache refresh brings the Legacy Service back in sync with the database.&lt;/p&gt;
&lt;p&gt;This is an important distinction: &lt;strong&gt;the TTL isn't what provides consistency by itself.&lt;/strong&gt; The consistency comes from the lease preventing conflicting access; the TTL and reaper provide failure recovery when the normal release path doesn't happen.&lt;/p&gt;
&lt;h3 id=&quot;the-tradeoff&quot;&gt;The Tradeoff&lt;/h3&gt;
&lt;p&gt;The solution wasn't free.&lt;/p&gt;
&lt;p&gt;The main cost was &lt;strong&gt;latency&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If a resource has many related records that need to be refreshed, releasing the lease and synchronizing the cache can take noticeable time and I verified this through tests.&lt;/p&gt;
&lt;p&gt;So I didn't view this as a permanent architecture.&lt;/p&gt;
&lt;p&gt;It's a &lt;strong&gt;migration bridge&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;During the migration, migrated endpoints use the lease mechanism to safely coordinate with the Legacy Service.&lt;/p&gt;
&lt;p&gt;As more functionality moves to the New Service, individual resources eventually become fully owned by the New Service.&lt;/p&gt;
&lt;p&gt;At that point, the Legacy Service no longer needs to read or modify those resources.&lt;/p&gt;
&lt;p&gt;That's our exit condition.&lt;/p&gt;
&lt;p&gt;Once a resource is completely owned by the New Service, we can remove the lease and cache-synchronization logic for it.&lt;/p&gt;
&lt;p&gt;This is important because migration code has a tendency to become permanent if nobody defines when it can be deleted. So I documented this as well.&lt;/p&gt;
&lt;h3 id=&quot;what-i-learned&quot;&gt;What I Learned&lt;/h3&gt;
&lt;h4 id=&quot;1-start-with-invariants-not-technologies&quot;&gt;1. Start with invariants, not technologies&lt;/h4&gt;
&lt;p&gt;My first instinct was to think about distributed locks and infrastructure such as Redis.&lt;/p&gt;
&lt;p&gt;But the more useful question was:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;What must never happen?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In this case, the important invariant was that the Legacy Service must not perform conflicting operations against a resource while the New Service is modifying it.&lt;/p&gt;
&lt;p&gt;Once that was clear, the technology choice became much easier.&lt;/p&gt;
&lt;h4 id=&quot;2-existing-systems-often-contain-useful-primitives&quot;&gt;2. Existing systems often contain useful primitives&lt;/h4&gt;
&lt;p&gt;At first, I thought of the Legacy Service as something we couldn't modify and therefore mostly had to work around.&lt;/p&gt;
&lt;p&gt;But &quot;don't modify the legacy system&quot; doesn't mean &quot;don't understand the legacy system.&quot;&lt;/p&gt;
&lt;p&gt;By digging into its existing APIs and lease lifecycle, I found a mechanism that already provided most of the coordination and recovery behavior we needed.&lt;/p&gt;
&lt;p&gt;Sometimes the best solution isn't introducing another system. It's discovering what the current system can already do.&lt;/p&gt;
&lt;h4 id=&quot;3-temporary-migration-code-needs-an-exit-condition&quot;&gt;3. Temporary migration code needs an exit condition&lt;/h4&gt;
&lt;p&gt;A migration introduces a lot of code that ideally won't exist forever.&lt;/p&gt;
&lt;p&gt;The lease wrapper is useful while ownership is split between the Legacy and New Services. But once the New Service completely owns a resource, keeping the wrapper would only add unnecessary latency and complexity.&lt;/p&gt;
&lt;p&gt;Defining the exact conditions under which the lease logic can be removed makes it much less likely that temporary migration infrastructure becomes permanent technical debt.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;This experience changed how I think about solving distributed systems problems.&lt;/p&gt;
&lt;p&gt;It's easy to reach for familiar tools like Redis for locking, Kafka for asynchronous communication, an outbox for reliable delivery.&lt;/p&gt;
&lt;p&gt;Sometimes those are exactly the right tools.&lt;/p&gt;
&lt;p&gt;But before introducing another component, it's worth understanding the guarantees and primitives that already exist in the system.&lt;/p&gt;
&lt;p&gt;In this case, the legacy system already had a resource-level lease, automatic expiration, a background reaper, and targeted cache synchronization.&lt;/p&gt;
&lt;p&gt;The challenge wasn't building a new distributed system.&lt;/p&gt;
&lt;p&gt;The best solution wasn't to build another system around the legacy one. It was to understand the system we already had well enough to make it work for us.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31009/keeping-a-legacy-cache-consistent-during-an-incremental-migration</guid>
<pubDate>Wed, 14 Oct 2026 04:04:39 +0000</pubDate>
</item>
<item>
<title>Realtime Is a Hint, Not Your Source of Truth</title>
<link>https://coderlegion.com/27482/realtime-is-a-hint-not-your-source-of-truth</link>
<description>&lt;p&gt;A notification badge looks like a small feature. In a distributed application, it can expose a large architectural assumption.&lt;/p&gt;
&lt;p&gt;I recently investigated a badge that sometimes stayed stale in an ASP.NET Core and Blazor application running across multiple server instances. The notification itself had been created successfully. Durable state was correct. A separate push path could also complete. Yet the visible count did not always change until the user left the screen and returned.&lt;/p&gt;
&lt;p&gt;The bug was not really about arithmetic. It was about assigning too much authority to a realtime transport.&lt;/p&gt;
&lt;h3 id=&quot;the-hidden-assumption&quot;&gt;The hidden assumption&lt;/h3&gt;
&lt;p&gt;The UI initially loaded its count from the server and then relied on live events to remain current. That feels reasonable while developing against one server with a stable connection. In production, several ordinary conditions weaken the assumption:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The client can disconnect briefly.&lt;/li&gt;
&lt;li&gt;A load balancer can place the sender and receiver on different instances.&lt;/li&gt;
&lt;li&gt;A workflow can create the same durable state without passing through the page that happens to broadcast.&lt;/li&gt;
&lt;li&gt;A transport or hub can fail after the database operation has already succeeded.&lt;/li&gt;
&lt;li&gt;An idempotent retry can revisit work that should not increment the UI again.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these conditions makes realtime technology a poor choice. They simply mean that a realtime message should usually be treated as a hint that fresh state is available, not as the state itself.&lt;/p&gt;
&lt;h3 id=&quot;a-three-layer-reliability-pattern&quot;&gt;A three-layer reliability pattern&lt;/h3&gt;
&lt;p&gt;The repair became much clearer once the responsibilities were separated into three layers.&lt;/p&gt;
&lt;h4 id=&quot;first-persist-the-truth&quot;&gt;First, persist the truth&lt;/h4&gt;
&lt;p&gt;The durable record must be committed independently of whether any connected client is listening. If the user closes the app, changes networks, or connects to another instance, the correct unread state must still be recoverable later.&lt;/p&gt;
&lt;p&gt;This ordering matters. Broadcasting first can produce a convincing UI update for data that subsequently fails to commit. Persisting first means a missed broadcast can be healed; an invented broadcast cannot be trusted.&lt;/p&gt;
&lt;h4 id=&quot;second-broadcast-from-the-business-chokepoint&quot;&gt;Second, broadcast from the business chokepoint&lt;/h4&gt;
&lt;p&gt;The live update originally depended on a narrower UI workflow. Other creation paths could produce the same durable result without emitting the event. Moving the broadcast to the shared dispatch path made the behavior consistent across interactive and background work.&lt;/p&gt;
&lt;p&gt;The broadcast was also tied to the outcome of the durable write. A genuinely new record emits a hint. An idempotent replay that finds the work already completed does not. That prevents duplicate increments and makes the event reflect a real state transition rather than merely an attempted command.&lt;/p&gt;
&lt;h4 id=&quot;third-reconcile-periodically&quot;&gt;Third, reconcile periodically&lt;/h4&gt;
&lt;p&gt;Even a correctly placed broadcast can be missed. The client therefore refreshes the authoritative count on a bounded interval. The loop is cancelled when the component is disposed, and a transient refresh failure does not kill future attempts.&lt;/p&gt;
&lt;p&gt;This is not polling as the primary experience. Realtime still gives the fast path. Polling is the repair path.&lt;/p&gt;
&lt;h3 id=&quot;the-trade-off&quot;&gt;The trade-off&lt;/h3&gt;
&lt;p&gt;Hybrid delivery is not free.&lt;/p&gt;
&lt;p&gt;Periodic reconciliation adds read traffic. The interval also defines a maximum period of visible staleness when a realtime event is missed. A very short interval increases load and can waste battery or bandwidth on mobile clients. A long interval lowers cost but leaves stale UI visible for longer.&lt;/p&gt;
&lt;p&gt;That makes the interval a product and operational decision, not a magic constant. Ask how damaging stale information is, how expensive the read is, how many active clients exist, and whether the endpoint can use a cheap aggregate or cache.&lt;/p&gt;
&lt;p&gt;For a badge, bounded staleness may be acceptable. For a payment confirmation or permission change, the interaction may require an immediate authoritative read instead. The pattern stays the same, but the recovery budget changes.&lt;/p&gt;
&lt;p&gt;Failure boundaries matter too. Once durable work has succeeded, a hub failure should not turn the whole operation into a failure. Otherwise a temporary transport problem can encourage retries of already-completed business work. Log the broadcast failure, observe it, and let reconciliation repair the presentation.&lt;/p&gt;
&lt;h3 id=&quot;test-the-contract-not-the-transport&quot;&gt;Test the contract, not the transport&lt;/h3&gt;
&lt;p&gt;The most useful tests were not attempts to prove that a network never fails. They pinned the business boundary:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;New durable state produces one realtime hint.&lt;/li&gt;
&lt;li&gt;Previously completed or deduplicated work produces no new hint.&lt;/li&gt;
&lt;li&gt;A failing realtime transport does not invalidate successful durable work.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Client-side tests can add the complementary guarantees: an initial authoritative load occurs, the reconciliation loop refreshes state, transient errors do not stop later ticks, and disposal cancels the loop.&lt;/p&gt;
&lt;p&gt;These tests document the intended hierarchy. The database-backed read is authoritative. Realtime reduces latency. Reconciliation restores convergence.&lt;/p&gt;
&lt;h3 id=&quot;a-practical-review-checklist&quot;&gt;A practical review checklist&lt;/h3&gt;
&lt;p&gt;When reviewing a live-update feature, I now ask five questions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What is the source of truth?&lt;/li&gt;
&lt;li&gt;Where is the single shared point that knows a real state transition occurred?&lt;/li&gt;
&lt;li&gt;Can the live event be safely missed or duplicated?&lt;/li&gt;
&lt;li&gt;How and how quickly does the client converge again?&lt;/li&gt;
&lt;li&gt;What happens if the transport fails after the business operation succeeds?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If those answers are vague, the feature may look realtime in a demo while remaining fragile in production.&lt;/p&gt;
&lt;p&gt;The broader lesson is modest: reliability often comes from combining mechanisms with explicit roles. Durable storage provides truth. Realtime provides speed. Periodic reconciliation provides healing. None needs to pretend it can do the others’ job.&lt;/p&gt;
&lt;p&gt;That separation made a small badge easier to trust, and it is a pattern I would reuse for dashboards, counters, presence indicators, job progress, and other derived UI state.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27482/realtime-is-a-hint-not-your-source-of-truth</guid>
<pubDate>Thu, 17 Sep 2026 08:20:15 +0000</pubDate>
</item>
<item>
<title>Why Local LLM Inference Needs Admission Control Before It Needs More Hardware</title>
<link>https://coderlegion.com/26773/why-local-llm-inference-needs-admission-control-before-it-needs-more-hardware</link>
<description>&lt;p&gt;Running an LLM locally is easy to demonstrate.&lt;/p&gt;
&lt;p&gt;Start Ollama or vLLM, load a model, send a request, and watch it generate tokens.&lt;/p&gt;
&lt;p&gt;The interesting problems start when you send more than one kind of request.&lt;/p&gt;
&lt;p&gt;Imagine a local inference server handling two workloads:&lt;/p&gt;
&lt;p&gt;Interactive requests where a user is waiting for an answer.&lt;br&gt;
Background jobs generating longer responses that can tolerate delay.&lt;/p&gt;
&lt;p&gt;With plenty of spare capacity, both workloads coexist peacefully.&lt;/p&gt;
&lt;p&gt;Under contention, they don't.&lt;/p&gt;
&lt;p&gt;A few large background requests can occupy enough inference capacity that interactive requests wait behind work that was never latency-sensitive in the first place.&lt;/p&gt;
&lt;p&gt;At that point, adding another HTTP worker or increasing a concurrency limit doesn't necessarily solve the problem.&lt;/p&gt;
&lt;p&gt;You have a scheduling problem.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Concurrency Is Not Capacity Management&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A common first approach is to put a semaphore around inference:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;maximum concurrent requests = 4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's useful. It prevents unlimited work from entering the system.&lt;/p&gt;
&lt;p&gt;But it doesn't answer a more important question:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Which four requests should be allowed to run?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Suppose all four slots are occupied by long-running batch generations.&lt;/p&gt;
&lt;p&gt;An interactive request arrives.&lt;/p&gt;
&lt;p&gt;The concurrency limit is working exactly as designed, yet the user still waits.&lt;/p&gt;
&lt;p&gt;The system protected itself from overload, but it didn't protect the workload that mattered most.&lt;/p&gt;
&lt;p&gt;This is where admission control becomes different from simple concurrency limiting.&lt;/p&gt;
&lt;p&gt;Concurrency asks:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;How much work can execute simultaneously?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Admission control asks:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Which work should consume scarce capacity right now?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tokens Make the Problem More Interesting&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;LLM workloads also aren't uniform.&lt;/p&gt;
&lt;p&gt;Consider two requests:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Request A
input: 500 tokens
maximum output: 200 tokens
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;Request B
input: 2,000 tokens
maximum output: 4,000 tokens
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Counting both as &quot;one request&quot; hides a large difference in potential resource consumption.&lt;/p&gt;
&lt;p&gt;A better admission decision can account for both request concurrency and estimated token footprint.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;estimated footprint = input tokens + maximum output tokens
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Before admitting a request, the controller asks whether enough capacity remains for that footprint.&lt;/p&gt;
&lt;p&gt;The estimate doesn't need to predict the exact number of generated tokens perfectly. It needs to provide a useful bound for deciding whether admitting more work is safe.&lt;/p&gt;
&lt;p&gt;When the request finishes, unused capacity can be returned.&lt;/p&gt;
&lt;p&gt;This gives the system another dimension of protection beyond request count alone.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Static Limits Still Leave a Policy Problem&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You can go further and reserve capacity.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Interactive: 3 slots
Batch:       1 slot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now interactive traffic is protected.&lt;/p&gt;
&lt;p&gt;But you've created another problem.&lt;/p&gt;
&lt;p&gt;What happens when there are no interactive requests?&lt;/p&gt;
&lt;p&gt;If three reserved slots sit unused while batch work waits, utilization suffers.&lt;/p&gt;
&lt;p&gt;The obvious improvement is borrowing.&lt;/p&gt;
&lt;p&gt;Let batch jobs temporarily consume unused interactive capacity:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Interactive demand low
        ↓
Batch borrows spare capacity
        ↓
Interactive demand returns
        ↓
Borrowed capacity must be restored
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That last step is where things become interesting.&lt;/p&gt;
&lt;p&gt;Borrowing idle capacity is easy.&lt;/p&gt;
&lt;p&gt;Safely reclaiming it is the hard part.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Borrowing Without Restoration Isn't Really a Reservation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Suppose an interactive class is guaranteed three units of capacity.&lt;/p&gt;
&lt;p&gt;During an idle period, batch workloads borrow two of them.&lt;/p&gt;
&lt;p&gt;Then interactive traffic suddenly returns.&lt;/p&gt;
&lt;p&gt;If those batch requests can't be interrupted, the interactive reservation doesn't immediately exist anymore. It exists only after the borrowers finish.&lt;/p&gt;
&lt;p&gt;That means the actual guarantee depends on restoration behavior.&lt;/p&gt;
&lt;p&gt;A useful reservation policy therefore needs to define more than:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;interactive floor = 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It also needs to answer:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;How quickly can that floor become usable again?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This distinction matters especially with inference because some resources can be reclaimed immediately while others cannot.&lt;/p&gt;
&lt;p&gt;A controller may stop granting new batch work immediately.&lt;/p&gt;
&lt;p&gt;But an upstream inference request already running on a GPU may continue until completion.&lt;/p&gt;
&lt;p&gt;So &quot;capacity restored&quot; can mean at least two different things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The scheduler has restored the entitlement.&lt;/li&gt;
&lt;li&gt;The physical resource is actually available again.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Those are not necessarily the same moment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Testing This Against a Real Local Model&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I've been experimenting with this problem while building MoFlux, an admission-control system for shared workloads.&lt;/p&gt;
&lt;p&gt;One of the useful milestones was running the controller against a real local Ollama instance instead of only synthetic workloads.&lt;/p&gt;
&lt;p&gt;The first result wasn't interesting because MoFlux somehow made the model faster.&lt;/p&gt;
&lt;p&gt;It didn't.&lt;/p&gt;
&lt;p&gt;The interesting result was that the admission policy could operate in front of real local inference and observe the same contention problem that appears in simulations.&lt;/p&gt;
&lt;p&gt;That changes what can be tested.&lt;/p&gt;
&lt;p&gt;Instead of asking only:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Does the controller make the correct decision?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;we can ask:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;What happens to real inference when interactive and batch workloads&lt;br&gt;
compete for the same model?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now measurements such as these become meaningful:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Time to first token&lt;/li&gt;
&lt;li&gt;End-to-end latency&lt;/li&gt;
&lt;li&gt;Interactive success rate&lt;/li&gt;
&lt;li&gt;Batch throughput&lt;/li&gt;
&lt;li&gt;Admission rejection rate&lt;/li&gt;
&lt;li&gt;Capacity restoration time&lt;/li&gt;
&lt;li&gt;Protected-capacity violations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is where benchmarks start becoming more useful than raw throughput numbers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Throughput Is Not Always the Goal&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Imagine two configurations.&lt;/p&gt;
&lt;p&gt;Configuration A completes 110 requests per minute.&lt;/p&gt;
&lt;p&gt;Configuration B completes 100.&lt;/p&gt;
&lt;p&gt;At first glance, A wins.&lt;/p&gt;
&lt;p&gt;But suppose under A:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;interactive latency becomes unpredictable,&lt;/li&gt;
&lt;li&gt;user-facing requests frequently miss their SLO,&lt;/li&gt;
&lt;li&gt;background work dominates capacity during bursts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Configuration B might complete fewer total requests while completing substantially more useful requests within their objectives.&lt;/p&gt;
&lt;p&gt;That's the difference between throughput and goodput.&lt;/p&gt;
&lt;p&gt;For shared inference infrastructure, the question isn't simply:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;How many generations can this GPU complete?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A more useful question is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;How much useful work can this GPU complete while preserving the&lt;br&gt;
guarantees of the workloads sharing it?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is a capacity-allocation problem.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why This Matters for Local Inference&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Local inference is becoming increasingly practical.&lt;/p&gt;
&lt;p&gt;Developers are running models on workstations, edge machines, private servers, and internal GPU clusters.&lt;/p&gt;
&lt;p&gt;The first optimization instinct is often hardware-oriented:&lt;/p&gt;
&lt;p&gt;Need more throughput?&lt;br&gt;
Buy a faster GPU.&lt;/p&gt;
&lt;p&gt;Sometimes that's the right answer.&lt;/p&gt;
&lt;p&gt;But if the actual problem is uncontrolled contention between workloads, more hardware can simply postpone the same failure mode.&lt;/p&gt;
&lt;p&gt;Before scaling hardware, it's worth understanding:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Who is consuming capacity?&lt;/p&gt;
&lt;p&gt;How much are they allowed to consume?&lt;/p&gt;
&lt;p&gt;Which workloads need protection?&lt;/p&gt;
&lt;p&gt;Can idle capacity be borrowed?&lt;/p&gt;
&lt;p&gt;How quickly can it be reclaimed?&lt;/p&gt;
&lt;p&gt;What happens when demand suddenly changes?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Those questions exist whether your inference server is a laptop running Ollama or a production cluster serving thousands of requests.&lt;/p&gt;
&lt;p&gt;The scale changes.&lt;/p&gt;
&lt;p&gt;The scheduling problem doesn't.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Bigger Lesson&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A single local LLM request is mostly a model-serving problem.&lt;/p&gt;
&lt;p&gt;A shared inference server is a distributed-systems problem.&lt;/p&gt;
&lt;p&gt;Once multiple workloads compete for a finite model, GPU, memory budget, or concurrency pool, you need policy.&lt;/p&gt;
&lt;p&gt;Rate limiting controls how often work arrives.&lt;/p&gt;
&lt;p&gt;Concurrency limiting controls how much work runs simultaneously.&lt;/p&gt;
&lt;p&gt;Admission control decides whether the system should accept the work at all, given its current obligations.&lt;/p&gt;
&lt;p&gt;And workload isolation decides whose work gets protected when capacity becomes scarce.&lt;/p&gt;
&lt;p&gt;You may eventually need another GPU.&lt;/p&gt;
&lt;p&gt;But before buying one, send your local model two very different workloads at the same time.&lt;/p&gt;
&lt;p&gt;You might discover that the first thing you need isn't more capacity.&lt;/p&gt;
&lt;p&gt;It's a better way to decide who gets to use the capacity you already have.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26773/why-local-llm-inference-needs-admission-control-before-it-needs-more-hardware</guid>
<pubDate>Fri, 11 Sep 2026 02:47:45 +0000</pubDate>
</item>
<item>
<title>Foundations of System Design: Learn It the Way I Wish Someone Taught Me</title>
<link>https://coderlegion.com/26320/foundations-of-system-design-learn-it-the-way-i-wish-someone-taught-me</link>
<description>&lt;p&gt;When I first started learning system design, I felt overwhelmed.&lt;/p&gt;
&lt;p&gt;Load balancers. Caches. Message queues. Replication. Sharding. Distributed systems.&lt;/p&gt;
&lt;p&gt;There are a lot of terms. And most guides throw them at you like a vocabulary test.&lt;/p&gt;
&lt;p&gt;But here is the thing: I don't think the best way to learn system design is to memorize diagrams or vocabulary.&lt;/p&gt;
&lt;p&gt;I prefer starting with something much simpler:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Start with one request. Then introduce a new concept only when the system actually needs it.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is what we are going to do in this guide.&lt;/p&gt;
&lt;p&gt;We will start with a very small application and gradually make it capable of handling more traffic and more failures. Every new concept we introduce will be because the system actually broke and needed it.&lt;/p&gt;
&lt;p&gt;If you have ever felt like system design is &quot;too advanced&quot; or &quot;not for you&quot; yet, I promise you it is. You just need the right entry point.&lt;/p&gt;
&lt;p&gt;Let's build it together.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-we-will-cover&quot;&gt;What We Will Cover&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;What system design actually means (and why it is not just for senior engineers)&lt;/li&gt;
&lt;li&gt;How a browser request travels to your server&lt;/li&gt;
&lt;li&gt;Where the database fits and why relational databases are useful&lt;/li&gt;
&lt;li&gt;Transactions and why they matter&lt;/li&gt;
&lt;li&gt;Scaling: vertical and horizontal&lt;/li&gt;
&lt;li&gt;Load balancers and how they distribute traffic&lt;/li&gt;
&lt;li&gt;Health checks and detecting failed servers&lt;/li&gt;
&lt;li&gt;Redundancy and why it matters&lt;/li&gt;
&lt;li&gt;The architecture we built and the problems we have not solved yet&lt;/li&gt;
&lt;li&gt;The mental model for approaching any system design problem&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let's start.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-system-design-&quot;&gt;What Is System Design?&lt;/h3&gt;
&lt;p&gt;In simple terms, &lt;strong&gt;system design is about deciding how the different parts of a software system should work together as the system grows.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When your application has 10 users, almost any reasonable architecture can work.&lt;/p&gt;
&lt;p&gt;When it has 100,000 users, things start getting interesting.&lt;/p&gt;
&lt;p&gt;Your server has limited CPU and memory.&lt;/p&gt;
&lt;p&gt;Your database has limits.&lt;/p&gt;
&lt;p&gt;Networks fail.&lt;/p&gt;
&lt;p&gt;Servers crash.&lt;/p&gt;
&lt;p&gt;Requests take longer than expected.&lt;/p&gt;
&lt;p&gt;Two users can try to change the same data at the same time.&lt;/p&gt;
&lt;p&gt;So system design is not just:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;How do I make this application work?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It becomes:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&quot;How do I make this application continue working as traffic, data, and failures increase?&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If that question excites you, you are in the right place.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;a-request-from-the-browser&quot;&gt;A Request From the Browser&lt;/h3&gt;
&lt;p&gt;Imagine you have a frontend application running in the browser.&lt;/p&gt;
&lt;p&gt;The browser needs a list of products.&lt;/p&gt;
&lt;p&gt;It sends a request:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Browser
   |
   | GET /products
   ↓
api.example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But &lt;code&gt;api.example.com&lt;/code&gt; is not the server's IP address.&lt;/p&gt;
&lt;p&gt;It is a &lt;strong&gt;domain name&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The browser needs to find the IP address associated with that domain.&lt;/p&gt;
&lt;p&gt;This is where &lt;strong&gt;DNS (Domain Name System)&lt;/strong&gt; comes in.&lt;/p&gt;
&lt;p&gt;Very roughly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Browser
   |
   | &quot;Where is api.example.com?&quot;
   ↓
DNS
   |
   | &quot;It is available at this IP&quot;
   ↓
IP address
   |
   ↓
Server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;DNS is basically the system that helps translate human-readable domain names into network addresses.&lt;/p&gt;
&lt;p&gt;You do not need to remember the entire DNS process yet.&lt;/p&gt;
&lt;p&gt;For now, just remember:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Domain name → DNS resolution → IP address → server&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-server-processes-the-request&quot;&gt;The Server Processes the Request&lt;/h3&gt;
&lt;p&gt;Now the request reaches our server.&lt;/p&gt;
&lt;p&gt;The server receives:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;GET /products
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It needs to retrieve the products from the database.&lt;/p&gt;
&lt;p&gt;So our system currently looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client
   |
   | Request
   ↓
Server
   |
   | Query
   ↓
Database
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The database returns the data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Database
   |
   | Products
   ↓
Server
   |
   | JSON response
   ↓
Client
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The complete flow is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client
   ↓
Server
   ↓
Database
   ↓
Server
   ↓
Client
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At this point, everything looks simple.&lt;/p&gt;
&lt;p&gt;And that is exactly what we want.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Do not introduce a load balancer, Redis, Kafka, Kubernetes and 15 other things when one server is perfectly capable of handling the application.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;where-does-the-database-fit-&quot;&gt;Where Does the Database Fit?&lt;/h3&gt;
&lt;p&gt;Let us say our application has these entities:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Users
Products
Orders
Accounts
Sessions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This data has structure.&lt;/p&gt;
&lt;p&gt;A user has an ID, name and email.&lt;/p&gt;
&lt;p&gt;A product has an ID, name and price.&lt;/p&gt;
&lt;p&gt;An order belongs to a user and contains products.&lt;/p&gt;
&lt;p&gt;This is where a &lt;strong&gt;relational database&lt;/strong&gt; can be a very good fit.&lt;/p&gt;
&lt;p&gt;Examples include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;li&gt;MySQL&lt;/li&gt;
&lt;li&gt;Oracle&lt;/li&gt;
&lt;li&gt;SQLite&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Relational databases organize data into &lt;strong&gt;tables&lt;/strong&gt;, which contain &lt;strong&gt;rows&lt;/strong&gt; and &lt;strong&gt;columns&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;users&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;: 1&lt;/li&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;: Noor&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;email&lt;/code&gt;: &lt;em&gt;Emails are not allowed&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;id&lt;/code&gt;: 2&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;: Ali&lt;/li&gt;
&lt;li&gt;&lt;code&gt;email&lt;/code&gt;: &lt;em&gt;Emails are not allowed&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;products&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;: 1&lt;/li&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;: Keyboard&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;price&lt;/code&gt;: 100&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;id&lt;/code&gt;: 2&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;: Mouse&lt;/li&gt;
&lt;li&gt;&lt;code&gt;price&lt;/code&gt;: 50&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The important thing is not memorizing the names of databases.&lt;/p&gt;
&lt;p&gt;The important question is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;What kind of data and guarantees does my application need?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-relational-databases-are-useful&quot;&gt;Why Relational Databases Are Useful&lt;/h3&gt;
&lt;p&gt;One major advantage of relational databases is that they let us model relationships between data.&lt;/p&gt;
&lt;p&gt;Suppose Noor buys a keyboard.&lt;/p&gt;
&lt;p&gt;We might have:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;users
  ↓
orders
  ↓
order_items
  ↓
products
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The database can represent those relationships explicitly.&lt;/p&gt;
&lt;p&gt;We can then query information such as:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Which products did this user purchase?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is where SQL and operations such as &lt;code&gt;JOIN&lt;/code&gt; become useful.&lt;/p&gt;
&lt;p&gt;For example, conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;SELECT users.name, products.name
FROM users
JOIN orders ON orders.user_id = users.id
JOIN order_items ON order_items.order_id = orders.id
JOIN products ON products.id = order_items.product_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You do not need to memorize this query.&lt;/p&gt;
&lt;p&gt;The important idea is that relational databases are very good when your data has clear relationships and you need strong consistency around those relationships.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;transactions&quot;&gt;Transactions&lt;/h3&gt;
&lt;p&gt;Now we have reached our first really important system-design problem.&lt;/p&gt;
&lt;p&gt;Imagine a user places an order.&lt;/p&gt;
&lt;p&gt;Two things need to happen:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1. Create the order
2. Deduct the user's balance
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What happens if step 1 succeeds but step 2 fails?&lt;/p&gt;
&lt;p&gt;We could end up with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Order created
Money not deducted
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is an invalid state for many applications.&lt;/p&gt;
&lt;p&gt;This is where &lt;strong&gt;transactions&lt;/strong&gt; become important.&lt;/p&gt;
&lt;p&gt;A transaction allows us to group multiple database operations into one logical operation.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;BEGIN TRANSACTION

Create order
Deduct balance

COMMIT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If something goes wrong:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;BEGIN TRANSACTION

Create order
Deduct balance ← FAIL

ROLLBACK
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The database can roll back the changes made by the transaction.&lt;/p&gt;
&lt;p&gt;The simple mental model is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Either the required operations succeed together, or the transaction does not commit their changes.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Transactions are built around the &lt;strong&gt;ACID&lt;/strong&gt; properties:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;A → Atomicity
C → Consistency
I → Isolation
D → Durability
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You do not need to master ACID right now.&lt;/p&gt;
&lt;p&gt;Just understand why transactions exist:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;They help us keep related database operations in a valid state when something fails or when multiple operations happen concurrently.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;our-first-bottleneck&quot;&gt;Our First Bottleneck&lt;/h3&gt;
&lt;p&gt;So far we have:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;                  ┌──────────┐
Client ────────→ │  Server  │
                  └────┬─────┘
                       │
                       ↓
                  ┌──────────┐
                  │ Database │
                  └──────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now imagine our application becomes popular.&lt;/p&gt;
&lt;p&gt;We start getting:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;10 requests/second
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;100 requests/second
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1,000 requests/second
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Our server has limited resources.&lt;/p&gt;
&lt;p&gt;It has:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;CPU
RAM
Network
Disk
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At some point, one machine may no longer be enough.&lt;/p&gt;
&lt;p&gt;Now we have to think about scaling.&lt;/p&gt;
&lt;p&gt;This is where system design starts becoming interesting.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;vertical-scaling&quot;&gt;Vertical Scaling&lt;/h3&gt;
&lt;p&gt;The first thing we can do is make the existing server stronger.&lt;/p&gt;
&lt;p&gt;Maybe we currently have:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;4 CPU cores
8 GB RAM
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We could upgrade it to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;16 CPU cores
64 GB RAM
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is called &lt;strong&gt;vertical scaling&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In simple terms:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Make the machine bigger.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It is easy to understand and often the simplest solution.&lt;/p&gt;
&lt;p&gt;But it has limits.&lt;/p&gt;
&lt;h4 id=&quot;problem-1-hardware-has-limits&quot;&gt;Problem 1: Hardware Has Limits&lt;/h4&gt;
&lt;p&gt;You cannot infinitely increase the CPU and RAM of one machine.&lt;/p&gt;
&lt;p&gt;Eventually you reach the limits of the available hardware or the cost becomes unreasonable.&lt;/p&gt;
&lt;h4 id=&quot;problem-2-single-point-of-failure&quot;&gt;Problem 2: Single Point of Failure&lt;/h4&gt;
&lt;p&gt;We still have one server.&lt;/p&gt;
&lt;p&gt;If that server goes down:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Server crashes
   ↓
Application unavailable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a &lt;strong&gt;single point of failure&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A single point of failure is a component whose failure can bring down an important part of the system.&lt;/p&gt;
&lt;p&gt;So we have solved one problem but created another question:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Can we run multiple servers?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;horizontal-scaling&quot;&gt;Horizontal Scaling&lt;/h3&gt;
&lt;p&gt;Instead of making one server bigger, we can add more servers.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;         Server 1
       /
Client ── Server 2
       \
         Server 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is called &lt;strong&gt;horizontal scaling&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Instead of:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1 very powerful server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;we have:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;3 servers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now if one server fails, the other servers may still be able to handle requests.&lt;/p&gt;
&lt;p&gt;But we have created a new problem.&lt;/p&gt;
&lt;p&gt;How does the client know which server to talk to?&lt;/p&gt;
&lt;p&gt;We do not want clients manually choosing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;server-1.example.com
server-2.example.com
server-3.example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We need something in front of them.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;load-balancer&quot;&gt;Load Balancer&lt;/h3&gt;
&lt;p&gt;This is where a &lt;strong&gt;load balancer&lt;/strong&gt; comes in.&lt;/p&gt;
&lt;p&gt;A load balancer receives incoming requests and decides which server should handle each request.&lt;/p&gt;
&lt;p&gt;Our architecture becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;                    ┌──────────┐
                    │ Server 1 │
                  /
Client → Load Balancer → Server 2
                  \
                    └──────────┘
                         Server 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The client does not need to know about all the application servers.&lt;/p&gt;
&lt;p&gt;It talks to the load balancer.&lt;/p&gt;
&lt;p&gt;The load balancer distributes traffic between available servers.&lt;/p&gt;
&lt;p&gt;Now we have:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client
   ↓
Load Balancer
   ↓
┌────────┬────────┬────────┐
│Server 1│Server 2│Server 3│
└────────┴────────┴────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives us more capacity and can improve availability.&lt;/p&gt;
&lt;p&gt;But now we need to decide:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;How does the load balancer choose a server?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;load-balancing-algorithms&quot;&gt;Load-Balancing Algorithms&lt;/h3&gt;
&lt;p&gt;There is not one universal algorithm.&lt;/p&gt;
&lt;p&gt;Different algorithms work better for different workloads.&lt;/p&gt;
&lt;h4 id=&quot;round-robin&quot;&gt;Round Robin&lt;/h4&gt;
&lt;p&gt;The load balancer sends requests in sequence:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Request 1 → Server 1
Request 2 → Server 2
Request 3 → Server 3
Request 4 → Server 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Simple and useful when servers have similar capacity and requests have roughly similar cost.&lt;/p&gt;
&lt;h4 id=&quot;weighted-round-robin&quot;&gt;Weighted Round Robin&lt;/h4&gt;
&lt;p&gt;Maybe our servers are not equally powerful.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Server 1 → weight 1
Server 2 → weight 2
Server 3 → weight 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The stronger server receives more traffic.&lt;/p&gt;
&lt;h4 id=&quot;least-connections&quot;&gt;Least Connections&lt;/h4&gt;
&lt;p&gt;The load balancer sends a new request to the server currently handling the fewest active connections.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Server 1 → 30 connections
Server 2 → 12 connections
Server 3 → 21 connections

New request → Server 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This can be useful when requests have different durations.&lt;/p&gt;
&lt;h4 id=&quot;ip-hash&quot;&gt;IP Hash&lt;/h4&gt;
&lt;p&gt;The load balancer hashes the client's IP address and uses the result to choose a server.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client IP
   ↓
Hash
   ↓
Server selection
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This can provide a form of request affinity, although relying on IP alone has limitations.&lt;/p&gt;
&lt;h4 id=&quot;random&quot;&gt;Random&lt;/h4&gt;
&lt;p&gt;The load balancer randomly chooses an available server.&lt;/p&gt;
&lt;p&gt;Simple, but whether it is appropriate depends on the workload and implementation.&lt;/p&gt;
&lt;h4 id=&quot;least-response-time&quot;&gt;Least Response Time&lt;/h4&gt;
&lt;p&gt;The load balancer can consider response times and direct traffic toward servers that are responding faster.&lt;/p&gt;
&lt;p&gt;This can be useful when server load is not evenly distributed.&lt;/p&gt;
&lt;h4 id=&quot;consistent-hashing&quot;&gt;Consistent Hashing&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Consistent hashing&lt;/strong&gt; is useful in systems where we want related keys to map predictably to servers while minimizing how much mapping changes when servers are added or removed.&lt;/p&gt;
&lt;p&gt;It becomes especially useful in distributed caches and partitioned systems.&lt;/p&gt;
&lt;p&gt;Do not worry if that sounds complicated.&lt;/p&gt;
&lt;p&gt;We will come back to it when we actually need it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;but-what-if-a-server-is-dead-&quot;&gt;But What If a Server Is Dead?&lt;/h3&gt;
&lt;p&gt;Imagine we have:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Server 1 → healthy
Server 2 → healthy
Server 3 → crashed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We do not want the load balancer to continue sending traffic to Server 3.&lt;/p&gt;
&lt;p&gt;This is where &lt;strong&gt;health checks&lt;/strong&gt; come in.&lt;/p&gt;
&lt;p&gt;The load balancer periodically checks whether a server is healthy.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;GET /health
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server might respond:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;status&quot;: &quot;ok&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the server stops responding or reports that it is not ready, the load balancer can stop sending new traffic to it.&lt;/p&gt;
&lt;p&gt;So now:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;              Load Balancer
               /         \
              ↓           ↓
         Server 1      Server 2
         healthy       healthy

         Server 3
        unhealthy
            X
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is much better than blindly trusting that every server is alive.&lt;/p&gt;
&lt;p&gt;If you want a ready-made health check implementation you can drop into an Express, Fastify or Hono app, I built one for Blockend. It handles liveness and readiness checks out of the box:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://blockend.noorulhassan.com/docs/02-blocks/07-health-check&quot;&gt;https://blockend.noorulhassan.com/docs/02-blocks/07-health-check&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;health-checks&quot;&gt;Health Checks&lt;/h3&gt;
&lt;p&gt;A load balancer should not blindly send traffic to every server.&lt;/p&gt;
&lt;p&gt;Instead, it should periodically check whether each server is healthy.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Load Balancer
       /         \
      ↓           ↓
 Server 1      Server 2
 healthy       healthy

 Server 3
unhealthy
    X
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is much better than blindly trusting that every server is alive.&lt;/p&gt;
&lt;p&gt;If you want a ready-made health check implementation you can drop into an Express, Fastify, or Hono app, I built one for Blockend. It handles liveness and readiness checks out of the box:&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://blockend.noorulhassan.com/docs/02-blocks/07-health-check&quot;&gt;https://blockend.noorulhassan.com/docs/02-blocks/07-health-check&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;health-checks-are-more-than-is-the-process-running-&quot;&gt;Health Checks Are More Than &quot;Is the Process Running?&quot;&lt;/h3&gt;
&lt;p&gt;There is an important distinction here.&lt;/p&gt;
&lt;p&gt;A server process can be running while the application is not actually ready to serve traffic.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Node process → running
Database     → unavailable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The process itself is alive.&lt;/p&gt;
&lt;p&gt;But the application might not be able to perform useful work.&lt;/p&gt;
&lt;p&gt;This is why production systems often distinguish between two different concepts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Liveness&lt;/li&gt;
&lt;li&gt;Readiness&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;liveness&quot;&gt;Liveness&lt;/h4&gt;
&lt;p&gt;Liveness asks something like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Is the application process alive?
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;readiness&quot;&gt;Readiness&lt;/h4&gt;
&lt;p&gt;Readiness asks:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Is the application ready to receive traffic?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This distinction becomes important when deploying applications, restarting services, and handling failures.&lt;/p&gt;
&lt;h3 id=&quot;redundancy&quot;&gt;Redundancy&lt;/h3&gt;
&lt;p&gt;We have now introduced another important idea:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Redundancy.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Instead of depending on one component:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;we have:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Server 1
Server 2
Server 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If one fails, the others can continue serving traffic.&lt;/p&gt;
&lt;p&gt;The same principle can apply to other parts of the system.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Multiple application servers&lt;/li&gt;
&lt;li&gt;Multiple database replicas&lt;/li&gt;
&lt;li&gt;Multiple cache nodes&lt;/li&gt;
&lt;li&gt;Multiple availability zones&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal is to avoid unnecessary single points of failure.&lt;/p&gt;
&lt;p&gt;But redundancy creates another problem.&lt;/p&gt;
&lt;p&gt;If we have three application servers, where is the user's session stored?&lt;/p&gt;
&lt;p&gt;And if all three servers need the same data, how do they share it?&lt;/p&gt;
&lt;p&gt;This is where our simple architecture starts becoming a distributed system.&lt;/p&gt;
&lt;h3 id=&quot;the-architecture-we-have-built&quot;&gt;The Architecture We Have Built&lt;/h3&gt;
&lt;p&gt;Let us stop here and look at how far we have come.&lt;/p&gt;
&lt;p&gt;We started with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client → Server → Database
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then traffic increased.&lt;/p&gt;
&lt;p&gt;We added vertical scaling.&lt;/p&gt;
&lt;p&gt;Then we reached the limits of one machine.&lt;/p&gt;
&lt;p&gt;We added horizontal scaling.&lt;/p&gt;
&lt;p&gt;Then we needed something to distribute traffic.&lt;/p&gt;
&lt;p&gt;We added a load balancer.&lt;/p&gt;
&lt;p&gt;Then we needed to detect failed servers.&lt;/p&gt;
&lt;p&gt;We added health checks.&lt;/p&gt;
&lt;p&gt;Our architecture now looks roughly like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;                            ┌───────────┐
                            │ Database  │
                            └─────▲─────┘
                                  │
                                  │
                            ┌─────┴─────┐
                            │   Load    │
Client ─────────────────→ │ Balancer  │
                            └─────┬─────┘
                            ┌─────┼─────┐
                            ↓     ↓     ↓
                        Server Server Server
                           1     2     3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice something important.&lt;/p&gt;
&lt;p&gt;We did not start with all of these components.&lt;/p&gt;
&lt;p&gt;We earned each component by encountering a problem.&lt;/p&gt;
&lt;p&gt;That is how I think system design should be learned.&lt;/p&gt;
&lt;h3 id=&quot;the-problems-we-have-not-solved-yet&quot;&gt;The Problems We Have Not Solved Yet&lt;/h3&gt;
&lt;p&gt;Our system is better, but we are not finished.&lt;/p&gt;
&lt;p&gt;We still have several problems.&lt;/p&gt;
&lt;h4 id=&quot;database-bottleneck&quot;&gt;Database Bottleneck&lt;/h4&gt;
&lt;p&gt;All servers are talking to one database.&lt;/p&gt;
&lt;p&gt;What happens when database traffic becomes too high?&lt;/p&gt;
&lt;p&gt;We might need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Indexes&lt;/li&gt;
&lt;li&gt;Query optimization&lt;/li&gt;
&lt;li&gt;Connection pooling&lt;/li&gt;
&lt;li&gt;Caching&lt;/li&gt;
&lt;li&gt;Read replicas&lt;/li&gt;
&lt;li&gt;Partitioning&lt;/li&gt;
&lt;li&gt;Sharding&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But we should not add these just because they exist.&lt;/p&gt;
&lt;p&gt;We add them when the problem requires them.&lt;/p&gt;
&lt;h4 id=&quot;repeated-expensive-requests&quot;&gt;Repeated Expensive Requests&lt;/h4&gt;
&lt;p&gt;Suppose thousands of users request the same product:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-http&quot;&gt;GET /products/123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every request goes:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Server → Database
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Why query the database 10,000 times if the data does not change frequently?&lt;/p&gt;
&lt;p&gt;This is where caching becomes useful.&lt;/p&gt;
&lt;h4 id=&quot;background-work&quot;&gt;Background Work&lt;/h4&gt;
&lt;p&gt;Suppose creating an account also requires:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Send email&lt;/li&gt;
&lt;li&gt;Generate report&lt;/li&gt;
&lt;li&gt;Process image&lt;/li&gt;
&lt;li&gt;Notify another service&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Do we really want the HTTP request to wait for all of that?&lt;/p&gt;
&lt;p&gt;Probably not.&lt;/p&gt;
&lt;p&gt;This leads us toward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Queues&lt;/li&gt;
&lt;li&gt;Background workers&lt;/li&gt;
&lt;li&gt;Asynchronous processing&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;service-failures&quot;&gt;Service Failures&lt;/h4&gt;
&lt;p&gt;What happens if another service is temporarily unavailable?&lt;/p&gt;
&lt;p&gt;Do we retry?&lt;/p&gt;
&lt;p&gt;How many times?&lt;/p&gt;
&lt;p&gt;How quickly?&lt;/p&gt;
&lt;p&gt;What if retries make the problem worse?&lt;/p&gt;
&lt;p&gt;This leads us toward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Timeouts&lt;/li&gt;
&lt;li&gt;Retries&lt;/li&gt;
&lt;li&gt;Backoff&lt;/li&gt;
&lt;li&gt;Circuit breakers&lt;/li&gt;
&lt;li&gt;Idempotency&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;data-growth&quot;&gt;Data Growth&lt;/h4&gt;
&lt;p&gt;What happens when our database grows from:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1 GB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;to:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;1 TB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Eventually we may need to think about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Partitioning&lt;/li&gt;
&lt;li&gt;Archiving&lt;/li&gt;
&lt;li&gt;Replication&lt;/li&gt;
&lt;li&gt;Sharding&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And this is where system design starts becoming much deeper.&lt;/p&gt;
&lt;h3 id=&quot;the-mental-model-i-want-you-to-keep&quot;&gt;The Mental Model I Want You to Keep&lt;/h3&gt;
&lt;p&gt;If you are learning system design, do not try to memorize diagrams.&lt;/p&gt;
&lt;p&gt;Instead, keep asking:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;What is the bottleneck?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;What failure can happen here?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;What happens when traffic increases?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;What happens when this component goes down?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;What consistency does the application actually need?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;What is the simplest solution that solves the problem?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That last question is important.&lt;/p&gt;
&lt;p&gt;A system with:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client
  ↓
Server
  ↓
PostgreSQL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;is not a bad architecture simply because it does not have Kafka, Redis, Kubernetes, and 20 microservices.&lt;/p&gt;
&lt;p&gt;If that system handles the workload reliably, it is a good system.&lt;/p&gt;
&lt;p&gt;Good system design is not about adding more components.&lt;/p&gt;
&lt;p&gt;It is about making the right tradeoffs for the problem you actually have.&lt;/p&gt;
&lt;h3 id=&quot;where-we-go-from-here&quot;&gt;Where We Go From Here&lt;/h3&gt;
&lt;p&gt;We started with one request:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client
  ↓
Server
  ↓
Database
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we introduced concepts only when we needed them:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;More traffic
     ↓
Scaling
     ↓
Horizontal scaling
     ↓
Load balancer
     ↓
Health checks
     ↓
Redundancy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And this is only the beginning.&lt;/p&gt;
&lt;p&gt;From here, we can start introducing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Caching
   ↓
Database scaling
   ↓
Read replicas
   ↓
Queues
   ↓
Background workers
   ↓
Retries and backoff
   ↓
Idempotency
   ↓
Consistency
   ↓
Replication
   ↓
Partitioning
   ↓
Sharding
   ↓
Distributed systems
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We will introduce each one when there is a real problem that requires it.&lt;/p&gt;
&lt;p&gt;Because that is the way I wish someone had taught me system design.&lt;/p&gt;
&lt;p&gt;Not:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;Here are 50 components. Memorize them.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&quot;Here is a system. Let's see what breaks when we push it.&quot;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is where system design actually starts.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-you-learned&quot;&gt;What You Learned&lt;/h3&gt;
&lt;p&gt;Let us take a step back.&lt;/p&gt;
&lt;p&gt;You started with a single browser request and ended up with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A server that processes requests&lt;/li&gt;
&lt;li&gt;A relational database that stores structured data&lt;/li&gt;
&lt;li&gt;Transactions that keep related operations consistent&lt;/li&gt;
&lt;li&gt;Vertical scaling to handle more load&lt;/li&gt;
&lt;li&gt;Horizontal scaling to handle even more&lt;/li&gt;
&lt;li&gt;A load balancer that distributes traffic across servers&lt;/li&gt;
&lt;li&gt;Health checks that detect failed servers&lt;/li&gt;
&lt;li&gt;Redundancy that avoids single points of failure&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And you did not just memorize these concepts.&lt;/p&gt;
&lt;p&gt;You &lt;strong&gt;encountered each problem&lt;/strong&gt; and solved it with the right tool.&lt;/p&gt;
&lt;p&gt;That is the mental model. That is what matters.&lt;/p&gt;
&lt;p&gt;The next time someone says &quot;you need a load balancer,&quot; you will not just know what it is.&lt;/p&gt;
&lt;p&gt;You will know &lt;strong&gt;why&lt;/strong&gt; it exists.&lt;/p&gt;
&lt;p&gt;And that makes all the difference.&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/26320/foundations-of-system-design-learn-it-the-way-i-wish-someone-taught-me</guid>
<pubDate>Sun, 06 Sep 2026 13:32:05 +0000</pubDate>
</item>
<item>
<title>The Bug That's Been Breaking Software for 40 Years Is Now Breaking Your AI Agents, Too</title>
<link>https://coderlegion.com/25313/the-bug-thats-been-breaking-software-for-40-years-is-now-breaking-your-ai-agents-too</link>
<description>&lt;p&gt;Picture a Java developer shipping an AI agent into production. The agent gets a prompt and starts working through a task, 50 steps, each one a call to a model like OpenAI or Anthropic, a database write, maybe a few dollars of compute. At step 49 of 50, something fails: a bug, a network blip, an AWS outage, a Kubernetes scale-down mid-run. What happens next, according to Yaron Schneider, CTO and co-founder of Diagrid, is the part nobody budgets for. &quot;The agent is going to need to pick back up all the way from the beginning, and you're going to be paying for all of those LLM calls right from the very beginning,&quot; he said. &quot;Which is a great way to go bankrupt.&quot;&lt;/p&gt;
&lt;p&gt;That failure mode is what Diagrid's new integration, diagrid-spring-ai, is built to close. Announced this week, it brings durable execution to Spring AI, the framework Java developers already use to build agents, without asking them to rethink how they write one.&lt;/p&gt;
&lt;h3 id=&quot;the-same-40-year-old-problem-with-worse-consequences&quot;&gt;The same 40-year-old problem, with worse consequences&lt;/h3&gt;
&lt;p&gt;Schneider doesn't frame this as a new category of failure. It's the same reliability problem distributed systems have always had. &quot;The exact same problems that made software unreliable for the past 40 years are still true for AI agents,&quot; he said. &quot;Only with AI agents, the consequences are much bigger.&quot; A crashed script used to mean lost time. A crashed agent means lost time and a re-billed stack of model calls, at a cost that scales with however many steps the agent had already completed.&lt;/p&gt;
&lt;p&gt;Schneider said the companies Diagrid works with see failures on roughly 30% of production agent runs, on average, not rare edge cases, but a routine cost of running agents at scale.&lt;/p&gt;
&lt;h3 id=&quot;zero-code-changes-by-design&quot;&gt;Zero code changes, by design&lt;/h3&gt;
&lt;p&gt;The integration's core pitch is unusually low-friction for infrastructure-level tooling. &quot;With Spring AI, literally nothing,&quot; Schneider said, when asked what a developer has to change to adopt it. &quot;All you need to do is pull in the package. There is zero code changes. You do not need to recompile your code.&quot; Underneath, the integration runs on Dapr Workflow, the open-source project Schneider originally built at Microsoft and later spun into Diagrid, now used by more than 30,000 companies, including over half the Fortune 500 and, notably, NASA.&lt;/p&gt;
&lt;h3 id=&quot;how-it-actually-avoids-re-running-side-effects&quot;&gt;How it actually avoids re-running side effects&lt;/h3&gt;
&lt;p&gt;The obvious follow-up question for anyone who's built distributed systems: how do you resume an interrupted agent without re-executing a step that already sent an email or wrote to a database? Diagrid's answer is an immutable ledger. &quot;We keep an immutable record of everything that the agent did. We're essentially journaling,&quot; Schneider said. When an agent fails mid-run, the platform reads the journal, recovers the state, exactly what was completed, its inputs, outputs, and memory, and resumes from that point rather than replaying anything already finished.&lt;/p&gt;
&lt;p&gt;Schneider was direct about the confidence customers should have in that recovery: &quot;If they're using our technology, they know for 100%, for a fact, that we will always resume where they left off mid-run.&quot; He added that customers can and do test this themselves, running failure simulations in their own environments to verify the behavior before trusting it in production.&lt;/p&gt;
&lt;h3 id=&quot;why-this-isn-t-just-a-spring-ai-feature&quot;&gt;Why this isn't just a Spring AI feature&lt;/h3&gt;
&lt;p&gt;A natural question is why durable execution lives as a separate integration rather than something Spring AI ships with directly. Schneider's answer comes down to what a workflow engine actually requires. &quot;Workflow engines are incredibly hard to get right. They require their own separate infrastructure, and Spring is just not that,&quot; he said. Bundling a database and deployment model into a lightweight developer framework would hurt adoption, not help it. Building that infrastructure in-house isn't a reasonable alternative either: Schneider pointed to Microsoft's own investment, a team of 15 engineers spending more than $10 million a year maintaining its workflow engine, as a sense of the actual cost most teams would take on trying to solve this themselves.&lt;/p&gt;
&lt;h3 id=&quot;the-claim-schneider-thinks-is-overrated&quot;&gt;The claim Schneider thinks is overrated&lt;/h3&gt;
&lt;p&gt;Asked what's overstated in the broader AI agent conversation right now, Schneider didn't hesitate: model accuracy. &quot;Models are going to continue getting better. They're going to continue being cheaper,&quot; he said. &quot;I see a lot of emphasis being put on that instead of, you know, how do you orchestrate multiple agents needing to solve a task and all of them failing at the same time.&quot; In his view, the infrastructure problem, keeping agents reliable when things break, is a bigger and less-discussed risk than whatever incremental accuracy gain the next model release brings.&lt;/p&gt;
&lt;h3 id=&quot;beyond-spring&quot;&gt;Beyond Spring&lt;/h3&gt;
&lt;p&gt;diagrid-spring-ai is one piece of a broader set of integrations Diagrid maintains across roughly ten agent frameworks spanning Python, C#, and Java, including LangChain, LangGraph, and CrewAI. Developers can try the platform directly at diagrid.io, where Schneider said quick-start guides let teams test the failure-recovery behavior themselves before committing to it in production.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25313/the-bug-thats-been-breaking-software-for-40-years-is-now-breaking-your-ai-agents-too</guid>
<pubDate>Tue, 25 Aug 2026 00:00:00 +0000</pubDate>
</item>
<item>
<title>Retries Can Make Outages Worse</title>
<link>https://coderlegion.com/25172/retries-can-make-outages-worse</link>
<description>&lt;p&gt;Retries are one of the easiest resilience patterns to add and one of the easiest to get wrong.&lt;/p&gt;
&lt;p&gt;The idea is sound. Transient failures happen. Networks drop packets. Load balancers close idle connections. A database primary moves. A single failed call should not always fail the whole operation.&lt;/p&gt;
&lt;p&gt;But retries also create traffic.&lt;/p&gt;
&lt;p&gt;During a healthy moment, that extra traffic is small. During an outage, it can be enormous. Every timeout becomes multiple calls. Every slow dependency receives new requests from callers that have already given up on older ones. The dependency is not just handling user traffic anymore. It is handling user traffic plus the system's anxiety about user traffic.&lt;/p&gt;
&lt;p&gt;That is how retries turn a partial failure into a larger one.&lt;/p&gt;
&lt;h3 id=&quot;retry-storms&quot;&gt;Retry Storms&lt;/h3&gt;
&lt;p&gt;A retry storm starts when many clients observe the same failure at roughly the same time.&lt;/p&gt;
&lt;p&gt;Without jitter, they retry at roughly the same time too.&lt;/p&gt;
&lt;p&gt;If the dependency is overloaded, those synchronized retries make recovery harder. The service needs quiet time to drain queues, rebuild caches, elect leaders, or scale out. Instead it receives a second wave of traffic. Then a third.&lt;/p&gt;
&lt;p&gt;The clients think they are being persistent. The dependency experiences persistence as pressure.&lt;/p&gt;
&lt;h3 id=&quot;timeouts-come-first&quot;&gt;Timeouts Come First&lt;/h3&gt;
&lt;p&gt;Retries without timeouts are not retries. They are a way to accumulate stuck work.&lt;/p&gt;
&lt;p&gt;Every retry policy needs a per-attempt timeout and an overall deadline. The per-attempt timeout prevents one slow call from consuming the entire budget. The overall deadline prevents retries from continuing after the user or caller no longer cares.&lt;/p&gt;
&lt;p&gt;In Go, that usually means retries must be tied to &lt;code&gt;context.Context&lt;/code&gt;, not a loop with &lt;code&gt;time.Sleep&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func callWithRetry(ctx context.Context, attempts int) error {
    for i := 0; i &amp;lt; attempts; i++ {
        if err := ctx.Err(); err != nil {
            return err
        }

        attemptCtx, cancel := context.WithTimeout(ctx, 200*time.Millisecond)
        err := callDependency(attemptCtx)
        cancel()

        if err == nil {
            return nil
        }
        if !isRetriable(err) {
            return err
        }

        if !sleepWithJitter(ctx, i) {
            return ctx.Err()
        }
    }
    return errors.New(&quot;dependency failed after retries&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The exact helper functions matter less than the shape: bounded attempts, bounded time, cancellation-aware sleep, and retry only for errors that are actually retriable.&lt;/p&gt;
&lt;h3 id=&quot;retry-budgets&quot;&gt;Retry Budgets&lt;/h3&gt;
&lt;p&gt;A retry budget limits how much extra traffic retries may create.&lt;/p&gt;
&lt;p&gt;For example, a client might allow retries to add no more than 10 percent extra request volume over a rolling window. When the budget is exhausted, the client stops retrying and returns errors faster.&lt;/p&gt;
&lt;p&gt;That sounds harsh, but it protects the dependency and the caller. If the system is already failing, unlimited retries mostly buy longer queues and slower recovery.&lt;/p&gt;
&lt;p&gt;Retries should be treated as a shared resource.&lt;/p&gt;
&lt;h3 id=&quot;not-every-error-is-retriable&quot;&gt;Not Every Error Is Retriable&lt;/h3&gt;
&lt;p&gt;Retrying a deterministic failure is just load generation.&lt;/p&gt;
&lt;p&gt;Good retry policies distinguish between:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;connection resets&lt;/li&gt;
&lt;li&gt;timeouts&lt;/li&gt;
&lt;li&gt;rate limits&lt;/li&gt;
&lt;li&gt;temporary 5xx responses&lt;/li&gt;
&lt;li&gt;validation failures&lt;/li&gt;
&lt;li&gt;authentication failures&lt;/li&gt;
&lt;li&gt;not found responses&lt;/li&gt;
&lt;li&gt;conflicts caused by stale state&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Some of those may deserve a retry. Many do not. A 400 response should usually not be retried. A 401 should not become three 401s. A permanent validation error is not going to become valid because the client waited 100 milliseconds.&lt;/p&gt;
&lt;p&gt;The retry policy should be closer to the protocol semantics than to the transport library.&lt;/p&gt;
&lt;h3 id=&quot;retries-need-backpressure&quot;&gt;Retries Need Backpressure&lt;/h3&gt;
&lt;p&gt;Retries and backpressure belong together.&lt;/p&gt;
&lt;p&gt;If the dependency returns &lt;code&gt;429 Too Many Requests&lt;/code&gt; or a clear overload signal, callers should respect it. If there is a &lt;code&gt;Retry-After&lt;/code&gt; header, callers should use it. If the local queue is already full, the client should stop adding retry work to it.&lt;/p&gt;
&lt;p&gt;The purpose of a retry is to survive a transient failure, not to argue with a saturated system.&lt;/p&gt;
&lt;h3 id=&quot;the-lesson&quot;&gt;The Lesson&lt;/h3&gt;
&lt;p&gt;Retries are useful when they are humble.&lt;/p&gt;
&lt;p&gt;They should have a deadline. They should have jitter. They should have a budget. They should understand which errors are worth retrying. They should stop when the caller no longer needs the answer. They should listen when the dependency says &quot;slow down.&quot;&lt;/p&gt;
&lt;p&gt;The question is not &quot;should we retry?&quot;&lt;/p&gt;
&lt;p&gt;The question is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;How much extra pressure are we allowed to create while trying to recover?
&lt;/code&gt;&lt;/pre&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25172/retries-can-make-outages-worse</guid>
<pubDate>Sun, 23 Aug 2026 12:35:25 +0000</pubDate>
</item>
<item>
<title>The Difference Between a Cache and a Lie</title>
<link>https://coderlegion.com/24933/the-difference-between-a-cache-and-a-lie</link>
<description>&lt;p&gt;A cache is a promise with an expiration date.&lt;/p&gt;
&lt;p&gt;That sounds obvious until a production system starts depending on cached state as if it were truth. Then the cache is no longer an optimization. It is a second source of truth with worse durability, weaker ownership, and fewer people watching it.&lt;/p&gt;
&lt;p&gt;That is where cache bugs become painful.&lt;/p&gt;
&lt;p&gt;The hard part of caching is not storing a value. The hard part is deciding what the value is allowed to mean.&lt;/p&gt;
&lt;h3 id=&quot;a-cache-should-have-a-narrow-job&quot;&gt;A Cache Should Have a Narrow Job&lt;/h3&gt;
&lt;p&gt;The safest caches answer narrow questions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Have we recently computed this result?
Have we recently seen this object absent?
Have we recently fetched this configuration?
Can we avoid doing the same expensive work again?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The more dangerous caches answer authority-shaped questions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Does this object exist?
Is this user allowed?
Is this state current?
Has this workflow completed?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sometimes a cache must sit near one of those questions. That is fine. But the design needs to say exactly how wrong the cache is allowed to be and what happens when it is wrong.&lt;/p&gt;
&lt;h3 id=&quot;the-three-questions-every-cache-needs&quot;&gt;The Three Questions Every Cache Needs&lt;/h3&gt;
&lt;p&gt;Before adding a cache, I like to ask three questions.&lt;/p&gt;
&lt;p&gt;First:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;What is the source of truth?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the team cannot answer this clearly, the cache will eventually become part of the data model by accident.&lt;/p&gt;
&lt;p&gt;Second:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;What is the failure mode when the cache is unavailable?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fail-open and fail-closed are both valid choices, but they belong to different classes of systems. A performance cache in front of an idempotent delete path can usually fail open. An authorization decision probably cannot.&lt;/p&gt;
&lt;p&gt;Third:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;What is the failure mode when the cache is stale?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is where many designs get uncomfortable. A stale cache entry might mean an extra database read, which is fine. It might mean stale search results for a minute, which may be acceptable. It might mean granting access after permission was revoked, which is not the same kind of risk.&lt;/p&gt;
&lt;h3 id=&quot;ttl-is-not-invalidation&quot;&gt;TTL Is Not Invalidation&lt;/h3&gt;
&lt;p&gt;TTL is a recovery mechanism. It is not a complete correctness story.&lt;/p&gt;
&lt;p&gt;A TTL says:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;If we miss an invalidation, the system will eventually heal.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It does not say:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;The system is correct until the TTL expires.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If stale data can cause a correctness bug, the system still needs an invalidation path, a version check, a source-of-truth read, or a design that makes stale data harmless.&lt;/p&gt;
&lt;p&gt;TTL is useful because production systems are imperfect. It should not be the only thing standing between the user and a wrong answer.&lt;/p&gt;
&lt;h3 id=&quot;fail-open-vs-fail-closed&quot;&gt;Fail-Open vs Fail-Closed&lt;/h3&gt;
&lt;p&gt;The right behavior depends on what the cache protects.&lt;/p&gt;
&lt;p&gt;For a performance cache:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;cache hit   -&amp;gt; use cached result
cache miss  -&amp;gt; recompute or read from source
cache error -&amp;gt; bypass cache
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This keeps correctness independent of cache availability.&lt;/p&gt;
&lt;p&gt;For a safety or authorization cache, bypassing the cache may be wrong. If the cache is the only fast path to know whether an operation is allowed, fail-open can turn an outage into a security issue. In those systems, the right answer may be to fail closed, degrade the feature, or require a source-of-truth check.&lt;/p&gt;
&lt;p&gt;The cache design should make that tradeoff explicit.&lt;/p&gt;
&lt;h3 id=&quot;when-a-cache-becomes-a-lie&quot;&gt;When a Cache Becomes a Lie&lt;/h3&gt;
&lt;p&gt;A cache becomes a lie when the rest of the system forgets that it is allowed to be wrong.&lt;/p&gt;
&lt;p&gt;Common warning signs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;no documented source of truth&lt;/li&gt;
&lt;li&gt;no owner for invalidation&lt;/li&gt;
&lt;li&gt;no metric for hit rate, miss rate, or stale reads&lt;/li&gt;
&lt;li&gt;business logic that only exists in cache-writing code&lt;/li&gt;
&lt;li&gt;cache entries with no version, generation, or TTL&lt;/li&gt;
&lt;li&gt;code that treats cache errors as successful reads&lt;/li&gt;
&lt;li&gt;tests that mock the cache but never test cache loss&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The worst cache bugs are not caused by Redis being down. They are caused by Redis being trusted too much.&lt;/p&gt;
&lt;h3 id=&quot;the-lesson-1&quot;&gt;The Lesson&lt;/h3&gt;
&lt;p&gt;A good cache makes a system faster without becoming more important than the data it stores.&lt;/p&gt;
&lt;p&gt;It is allowed to be missing. It is allowed to be cold. It is allowed to be flushed. It may be useful when present and boring when absent.&lt;/p&gt;
&lt;p&gt;The moment a cache cannot be lost, rebuilt, bypassed, or corrected, it is no longer just a cache.&lt;/p&gt;
&lt;p&gt;It is part of the system of record, whether the architecture diagram admits it or not.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24933/the-difference-between-a-cache-and-a-lie</guid>
<pubDate>Wed, 19 Aug 2026 15:51:36 +0000</pubDate>
</item>
<item>
<title>Remote Execution, Explained for People Who Just Run make</title>
<link>https://coderlegion.com/23681/remote-execution-explained-for-people-who-just-run-make</link>
<description>&lt;p&gt;Most engineers think of the build as something that happens on their laptop. You type a command, the fans spin up, and a few minutes later you have a binary. That model is so ingrained that we rarely question it — until the codebase gets big enough that it quietly collapses.&lt;/p&gt;
&lt;p&gt;At a certain scale, the laptop build is too big for one machine, too slow to repeat, and too wasteful to run from scratch on every change. Ten engineers rebuild the same unchanged files ten times before lunch. A clean build takes forty minutes. CI is the bottleneck for the whole team. Remote execution is the answer large codebases reach for, and once you understand it, a lot of otherwise-baffling build-system design suddenly makes sense. Here is the mental model, minus the vendor jargon.&lt;/p&gt;
&lt;h3 id=&quot;the-core-idea-the-build-is-a-graph-of-pure-functions&quot;&gt;The core idea: the build is a graph of pure functions&lt;/h3&gt;
&lt;p&gt;Stop thinking of a build as a sequence of commands and start thinking of it as a graph. Each node is an &lt;em&gt;action&lt;/em&gt;: a single command plus the exact set of inputs it reads and outputs it produces. Compiling one file is an action. Linking is an action. Generating code from a schema is an action. The edges are dependencies — the linker action consumes the object files the compile actions produced.&lt;/p&gt;
&lt;p&gt;Here is the key insight that everything else rests on. If you treat each action as a pure function — same inputs always produce the same outputs — then two things become possible that are not possible with a pile of shell scripts. You can run independent actions anywhere, on any machine, in parallel. And you can cache the result of any action, because identical inputs mean you already know the output.&lt;/p&gt;
&lt;p&gt;Remote execution is what you get when you take those two possibilities seriously.&lt;/p&gt;
&lt;h3 id=&quot;what-an-action-actually-is&quot;&gt;What an action actually is&lt;/h3&gt;
&lt;p&gt;Concretely, an action is described by its command, the content of every input it depends on, and the environment it runs in. Crucially, inputs are identified by the hash of their content, not their path or timestamp:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Action {
  command:       [&quot;clang&quot;, &quot;-c&quot;, &quot;parser.c&quot;, &quot;-o&quot;, &quot;parser.o&quot;]
  input_digests: { &quot;parser.c&quot;: sha256(abc1...),
                   &quot;parser.h&quot;: sha256(def2...) }
  platform:      { os: linux, arch: x86_64, ... }
}

action_digest = sha256(serialize(Action))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;action_digest&lt;/code&gt; is the core lookup key. It fingerprints everything the build system has declared as relevant to the output. Correctness therefore depends on complete input and environment declarations: an undeclared dependency is invisible to the key. The build system computes the digest and asks a shared service one question: have you seen this exact declared action before?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cache hit: the service returns the output blobs immediately. No compilation happens at all — not locally, not remotely. You just fetched the answer.&lt;/li&gt;
&lt;li&gt;Cache miss: the service schedules the action on a remote worker, the worker runs the command in a clean sandbox, uploads the resulting &lt;code&gt;parser.o&lt;/code&gt; to a content-addressed store, and records the action-to-output mapping so the next person gets a hit.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Multiply that across a team and a CI fleet and the effect is dramatic. The first engineer to build a given commit pays the cost; everyone else, and every CI run on that commit, gets cache hits. Work is done once and shared.&lt;/p&gt;
&lt;h3 id=&quot;the-two-pieces-of-machinery&quot;&gt;The two pieces of machinery&lt;/h3&gt;
&lt;p&gt;Underneath, remote execution rests on two services that are simpler than they sound.&lt;/p&gt;
&lt;p&gt;The first is a content-addressed store (the CAS). It is a blob store keyed by the hash of the content. Put bytes in, get a hash back; present a hash, get the bytes. Because the key &lt;em&gt;is&lt;/em&gt; the content's fingerprint, identical files can be deduplicated and a retained blob cannot become stale under the same digest. Storage policies may evict blobs, so clients still have to handle missing content. Source files, intermediate objects, and final binaries can all live here.&lt;/p&gt;
&lt;p&gt;The second is the action cache: a map from &lt;code&gt;action_digest&lt;/code&gt; to the set of output digests it produced. That is the lookup that turns &quot;compile this&quot; into &quot;you already compiled this, here are the bytes.&quot;&lt;/p&gt;
&lt;p&gt;Bazel and Buck2 both support the common &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/bazelbuild/remote-apis&quot;&gt;Remote Execution API&lt;/a&gt; for these services, which allows compatible deployments to point either client at the same class of remote backend. You do not need to know the protocol to use it, but it helps to know it exists: the build tool on your laptop coordinates the graph while remote workers and shared caches handle much of the expensive execution and storage.&lt;/p&gt;
&lt;h3 id=&quot;why-this-reshapes-how-build-systems-are-designed&quot;&gt;Why this reshapes how build systems are designed&lt;/h3&gt;
&lt;p&gt;Once you see the build as a graph of cacheable pure functions, the design decisions of modern build systems stop looking arbitrary.&lt;/p&gt;
&lt;p&gt;Why does Bazel make you declare every input and output explicitly, instead of letting a command read whatever files it wants? Because the action digest has to capture every input. An undeclared input is an input the cache cannot see, which means the cache will hand you a stale result. Why the obsession with hermetic, sandboxed actions? Because a worker on the other side of the cluster has to be able to run your action with nothing but the declared inputs and get the same answer your laptop would. The strictness that feels like bureaucracy when you are writing a &lt;code&gt;BUILD&lt;/code&gt; file is the exact thing that makes the action a pure function — and a pure function is the thing you can distribute and cache.&lt;/p&gt;
&lt;h3 id=&quot;where-it-gets-hard&quot;&gt;Where it gets hard&lt;/h3&gt;
&lt;p&gt;It is not free, and the failure modes are worth knowing before you adopt it.&lt;/p&gt;
&lt;p&gt;The network becomes the new bottleneck. You have traded local CPU for cache lookups and blob transfers, so a slow link or a far-away CAS can make a &quot;cached&quot; build feel slower than a local one. Large inputs and outputs strain the CAS and the network; build graphs with giant artifacts need care. And the whole edifice rests on one assumption that deserves its own discussion: that actions are actually deterministic. The moment an action produces different output from identical inputs, the cache starts handing out wrong or useless answers, and the value proposition quietly inverts.&lt;/p&gt;
&lt;p&gt;That last point is important enough that it is the subject of its own post. For now, the mental model is the takeaway: a build is a graph of actions, each action is a pure function fingerprinted by its inputs, and remote execution is just the infrastructure for running those functions anywhere and remembering their answers. Once that clicks, the rest of the modern build world — the explicit inputs, the sandboxing, the content hashing — reads as the obvious consequence it is.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23681/remote-execution-explained-for-people-who-just-run-make</guid>
<pubDate>Tue, 28 Jul 2026 15:51:22 +0000</pubDate>
</item>
<item>
<title>Request-Response: The Foundation of Modern APIs</title>
<link>https://coderlegion.com/23486/request-response-the-foundation-of-modern-apis</link>
<description>&lt;p&gt;Most developers use Request-Response every day.&lt;/p&gt;
&lt;p&gt;Few stop to think about what actually happens after clicking a button.&lt;/p&gt;
&lt;p&gt;You open an app.&lt;/p&gt;
&lt;p&gt;You tap &lt;strong&gt;&quot;Login&quot;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A response appears almost instantly.&lt;/p&gt;
&lt;p&gt;Simple, right?&lt;/p&gt;
&lt;p&gt;Not exactly.&lt;/p&gt;
&lt;p&gt;Behind that seemingly simple interaction lies one of the most important communication patterns in software engineering:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Request-Response.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Whether you're using ChatGPT, ordering food through Uber Eats, checking your bank balance, or watching Netflix, countless Request-Response interactions occur every second.&lt;/p&gt;
&lt;p&gt;Understanding this pattern is one of the first steps toward understanding modern backend engineering.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-request-response-&quot;&gt;What Is Request-Response?&lt;/h3&gt;
&lt;p&gt;At its core, Request-Response is a conversation between two systems.&lt;/p&gt;
&lt;p&gt;One system asks for something.&lt;/p&gt;
&lt;p&gt;Another system answers.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client                    Server
   |                         |
   |------ Request ---------&amp;gt;|
   |                         |
   |&amp;lt;----- Response ---------|
   |                         |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The client initiates communication.&lt;/p&gt;
&lt;p&gt;The server processes the request and returns a response.&lt;/p&gt;
&lt;p&gt;This interaction is typically short-lived and synchronous.&lt;/p&gt;
&lt;p&gt;The client sends a request and waits for an answer.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;a-real-world-analogy&quot;&gt;A Real-World Analogy&lt;/h3&gt;
&lt;p&gt;Imagine you're sitting in a restaurant.&lt;/p&gt;
&lt;p&gt;You call the waiter.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Customer -&amp;gt; &quot;I'd like a burger.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The waiter takes your order to the kitchen.&lt;/p&gt;
&lt;p&gt;The kitchen prepares the meal.&lt;/p&gt;
&lt;p&gt;The waiter returns with your food.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Kitchen -&amp;gt; &quot;Here's your burger.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's Request-Response.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Customer = Client
Waiter = Network
Kitchen = Server
Burger = Response
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Most web applications operate using exactly this pattern.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-journey-of-a-request&quot;&gt;The Journey of a Request&lt;/h3&gt;
&lt;p&gt;Consider a simple login request.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;POST /login
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It may look simple from the browser's perspective.&lt;/p&gt;
&lt;p&gt;But internally:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Browser
   |
Load Balancer
   |
API Gateway
   |
Authentication Service
   |
Database
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The response must travel back through the same chain.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Database
   |
Authentication Service
   |
API Gateway
   |
Load Balancer
   |
Browser
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every step introduces latency, failure possibilities, and complexity.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;latency-the-hidden-cost-of-distance&quot;&gt;Latency: The Hidden Cost of Distance&lt;/h2&gt;
&lt;p&gt;Latency is the time required for a request to travel through the system and return a response.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Request Sent ---------------------&amp;gt;

&amp;lt;-------------------- Response Received
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Users don't care about architecture diagrams.&lt;/p&gt;
&lt;p&gt;They care about speed.&lt;/p&gt;
&lt;p&gt;When someone clicks a button, they expect a response immediately.&lt;/p&gt;
&lt;p&gt;Even small delays are noticeable.&lt;/p&gt;
&lt;p&gt;Typical user perception:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Response Time &lt;/th&gt;&lt;th&gt;User Experience&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;0–100 ms &lt;/td&gt;&lt;td&gt;Instant&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;100–300 ms &lt;/td&gt;&lt;td&gt;Fast&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;300–1000 ms &lt;/td&gt;&lt;td&gt;Noticeable&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;1–3 sec &lt;/td&gt;&lt;td&gt;Slow&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;3+ sec &lt;/td&gt;&lt;td&gt;Frustrating&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;At scale, reducing latency becomes a competitive advantage.&lt;/p&gt;
&lt;p&gt;This is one reason companies invest heavily in:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;CDNs&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Caching&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Edge Computing&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Database Optimization&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Load Balancing&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;timeouts-waiting-forever-is-not-an-option&quot;&gt;Timeouts: Waiting Forever Is Not an Option&lt;/h2&gt;
&lt;p&gt;Imagine calling a restaurant and nobody answers.&lt;/p&gt;
&lt;p&gt;How long should you wait?&lt;/p&gt;
&lt;p&gt;Five seconds?&lt;/p&gt;
&lt;p&gt;Five minutes?&lt;/p&gt;
&lt;p&gt;An hour?&lt;/p&gt;
&lt;p&gt;Software faces the same problem.&lt;/p&gt;
&lt;p&gt;A timeout defines the maximum amount of time a system is willing to wait for a response.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client ---- Request ----&amp;gt;
                ...
                ...
                ...
Timeout Reached
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without timeouts, systems can become stuck waiting forever.&lt;/p&gt;
&lt;p&gt;This leads to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Hanging requests&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Resource exhaustion&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Poor user experience&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every production-grade system uses timeouts.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;retries-what-if-the-request-fails-&quot;&gt;Retries: What If the Request Fails?&lt;/h2&gt;
&lt;p&gt;Networks are imperfect.&lt;/p&gt;
&lt;p&gt;Packets get lost.&lt;/p&gt;
&lt;p&gt;Servers restart.&lt;/p&gt;
&lt;p&gt;Databases become temporarily unavailable.&lt;/p&gt;
&lt;p&gt;A single failure doesn't necessarily mean the operation should fail.&lt;/p&gt;
&lt;p&gt;Instead, many systems retry.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Attempt #1 -&amp;gt; Failed

Attempt #2 -&amp;gt; Failed

Attempt #3 -&amp;gt; Success
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Retries improve reliability.&lt;/p&gt;
&lt;p&gt;But they also introduce a dangerous problem.&lt;/p&gt;
&lt;p&gt;What if the original request actually succeeded?&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;idempotency-preventing-duplicate-operations&quot;&gt;Idempotency: Preventing Duplicate Operations&lt;/h2&gt;
&lt;p&gt;Imagine clicking:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Pay Now
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The payment succeeds.&lt;/p&gt;
&lt;p&gt;But the response never reaches your browser.&lt;/p&gt;
&lt;p&gt;Your application thinks the request failed.&lt;/p&gt;
&lt;p&gt;It retries.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Charge Card
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Again.&lt;/p&gt;
&lt;p&gt;Without protection, the customer could be charged twice.&lt;/p&gt;
&lt;p&gt;This is why payment systems use idempotency.&lt;/p&gt;
&lt;p&gt;An idempotent operation produces the same result even if executed multiple times.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Request ID: abc123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the same request arrives again:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Request ID: abc123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server recognizes it and returns the previous result instead of processing the payment again.&lt;/p&gt;
&lt;p&gt;This concept is critical in systems like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Stripe&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;PayPal&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Banking Platforms&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Order Processing Systems&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;fan-out-one-request-can-become-many&quot;&gt;Fan-Out: One Request Can Become Many&lt;/h2&gt;
&lt;p&gt;A request rarely stays simple for long.&lt;/p&gt;
&lt;p&gt;Imagine opening a product page.&lt;/p&gt;
&lt;p&gt;The backend may need:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Product Service
Review Service
Inventory Service
Pricing Service
Recommendation Service
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One incoming request becomes many downstream requests.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;                 Product
                /
Client ---- API Gateway
                \
                 Reviews

                 Inventory

                 Pricing
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This pattern is called Fan-Out.&lt;/p&gt;
&lt;p&gt;Fan-Out increases functionality.&lt;/p&gt;
&lt;p&gt;But it also increases risk.&lt;/p&gt;
&lt;p&gt;If one dependency becomes slow, the entire response can become slow.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;cascading-failures&quot;&gt;Cascading Failures&lt;/h2&gt;
&lt;p&gt;Now imagine the Review Service becomes overloaded.&lt;/p&gt;
&lt;p&gt;Requests begin timing out.&lt;/p&gt;
&lt;p&gt;The Product Service waits.&lt;/p&gt;
&lt;p&gt;Threads become blocked.&lt;/p&gt;
&lt;p&gt;Connection pools fill up.&lt;/p&gt;
&lt;p&gt;More requests pile up.&lt;/p&gt;
&lt;p&gt;Soon:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Review Service Failure
        |
        v
Product Service Slow
        |
        v
API Gateway Slow
        |
        v
Entire Application Slow
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A small issue spreads throughout the system.&lt;/p&gt;
&lt;p&gt;This is called a Cascading Failure.&lt;/p&gt;
&lt;p&gt;Many major outages start exactly this way.&lt;/p&gt;
&lt;p&gt;A single dependency struggles.&lt;/p&gt;
&lt;p&gt;Everything connected to it suffers.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;observability-seeing-what-the-system-sees&quot;&gt;Observability: Seeing What the System Sees&lt;/h2&gt;
&lt;p&gt;When users complain:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&quot;The app feels slow.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Where exactly is the problem?&lt;/p&gt;
&lt;p&gt;The database?&lt;/p&gt;
&lt;p&gt;The cache?&lt;/p&gt;
&lt;p&gt;The network?&lt;/p&gt;
&lt;p&gt;The payment service?&lt;/p&gt;
&lt;p&gt;Without visibility, debugging becomes guesswork.&lt;/p&gt;
&lt;p&gt;Modern systems rely on observability.&lt;/p&gt;
&lt;p&gt;Three pillars are commonly used:&lt;/p&gt;
&lt;h4 id=&quot;logs&quot;&gt;Logs&lt;/h4&gt;
&lt;p&gt;Individual events.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;User logged in
Payment created
Order shipped
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;metrics&quot;&gt;Metrics&lt;/h4&gt;
&lt;p&gt;System health measurements.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;CPU Usage
Memory Usage
Request Rate
Error Rate
Latency
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;traces&quot;&gt;Traces&lt;/h4&gt;
&lt;p&gt;The complete journey of a request.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Client
  -&amp;gt; Gateway
     -&amp;gt; Auth Service
        -&amp;gt; Database
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Tracing reveals where time is spent.&lt;/p&gt;
&lt;p&gt;At scale, observability becomes essential.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;why-request-response-eventually-becomes-a-bottleneck&quot;&gt;Why Request-Response Eventually Becomes a Bottleneck&lt;/h2&gt;
&lt;p&gt;Request-Response is simple.&lt;/p&gt;
&lt;p&gt;That's its greatest strength.&lt;/p&gt;
&lt;p&gt;But it's also its limitation.&lt;/p&gt;
&lt;p&gt;The client must wait.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Request ---&amp;gt;

&amp;lt;--- Response
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No response means no progress.&lt;/p&gt;
&lt;p&gt;As systems grow, engineers begin exploring alternatives:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Polling&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Long Polling&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Server-Sent Events (SSE)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;WebSockets&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pub/Sub&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Event-Driven Architectures&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These patterns exist because traditional Request-Response is not always enough.&lt;/p&gt;
&lt;p&gt;But every one of them builds upon the foundation established by Request-Response.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;final-thoughts-1&quot;&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;Request-Response appears deceptively simple.&lt;/p&gt;
&lt;p&gt;A client asks.&lt;/p&gt;
&lt;p&gt;A server answers.&lt;/p&gt;
&lt;p&gt;Yet hidden beneath that simplicity are some of the most important challenges in software engineering:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Latency&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Timeouts&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Retries&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Idempotency&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fan-Out&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cascading Failures&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Observability&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Understanding these concepts is what separates someone who can build APIs from someone who can design reliable systems.&lt;/p&gt;
&lt;p&gt;Before learning WebSockets, Event Streaming, or Distributed Systems, it's worth mastering the communication pattern that powers nearly every application on the internet:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Request-Response.&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23486/request-response-the-foundation-of-modern-apis</guid>
<pubDate>Sun, 26 Jul 2026 06:46:57 +0000</pubDate>
</item>
<item>
<title>I Doubled My Servers and Wrecked My Availability</title>
<link>https://coderlegion.com/21448/i-doubled-my-servers-and-wrecked-my-availability</link>
<description>&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/10g0fapwadet1hbtfb4h.png&quot; alt=&quot;Cover Image&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Reliability is the most important feature of any system.” — Bill Gates&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Why does your bank never show the wrong balance, but your favorite app sometimes takes a few seconds to update? Why do some systems feel fast but crash under load, while others feel slower but never fail?&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Performance answers: “How fast is the system right now?”&lt;br&gt;
Scalability answers: “What happens when demand increases?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;These questions point to a fundamental tension in system design: &lt;strong&gt;performance vs scalability&lt;/strong&gt;.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;A system can improve performance metrics like throughput and latency, yet fail to scale under load. This article demonstrates exactly that.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In this article, we’ll explore this trade-off through a simple analogy, and then connect it to real-world systems using measurable metrics like latency, throughput, and error rate.&lt;/p&gt;
&lt;h3 id=&quot;the-burger-shop-analogy&quot;&gt;The Burger Shop Analogy&lt;/h3&gt;
&lt;p&gt;Imagine a small burger shop run by a single worker, let’s call them &lt;strong&gt;A&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Let’s assume the following things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Burgers are prepared automatically by machines, so A only handles taking and serving orders.&lt;/li&gt;
&lt;li&gt;Finished burgers are placed in a shared box, from which A picks them up and delivers them to customers.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We intentionally ignore preparation time and assume burgers are instantly ready. This allows us to focus purely on how requests are handled, rather than how they are produced.&lt;/p&gt;
&lt;h4 id=&quot;the-single-worker-system&quot;&gt;The Single Worker System&lt;/h4&gt;
&lt;p&gt;In this setup, a single worker handles all incoming requests.&lt;/p&gt;
&lt;p&gt;When you enter the shop, you join a single queue and wait your turn before placing an order. Every request is processed sequentially (one at a time).&lt;/p&gt;
&lt;p&gt;A few key observations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Single queue, sequential processing&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;With only one line, every customer must wait for those ahead of them. This results in higher waiting time, which translates to &lt;strong&gt;high latency&lt;/strong&gt; and &lt;strong&gt;low throughput&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No contention&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Since only one request is handled at a time, there is no competition for shared resources. The system operates without conflicts or synchronization issues.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Efficient resource usage&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Resources are used strictly on demand. Each burger is prepared only when needed, with no excess consumption or overhead.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;what-does-this-imply-&quot;&gt;What does this imply?&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;High latency&lt;/strong&gt; → Each request experiences delay due to queuing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Low throughput&lt;/strong&gt; → Only one request is processed at any given time&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No contention&lt;/strong&gt; → No conflicts between concurrent operations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Despite its limitations, this system is &lt;strong&gt;remarkably stable&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;There are no race conditions, no lost work, and no unexpected failures. Every request is handled in a predictable and controlled manner.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Slow, but predictable.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;scaling-the-system&quot;&gt;Scaling the System&lt;/h4&gt;
&lt;p&gt;So far, the limitation has been clear: a single worker handling all requests.&lt;/p&gt;
&lt;p&gt;To improve this, we introduce more machines. Instead of relying on a single machine, worker A can now distribute incoming orders across multiple machines.&lt;/p&gt;
&lt;p&gt;Let’s examine what changes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Parallel processing&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;With multiple machines available, several orders can be handled simultaneously. This increases the number of requests processed per unit time, leading to &lt;strong&gt;higher throughput&lt;/strong&gt; and &lt;strong&gt;reduced latency&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Decision overhead (routing)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Worker A must now decide which machine should handle each request. This introduces a small coordination overhead — similar to how a load balancer (e.g., Nginx) routes requests in real systems.&lt;/p&gt;
&lt;p&gt;For simplicity, we will ignore this overhead in our analysis.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;what-happens-to-the-metrics-&quot;&gt;What happens to the metrics?&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Multiple orders can be processed simultaneously.&lt;/li&gt;
&lt;li&gt;Waiting time decreases&lt;/li&gt;
&lt;li&gt;Throughput increases&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This approach is known as &lt;strong&gt;horizontal scaling&lt;/strong&gt;, increasing system capacity by adding more resources rather than making a single component more powerful.&lt;/p&gt;
&lt;h4 id=&quot;the-hidden-problem&quot;&gt;The Hidden Problem&lt;/h4&gt;
&lt;p&gt;So far, scaling seems like a clear improvement.&lt;/p&gt;
&lt;p&gt;But consider the shared burger box , the place where all machines deposit finished burgers.&lt;/p&gt;
&lt;p&gt;What happens if multiple machines try to place burgers into the box at the same time?&lt;/p&gt;
&lt;p&gt;This introduces two critical failure scenarios:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Lost Work&lt;/p&gt;
&lt;p&gt;One burger makes it into the box, but another falls to the ground.&lt;/p&gt;
&lt;p&gt;That burger is wasted, and the customer never receives their order.&lt;/p&gt;
&lt;p&gt;In system terms, this is a &lt;strong&gt;failed request&lt;/strong&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;System Blockage&lt;/p&gt;
&lt;p&gt;Both burgers get stuck at the entrance of the box, blocking it completely.&lt;/p&gt;
&lt;p&gt;Now no further burgers can be placed.&lt;/p&gt;
&lt;p&gt;In system terms, this resembles &lt;strong&gt;contention or resource locking&lt;/strong&gt;, where the system slows down or stops entirely.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;What Changed ?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;By introducing multiple machines, we improved performance:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Higher throughput, lower latency&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Multiple burgers can now be prepared simultaneously. This increases throughput and reduces waiting time for customers, improving overall performance.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Increased coordination and state management&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;With two machines handling requests, worker A must decide where each order goes. Both machines can process requests, so their state must be tracked and logs must be maintained. This introduces coordination overhead.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Inefficiency due to lost work&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Some burgers may fall to the ground before reaching the box. These orders are wasted, and the customer never receives their burger. In system terms, this represents failed or lost requests.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Unpredictable latency&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Consider a customer waiting for an order that never arrives. If they leave after 30 seconds, that request now has very high latency. Even if other customers are served quickly, overall latency becomes inconsistent and harder to predict.This is the core trade-off.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In system terms, we have introduced:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Contention for shared resources&lt;/li&gt;
&lt;li&gt;Increased system complexity&lt;/li&gt;
&lt;li&gt;Possibility of failure&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;the-core-trade-off&quot;&gt;The Core Trade-off&lt;/h4&gt;
&lt;p&gt;This is the fundamental trade-off in system design.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Some customers are served faster, but others may never get their order.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;performance&quot;&gt;Performance&lt;/h3&gt;
&lt;p&gt;Performance refers to how effectively a system executes tasks within a given time frame. It is typically evaluated in terms of speed, responsiveness, and how efficiently system resources are utilized under load.&lt;/p&gt;
&lt;p&gt;To understand and measure performance, we rely on a few key metrics:&lt;/p&gt;
&lt;h4 id=&quot;latency&quot;&gt;Latency&lt;/h4&gt;
&lt;p&gt;Latency is the time it takes for a system to respond to a request.&lt;/p&gt;
&lt;p&gt;In simpler terms, it measures how long a user has to wait after initiating an action.&lt;/p&gt;
&lt;h4 id=&quot;throughput&quot;&gt;Throughput&lt;/h4&gt;
&lt;p&gt;Throughput represents the number of requests or transactions a system can handle per unit of time.&lt;/p&gt;
&lt;p&gt;It reflects the system’s capacity to process workload at scale.&lt;/p&gt;
&lt;h4 id=&quot;resource-utilization&quot;&gt;Resource Utilization&lt;/h4&gt;
&lt;p&gt;Resource utilization measures how much of the system’s resources, such as CPU, memory, and network bandwidth are consumed while handling requests.&lt;/p&gt;
&lt;p&gt;Efficient systems aim to maximize output while minimizing unnecessary resource consumption.&lt;/p&gt;
&lt;h4 id=&quot;efficiency&quot;&gt;Efficiency&lt;/h4&gt;
&lt;p&gt;Efficiency describes how well a system converts resources into useful work.&lt;/p&gt;
&lt;p&gt;A highly efficient system achieves high throughput and low latency while using minimal resources.&lt;/p&gt;
&lt;h3 id=&quot;scalability&quot;&gt;Scalability&lt;/h3&gt;
&lt;p&gt;Scalability refers to a system’s ability to handle increasing load by proportionally utilizing additional resources, while maintaining acceptable performance.&lt;/p&gt;
&lt;p&gt;In other words, a scalable system does not degrade significantly as the number of users or requests grows, it adapts by efficiently distributing the workload.&lt;/p&gt;
&lt;p&gt;To evaluate scalability, we observe how key performance metrics behave under increasing load, including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Latency under load&lt;/li&gt;
&lt;li&gt;Throughput under load&lt;/li&gt;
&lt;li&gt;Resource utilization&lt;/li&gt;
&lt;li&gt;Error rates (failures, timeouts)&lt;/li&gt;
&lt;li&gt;Number of concurrent users supported&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A system is considered scalable if these metrics remain stable or degrade gracefully as demand increases.&lt;/p&gt;
&lt;h4 id=&quot;availability&quot;&gt;Availability&lt;/h4&gt;
&lt;p&gt;Availability is the measure of a system’s ability to respond to requests within an acceptable time frame.&lt;/p&gt;
&lt;p&gt;A system is considered available if it is operational and capable of returning responses, even under load. High availability systems are designed to minimize downtime and ensure that users can reliably access the service.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“A system can be highly performant but not scalable. for example, a single powerful server may respond quickly, but fail when user load increases.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;the-monolith-vs-the-load-balanced-fleet-a-vs-2a-s-&quot;&gt;The Monolith vs. The Load-Balanced Fleet (A vs 2A’s)&lt;/h3&gt;
&lt;p&gt;To move beyond theory, we need to observe how a system behaves when pushed to its absolute limits. To test the relationship between compute scaling, database bottlenecks, and overall availability, an A/B load test was constructed.&lt;/p&gt;
&lt;p&gt;The code for the experiments can be found in attached github repository: {% github &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/c0d3l0v3r-HeHe/performance-vs-scalability-experiment&quot;&gt;https://github.com/c0d3l0v3r-HeHe/performance-vs-scalability-experiment&lt;/a&gt; %}&lt;/p&gt;
&lt;h3 id=&quot;assumptions&quot;&gt;Assumptions&lt;/h3&gt;
&lt;p&gt;Before running the load test, the architecture was built on three core hypotheses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Linear Scalability:&lt;/strong&gt; Doubling the compute resources (adding a second application server) will linearly increase the system’s maximum throughput.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Testing Default Database Limits:&lt;/strong&gt; We hypothesized that the default connection pool of a single MongoDB instance would be sufficient to handle the concurrent load of doubled compute resources.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Load Balancer Penalty is Negligible:&lt;/strong&gt; The network hop introduced by routing traffic through a reverse proxy will be outweighed by the massive gains in request processing speed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;setup-methodology&quot;&gt;Setup Methodology&lt;/h3&gt;
&lt;p&gt;The experiment was conducted locally using Docker to containerize and isolate the environments. The testing tool, k6, was configured to inject a stepped load simulating aggressive traffic spikes: starting at 100 concurrent virtual users and ramping up to 2,000 users over an 80-second window.&lt;/p&gt;
&lt;p&gt;The workload itself was a mixed Read/Write operation. Every request forced the application to write a new document to the database and immediately read a document back, simulating a standard transactional endpoint.&lt;/p&gt;
&lt;p&gt;We tested three distinct architectures:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Architecture A (The Baseline):&lt;/strong&gt; A single Node.js/Express application connected directly to a standalone MongoDB instance. This represents a traditional, unscaled monolith.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Architecture 2A (The Distributed Compute):&lt;/strong&gt; Two identical Node.js/Express applications sitting behind an Nginx load balancer. Nginx utilized a round-robin strategy to distribute incoming k6 traffic across both nodes. Crucially, both application nodes were wired to the exact same single MongoDB instance.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const express = require(&quot;express&quot;);
const mongoose = require(&quot;mongoose&quot;);
const os = require(&quot;os&quot;);

const app = express();


mongoose.connect(&quot;mongodb://mongo:27017/testdb&quot;, {
  maxPoolSize: 50,
});

const TestSchema = new mongoose.Schema({
  value: Number,
});

const Test = mongoose.model(&quot;Test&quot;, TestSchema);

app.get(&quot;/&quot;, async (req, res) =&amp;gt; {
  const start = Date.now();

  try {
    await Test.create({ value: Math.random() });
    await Test.findOne();

    const latency = Date.now() - start;

    res.json({
      latency,
      server: os.hostname(), 
    });
  } catch (err) {
    res.status(500).json({ error: &quot;DB error&quot; });
  }
});

app.listen(3000, () =&amp;gt; console.log(`Server running on ${os.hostname()}`));
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;results&quot;&gt;Results&lt;/h4&gt;
&lt;p&gt;The k6 load test yielded drastically different saturation points for the two architectures.&lt;/p&gt;
&lt;p&gt;These reports are present as the .html files in the github respository.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Architecture A (Single Node Baseline):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/xy7rb03cgxh4kec3dtq0.png&quot; alt=&quot;The latency trade-off. With a single Node.js event loop acting as a natural throttle, the p95 latency sat at ~796ms. The system was relatively slow under heavy load, but perfectly stable.&quot;&gt;&lt;/p&gt;
&lt;p&gt;The latency trade-off. With a single Node.js event loop acting as a natural throttle, the p95 latency sat at ~796ms. The system was relatively slow under heavy load, but perfectly stable.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/gpjcg2yl4lcfzgkh8ey6.png&quot; alt=&quot;Baseline throughput. The single-node architecture safely processed a total of 176,584 requests, hitting a hard bottleneck at roughly 2,193 requests per second.&quot;&gt;&lt;/p&gt;
&lt;p&gt;Baseline throughput. The single-node architecture safely processed a total of 176,584 requests, hitting a hard bottleneck at roughly 2,193 requests per second.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/vnaupmlr7zaamfuv26lo.png&quot; alt=&quot;_100% Availability. Because the compute layer could not overwhelm the database connection pool, every single read/write transaction was successfully completed without a single dropped request._&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;100% Availability. Because the compute layer could not overwhelm the database connection pool, every single read/write transaction was successfully completed without a single dropped request.&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Total Requests Processed:&lt;/strong&gt; 176,584&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Throughput:&lt;/strong&gt; 2,193 requests/second&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Latency (p95):&lt;/strong&gt; ~796 ms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Failure Rate:&lt;/strong&gt; 0.00% (100% Success)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Architecture 2A (Distributed Compute):&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/y6i59v9irnffmubrmxu6.png&quot; alt=&quot;The smoking gun. While the p95 latency for successful requests actually dropped to ~492ms, the maximum latency spiked to an unusable 36 seconds. The explicit HTTP 500 errors confirm the single MongoDB instance was pushed into connection exhaustion.&quot;&gt;&lt;/p&gt;
&lt;p&gt;The smoking gun. While the p95 latency for successful requests actually dropped to ~492ms, the maximum latency spiked to an unusable 36 seconds. The explicit HTTP 500 errors confirm the single MongoDB instance was pushed into connection exhaustion.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/cr2vdpu34fu2dpepiv33.png&quot; alt=&quot;The illusion of scale. Adding a load balancer and a second application node successfully pushed our throughput to over 3,600 requests per second, processing 350,261 total requests.&quot;&gt;&lt;/p&gt;
&lt;p&gt;The illusion of scale. Adding a load balancer and a second application node successfully pushed our throughput to over 3,600 requests per second, processing 350,261 total requests.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/n1gfuo2yla5pnbw37fwn.png&quot; alt=&quot;The breaking point. Scaling the compute layer while leaving the data layer untouched resulted in a massive 26.5% failure rate, actively degrading the system’s availability.&quot;&gt;&lt;/p&gt;
&lt;p&gt;The breaking point. Scaling the compute layer while leaving the data layer untouched resulted in a massive 26.5% failure rate, actively degrading the system’s availability.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Total Requests Processed:&lt;/strong&gt; 350,261&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Throughput:&lt;/strong&gt; 3,638 requests/second&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Latency (p95):&lt;/strong&gt; ~492 ms (for successful requests)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Failure Rate:&lt;/strong&gt; &lt;strong&gt;26.57%&lt;/strong&gt; (93,069 failed requests, including explicit HTTP 500 database errors and massive timeouts spiking up to 36 seconds).&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;discussion&quot;&gt;Discussion&lt;/h3&gt;
&lt;p&gt;The experimental data explicitly demonstrates the systemic risks of isolated horizontal scaling. While adding compute resources improved specific performance metrics, it fundamentally compromised system reliability, proving that scaling a single tier does not equate to scaling the system as a whole.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. The Shifting Bottleneck and Resource Exhaustion&lt;/strong&gt; The baseline load test (Architecture A) established a clear system ceiling: a single Node.js instance capped throughput at 2,193 requests per second with a p95 latency of ~796ms. Crucially, the failure rate was 0.00%. In this configuration, the compute layer acted as a strict upstream bottleneck. Because a single server could not process requests fast enough to overwhelm the database’s connection pool (configured to a maximum of 50 connections), the database was naturally shielded from resource exhaustion. The system was relatively slow under peak load, but highly stable.&lt;/p&gt;
&lt;p&gt;By introducing a load balancer and a second application node (Architecture 2A), the compute-layer bottleneck was removed. The system accepted more concurrent traffic, driving throughput up by roughly 65% (to 3,638 req/sec). However, because both nodes funneled this doubled traffic into the single, unscaled MongoDB instance, the bottleneck immediately shifted to the data layer. The sudden influx of concurrent requests exhausted the database connection pool, leading to connection lockups and explicit HTTP 500 errors.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. The False Positive of “Improved” Performance&lt;/strong&gt; A superficial reading of Architecture 2A’s metrics might suggest a performance win: the p95 latency for successful requests actually dropped by ~38% (from 796ms down to 492ms), and total processed requests nearly doubled.&lt;/p&gt;
&lt;p&gt;However, a rigorous analysis reveals this “scale” is an illusion. The data shows a catastrophic failure rate of 26.57% (93,069 failed requests). The reduction in p95 latency for successful requests is effectively a survivorship bias metric; it only accounts for the requests that managed to secure a database connection. Meanwhile, the maximum latency for blocked requests spiked to an unusable 36 seconds. The experiment proves that forcing high throughput at the compute layer without matching capacity at the data layer destroys system availability.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. The Inadequacy of Isolated Scaling&lt;/strong&gt; The results strictly invalidate the hypothesis that doubling compute resources linearly increases maximum throughput &lt;em&gt;safely&lt;/em&gt;. Instead, it proves that architectural scaling must be holistic.&lt;/p&gt;
&lt;p&gt;When Architecture 2A pushed the database beyond its concurrency limits, the system did not degrade gracefully — it fractured, dropping more than a quarter of all user requests.&lt;/p&gt;
&lt;h4 id=&quot;conclusion&quot;&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;The data validates a core principle of distributed systems:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;throughput and availability are often inversely correlated when shared resources are unscaled.&lt;/strong&gt; Architecture A was unscalable but highly available (100% success). Architecture 2A was highly performant for a subset of users but critically unavailable for the rest (26.57% failure).&lt;/p&gt;
&lt;p&gt;To successfully scale this system beyond the 2,193 req/sec ceiling of Architecture A without incurring the 26% failure penalty of Architecture 2A, the architecture must evolve to protect the database. Based strictly on these failure modes, future iterations must either synchronously scale the data layer (e.g., database sharding/replicas to handle the higher connection volume) or implement an asynchronous buffer (e.g., a message queue) to decouple the request ingestion rate from the database write rate.&lt;/p&gt;
&lt;h3 id=&quot;acknowledgements-amp-references&quot;&gt;Acknowledgements &amp;amp; References&lt;/h3&gt;
&lt;p&gt;The experiments and conclusions drawn in this article were directly informed by studying foundational distributed systems literature. Translating these concepts from theory into a live, containerized k6 experiment was made possible by the following resources:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Performance, Scalability &amp;amp; Throughput&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=3HIV4MnLGCw&quot;&gt;&lt;strong&gt;MIT 6.004: Performance Measures (YouTube)&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;:&lt;/strong&gt; Provided the classic “laundry analogy” illustrating why pipelining improves overall throughput but not individual request latency.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.professorbeekums.com/performance-vs-scalability/&quot;&gt;&lt;strong&gt;Performance vs. Scalability (Prof. Beekums)&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;:&lt;/strong&gt; Clarified the boundaries between making a single server faster versus designing an architecture to handle increased load.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.allthingsdistributed.com/2006/03/a_word_on_scalability.html&quot;&gt;&lt;strong&gt;A Word on Scalability (Werner Vogels)&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;:&lt;/strong&gt; Foundational principles from Amazon’s CTO on building systems that scale gracefully.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://cs.fyi/guide/latency-vs-throughput&quot;&gt;&lt;strong&gt;Latency vs Throughput (CS.fyi)&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;&amp;amp;&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://community.cadence.com/cadence_blogs_8/b/fv/posts/understanding-latency-vs-throughput&quot;&gt;&lt;strong&gt;Understanding Latency vs Throughput (Cadence)&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;:&lt;/strong&gt; Core definitions and metrics for measuring system speed and capacity.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The CAP Theorem &amp;amp; Consistency Patterns&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://robertgreiner.com/cap-theorem-revisited/&quot;&gt;&lt;strong&gt;CAP Theorem Revisited (Robert Greiner)&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;&amp;amp;&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;http://ksat.me/a-plain-english-introduction-to-cap-theorem&quot;&gt;&lt;strong&gt;A Plain English Introduction to CAP (ksat.me)&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;:&lt;/strong&gt; Explained the core mathematical trade-off: why our partitioned, load-balanced system had to sacrifice availability when tied to a single database.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://cs.fyi/guide/consistency-patterns-week-strong-eventual&quot;&gt;&lt;strong&gt;Consistency Patterns (CS.fyi)&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;:&lt;/strong&gt; Detailed breakdowns of Weak, Eventual, and Strong Consistency, which directly inspired the Redis asynchronous queue experiment.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;High Availability, Failover &amp;amp; Database Replication&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.youtube.com/watch?v=LdvduBxZRLs&quot;&gt;&lt;strong&gt;Availability in Distributed Systems (GKCS — YouTube)&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;:&lt;/strong&gt; A practical war story detailing a real-world database migration, downtime management, and understanding the “9s of availability.”&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.slideshare.net/slideshow/scalability-availability-stability-patterns/4062682&quot;&gt;&lt;strong&gt;Scalability, Availability &amp;amp; Stability Patterns (SlideShare)&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;:&lt;/strong&gt; A comprehensive visual overview of architectural patterns.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.designgurus.io/blog/high-availability-system-design-basics&quot;&gt;&lt;strong&gt;High Availability System Design Basics (DesignGurus)&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;&amp;amp;&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/decoders_lord/system-design-availability-patterns-104i&quot;&gt;&lt;strong&gt;System Design Availability Patterns (Dev.to)&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;:&lt;/strong&gt; Practical guides on identifying and removing single points of failure.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.serverion.com/uncategorized/active-passive-vs-active-active-failover/&quot;&gt;&lt;strong&gt;Active-Passive vs Active-Active Failover (Serverion)&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;:&lt;/strong&gt; The blueprint used for configuring our Nginx load balancer to distribute compute traffic.&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.enjoyalgorithms.com/blog/introduction-to-database-replication-system-design&quot;&gt;&lt;strong&gt;Introduction to Database Replication (EnjoyAlgorithms)&lt;/strong&gt;&lt;/a&gt;&lt;strong&gt;:&lt;/strong&gt; A deep dive into separating read/write workloads and scaling the data tier.&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21448/i-doubled-my-servers-and-wrecked-my-availability</guid>
<pubDate>Sat, 27 Jun 2026 09:58:47 +0000</pubDate>
</item>
<item>
<title>&quot;Done&quot; Is Not a State</title>
<link>https://coderlegion.com/21278/done-is-not-a-state</link>
<description>&lt;blockquote&gt;&lt;p&gt;Completion generates silence. A task that finished and a task that was silently dropped produce the same signal: nothing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/blog/done-is-not-a-state/&quot;&gt;vibeagentmaking.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;On December 16, 2024, a developer filed a bug report against Trigger.dev, the open-source background job framework. A routine nightly server restart had caused random tasks to get stuck in a &quot;queued&quot; state. The system's recovery logic, working exactly as designed, detected the stalled tasks and requeued them. Then it detected them again. And again. By the time anyone checked the dashboard, 3,800 duplicate tasks were sitting in the queue, each one a faithful copy of work that had already been completed.&lt;/p&gt;
&lt;p&gt;The monitoring system showed no errors. Every task had succeeded. The duplicates were executing successfully too. From the system's perspective, nothing was wrong.&lt;/p&gt;
&lt;p&gt;This is the kind of bug that makes senior engineers go quiet. Not because it's complicated — the explanation fits in a sentence — but because the implications are uncomfortable. The system didn't malfunction. It did exactly what it was designed to do: detect abandoned work and retry it. The problem is that &quot;completed successfully&quot; and &quot;abandoned silently&quot; produce the same signal from the outside. Both go quiet.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;two-generals-and-no-good-options&quot;&gt;Two Generals and No Good Options&lt;/h3&gt;
&lt;p&gt;The theoretical foundation for this is older than most production systems running today. In 1985, Fischer, Lynch, and Paterson published their impossibility result in the &lt;em&gt;Journal of the ACM&lt;/em&gt;: given the possibility of even a single faulty process, it is impossible for a system of processes to agree on a decision. The paper is formally about consensus, but its practical implication is about something more mundane. It's about acknowledgments.&lt;/p&gt;
&lt;p&gt;You send a message. Did it arrive? You wait for an acknowledgment. Did the acknowledgment arrive? You could send an acknowledgment of the acknowledgment, but that just moves the problem one level up. This is the Two Generals Problem, and it has no solution. Not &quot;no known solution&quot; — no solution, period. It is a mathematical impossibility, as fundamental to distributed computing as the halting problem is to computation itself.&lt;/p&gt;
&lt;p&gt;Tyler Treat crystallized the practical consequence in a 2015 essay that has since become something of a canonical reference: &quot;You Cannot Have Exactly-Once Delivery.&quot; There are, he argued, exactly two real delivery semantics. At-most-once: acknowledge the message before processing it, accept that crashes will lose data. At-least-once: acknowledge after processing, accept that retries will duplicate work. Everything else is one of these two, wearing a better outfit.&lt;/p&gt;
&lt;p&gt;&quot;Exactly-once delivery in practice,&quot; Treat wrote, &quot;is by faking it&quot; — through idempotent operations, deduplication layers, or application-level state machines that make repeated processing &lt;em&gt;safe&lt;/em&gt; even when the underlying transport cannot make it &lt;em&gt;impossible&lt;/em&gt;. Apache ZooKeeper's Zab protocol demonstrates the approach: state changes are idempotent, so applying the same change multiple times produces no inconsistencies. But this is an application-level guarantee, not a network-level one. The network still delivers messages more than once. The application just learned not to care.&lt;/p&gt;
&lt;p&gt;The theory says duplicates are inevitable. The question isn't whether your system will duplicate work. It's whether it will notice.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-industry-said-this-out-loud&quot;&gt;The Industry Said This Out Loud&lt;/h3&gt;
&lt;p&gt;Here is the part that makes the Trigger.dev incident less surprising and more damning. The largest cloud providers in the world don't just acknowledge duplicate execution. They document it as expected behavior.&lt;/p&gt;
&lt;p&gt;Google Cloud Tasks states it plainly: &quot;In situations where a design trade-off must be made between guaranteed execution and duplicate execution, the service errs on the side of guaranteed execution.&quot; Their published metric: more than 99.999% of tasks are executed only once. Five nines of uniqueness sounds impeccable until you do the arithmetic. At one million tasks per day — a modest load for any serious deployment — 99.999% means ten duplicates daily. Three thousand six hundred and fifty per year. Whether that number is acceptable depends entirely on whether each task is counting page views or charging credit cards.&lt;/p&gt;
&lt;p&gt;AWS is equally explicit. Standard SQS queues guarantee &quot;at-least-once&quot; delivery, and the documentation enumerates three specific scenarios in which Lambda functions will be invoked more than once for the same message: the Lambda service fails to delete the message from SQS before the visibility timeout expires; the Lambda service sends the event but fails to receive acknowledgment; an intermittent issue causes SQS to return the same message on a subsequent poll. The documented mitigation is to store message IDs in DynamoDB and check before processing. But this adds latency, cost, and its own failure modes. What if the DynamoDB write succeeds but the SQS delete fails? You have added a deduplication layer that itself needs deduplication. The turtles go all the way down.&lt;/p&gt;
&lt;p&gt;The documentation exists. The warnings are in print. Almost nobody reads them until after the incident.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;airflow-s-four-year-war-against-done-&quot;&gt;Airflow's Four-Year War Against &quot;Done&quot;&lt;/h3&gt;
&lt;p&gt;If the Trigger.dev case is a snapshot, Apache Airflow's relationship with stuck-queued tasks is a time-lapse.&lt;/p&gt;
&lt;p&gt;With CeleryExecutor — Airflow's most common production deployment pattern — tasks would routinely get stuck in a &quot;queued&quot; state for hours. Sometimes indefinitely. The GitHub issue tracker accumulated reports across several major versions: #21225, tasks stuck in queued state; #13542, tasks stuck scheduled or queued; #26773, tasks stuck after upgrade; #13808, tasks incorrectly marked as orphaned. The core issue was architectural. When a scheduler process died, its tasks became orphans. A different scheduler was supposed to &quot;adopt&quot; them. But if a task had already been marked as STARTED in the Celery results database while remaining QUEUED in Airflow's internal state, no scheduler would ever transfer it out. The task existed in a kind of superposition: simultaneously complete in one system and waiting in another.&lt;/p&gt;
&lt;p&gt;Neither system was wrong. They just disagreed about what &quot;done&quot; meant.&lt;/p&gt;
&lt;p&gt;Airflow 2.6.0, released in April 2023, finally addressed the problem — and the fix is more instructive than the bug. The team didn't write a better timeout algorithm. They didn't add smarter retry logic. They consolidated three separate timeout configurations — &lt;code&gt;kubernetes.worker_pods_pending_timeout&lt;/code&gt;, &lt;code&gt;celery.stalled_task_timeout&lt;/code&gt;, and &lt;code&gt;celery.task_adoption_timeout&lt;/code&gt; — into a single parameter: &lt;code&gt;scheduler.task_queued_timeout&lt;/code&gt;. The fix was moving the &quot;is this task stuck?&quot; question from the executor to the scheduler, giving one component authoritative ownership of the completion state. Even then, Airflow 2.6.3 had to patch additional edge cases where tasks could still get permanently stuck.&lt;/p&gt;
&lt;p&gt;The lesson is worth stating directly. You cannot fix a missing state by building better detection of its absence. If three components each maintain a partial view of &quot;done,&quot; no amount of timeout tuning will make them agree. The number of components that can declare a task complete is inversely proportional to the system's ability to notice when nothing has.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;when-correct-systems-duplicate-correctly&quot;&gt;When Correct Systems Duplicate Correctly&lt;/h3&gt;
&lt;p&gt;The Trigger.dev and Airflow cases are at least recognizable as engineering problems — recoverable, diagnosable, fixable. What happened to Coinbase customers in February 2018 is something different.&lt;/p&gt;
&lt;p&gt;Between January 22 and February 11, customers found duplicate charges on their credit and debit cards. Not two or three charges — seventeen to fifty repetitions of a single cryptocurrency purchase. The root cause was not a software failure. Visa had changed the Merchant Category Code for digital currency transactions. When major banks and card issuers reclassified purchases under the new code, the processing systems refunded original transactions and recharged them under the updated category. Many customers saw the recharge before the refund cleared, producing what looked like mass duplicate billing. Worldpay, Coinbase, and Visa worked together to reverse the duplicates.&lt;/p&gt;
&lt;p&gt;No system malfunctioned. Every component did precisely what it was designed to do. A category reclassification is not a retry — but it triggers the same downstream effect as one. The most dangerous duplicates don't come from bugs. They come from correct systems responding correctly to a state change that nobody modeled as a duplication event.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-invisible-state&quot;&gt;The Invisible State&lt;/h3&gt;
&lt;p&gt;There is a pattern running through every one of these incidents, and it isn't strictly about distributed systems theory. It's about visibility.&lt;/p&gt;
&lt;p&gt;Consider the states a task can occupy: queued, dispatched, running, retrying, failed. Every one of these generates observable activity. Queued tasks sit in a list. Running tasks consume resources. Failed tasks fire alerts. Even retrying tasks produce log entries. Each state is loud.&lt;/p&gt;
&lt;p&gt;Completion generates silence.&lt;/p&gt;
&lt;p&gt;From the perspective of any monitoring system, any reclamation process, any orphan-detection algorithm, a task that completed successfully and a task that was silently dropped look identical. Both stopped producing signals. Both stopped consuming resources. Both went quiet. The only difference between them is that one finished its work and the other didn't — and no system that relies on the &lt;em&gt;absence&lt;/em&gt; of activity can distinguish between the two.&lt;/p&gt;
&lt;p&gt;This is why &quot;done&quot; cannot be treated as the default — the thing that happens when nothing else is happening. &quot;Done&quot; must be an explicit transition, a first-class state with its own signal, its own timestamp, its own acknowledgment path. A task that completes must announce its completion as loudly as a task that fails announces its failure. Otherwise, every recovery system, every health check, every dashboard that monitors for activity will interpret completion as disappearance.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-idempotency-deflection&quot;&gt;The Idempotency Deflection&lt;/h3&gt;
&lt;p&gt;The standard engineering response to all of this is: make your operations idempotent. If running a task twice produces the same result as running it once, duplicates are harmless. Problem solved.&lt;/p&gt;
&lt;p&gt;This is true and incomplete. Idempotency makes duplicate execution safe. It does not make it visible. A pipeline that silently runs every task three times and produces correct results is not a well-functioning system — it is a system burning three times the compute, making three times the API calls, and generating three times the cost, while its dashboard reports 100% success with a clean conscience. Idempotency is a seatbelt, not a steering wheel. It protects you from the consequences of the crash. It does not prevent the crash, and it does not tell you one happened.&lt;/p&gt;
&lt;p&gt;The deeper fix is architectural: one component owns the definition of &quot;done.&quot; One system has the authority to mark a task complete, and every other system defers to it. This is what Airflow 2.6.0 did. This is what Trigger.dev's self-hosted deployments still needed as of late 2024. This is what every team eventually learns after their third duplicate-execution incident. The solution isn't making duplicates safe. It's making completion loud.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-dashboard-said-100-&quot;&gt;The Dashboard Said 100%&lt;/h3&gt;
&lt;p&gt;The most unsettling detail in the Trigger.dev report isn't the 3,800 duplicates. It's that every one of them succeeded. The monitoring dashboard showed a perfect success rate because every task — original and duplicate alike — completed without error. The system was not failing. It was succeeding too many times.&lt;/p&gt;
&lt;p&gt;We build monitoring to detect failure. We set up alerts for errors, timeouts, crashes. We watch for the system to go red. But the most expensive failure mode in distributed computing isn't the one that trips the alarm. It is the one that generates a clean bill of health while quietly tripling your workload, your costs, and your confidence in a number that was never what you thought it meant.&lt;/p&gt;
&lt;p&gt;Silence, in a distributed system, is not peace. It's ambiguity. And until your system learns to announce &quot;done&quot; as loudly as it announces &quot;broken,&quot; you are trusting that ambiguity to mean what you hope it means.&lt;/p&gt;
&lt;p&gt;Your dashboard says 100%. It might even be right. But &quot;right&quot; and &quot;once&quot; are not the same thing.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Sources: M. Fischer, N. Lynch, M. Paterson, &quot;Impossibility of Distributed Consensus with One Faulty Process,&quot; Journal of the ACM 32(2), April 1985. T. Treat, &quot;You Cannot Have Exactly-Once Delivery,&quot; Brave New Geek, 2015. Google Cloud, &quot;Issues and limitations — Cloud Tasks,&quot; cloud.google.com. AWS, &quot;Using Lambda with Amazon SQS,&quot; docs.aws.amazon.com. Trigger.dev GitHub Issue #1566, December 2024. RNHTTR, &quot;Unsticking Airflow: Stuck Queued Tasks Are No More in 2.6.0,&quot; Apache Airflow Blog, 2023. Apache Airflow GitHub Issues #21225, #13542, #26773, #13808. CNBC, &quot;Worldpay and Visa are reversing duplicate transactions for Coinbase users,&quot; February 17, 2018. TechCrunch, &quot;Visa confirms Coinbase wasn't at fault for overcharging users,&quot; February 16, 2018.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Your system announces &quot;broken&quot; with alerts and dashboards. Does it announce &quot;done&quot; with equal conviction?&lt;/strong&gt; Chain of Consciousness treats agent completion as an explicit, anchored event — every decision signed, every state transition timestamped, every completion producing a verifiable artifact rather than silence. One component owns the record, and every downstream system defers to it — the architectural fix this essay prescribes, applied to agent work. &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/verify/&quot;&gt;Verify an agent's decision chain&lt;/a&gt; | &lt;a rel=&quot;nofollow&quot; href=&quot;https://vibeagentmaking.com/chain/&quot;&gt;Follow a claim through its evidence&lt;/a&gt; | &lt;code&gt;pip install agent-rating-protocol&lt;/code&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21278/done-is-not-a-state</guid>
<pubDate>Wed, 24 Jun 2026 11:00:12 +0000</pubDate>
</item>
<item>
<title>Simple Auth Service to a Distributed Authentication Platform with Kafka, Debezium, and Observability</title>
<link>https://coderlegion.com/20838/simple-auth-service-distributed-authentication-platform-kafka-debezium-observability</link>
<description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/challenges/github-2026-05-21&quot;&gt;GitHub Finish-Up-A-Thon Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-i-built&quot;&gt;What I Built&lt;/h3&gt;
&lt;p&gt;I built a Distributed Authentication System as a long-term learning project to explore real-world backend and distributed systems concepts through a familiar use case: user authentication.&lt;/p&gt;
&lt;p&gt;The project started as a simple authentication service with signup and login functionality. Over time, it evolved into a distributed architecture that incorporates event-driven communication, change data capture (CDC), observability, horizontal scaling, and performance testing.&lt;/p&gt;
&lt;p&gt;The current system consists of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PostgreSQL as the primary source of truth for user credentials&lt;/li&gt;
&lt;li&gt;Redis for refresh token storage and session management&lt;/li&gt;
&lt;li&gt;Kafka as the event streaming platform&lt;/li&gt;
&lt;li&gt;Debezium for Change Data Capture (CDC) from PostgreSQL&lt;/li&gt;
&lt;li&gt;MongoDB for materialized user profile documents&lt;/li&gt;
&lt;li&gt;Nginx for load balancing across multiple service instances&lt;/li&gt;
&lt;li&gt;Prometheus and Grafana for monitoring and observability&lt;/li&gt;
&lt;li&gt;k6 for load testing and performance analysis&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When a user signs up, the authentication service writes user data to PostgreSQL. Debezium captures the database change and publishes it to Kafka. A separate profile service consumes the event and creates a corresponding profile document in MongoDB. This allows services to communicate asynchronously while keeping the architecture loosely coupled.&lt;/p&gt;
&lt;p&gt;Beyond implementing features, the main goal of this project was to understand the trade-offs involved in building distributed systems. Throughout development I conducted load-testing experiments, measured replication lag, analyzed bottlenecks, and documented the architectural decisions that shaped the system.&lt;/p&gt;
&lt;p&gt;This project has become my personal distributed systems playground where I can experiment with new ideas, evaluate design decisions, and learn how real systems behave under load.&lt;/p&gt;
&lt;h3 id=&quot;demo&quot;&gt;Demo&lt;/h3&gt;
&lt;h5 id=&quot;github-repository&quot;&gt;GitHub Repository&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/c0d3l0v3r-HeHe/distributed-auth-system&quot;&gt;https://github.com/c0d3l0v3r-HeHe/distributed-auth-system&lt;/a&gt;&lt;/p&gt;
&lt;h5 id=&quot;signup-flow&quot;&gt;Signup Flow&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/30edq7k4gtyzq4aa94en.png&quot; alt=&quot;Image showing the signup flow in the distributed system&quot;&gt;&lt;/p&gt;
&lt;h5 id=&quot;login-flow&quot;&gt;Login Flow&lt;/h5&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nt91s2dpdcn63mw3sbyk.png&quot; alt=&quot;Login architectural diagram&quot;&gt;&lt;/p&gt;
&lt;p&gt;The repository contains additional architecture diagrams, performance experiments, load-testing results, observability metrics, and detailed documentation covering the design decisions and trade-offs explored throughout the project.&lt;/p&gt;
&lt;p&gt;Rather than reproducing all of those results here, I've kept this submission focused on the project's journey and evolution. If you're interested in the deeper technical details, bottleneck analysis, replication lag measurements, scaling experiments, or observability setup, you'll find them documented in the repository.&lt;/p&gt;
&lt;h3 id=&quot;the-comeback-story&quot;&gt;The Comeback Story&lt;/h3&gt;
&amp;lt;!-- Tell us where the project was before and what you changed, fixed, or added to finish it up. --&amp;gt;
&lt;p&gt;This project was not originally intended to be a standalone distributed authentication platform.&lt;/p&gt;
&lt;p&gt;In May, I started building it as the authentication backend for a larger job portal project. The initial goal was fairly straightforward: implement signup, login, token management, and the supporting infrastructure needed for user authentication.&lt;/p&gt;
&lt;p&gt;After building the core authentication service and setting up the initial infrastructure, I shifted my focus to other work and the project was left unfinished. While the foundation existed, many of the ideas I wanted to explore—distributed systems patterns, observability, scalability, and performance analysis—were still missing.&lt;/p&gt;
&lt;p&gt;A few weeks later, I came across the GitHub Finish-Up-A-Thon Challenge and decided to revisit the project instead of letting it remain another abandoned repository.&lt;/p&gt;
&lt;p&gt;Rather than simply cleaning up old code, I used the opportunity to significantly expand the project and turn it into a distributed systems playground.&lt;/p&gt;
&lt;p&gt;During the revival, I:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Added Redis-based refresh token management&lt;/li&gt;
&lt;li&gt;Implemented a CDC pipeline using PostgreSQL, Debezium, and Kafka&lt;/li&gt;
&lt;li&gt;Added a dedicated profile service backed by MongoDB&lt;/li&gt;
&lt;li&gt;Introduced Prometheus metrics and Grafana dashboards&lt;/li&gt;
&lt;li&gt;Added k6 load-testing infrastructure&lt;/li&gt;
&lt;li&gt;Scaled the authentication service horizontally behind Nginx&lt;/li&gt;
&lt;li&gt;Conducted multiple performance experiments and documented the results&lt;/li&gt;
&lt;li&gt;Created architecture diagrams and expanded the project documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One of the most valuable outcomes of revisiting the project was discovering bottlenecks that only became visible under load. While scaling the authentication service improved CPU utilization, load testing revealed that MongoDB and the CDC pipeline became the primary bottlenecks. Investigating these trade-offs taught me far more than simply building the original authentication service.&lt;/p&gt;
&lt;p&gt;By the end of the challenge, the project had evolved from an unfinished backend component into a fully documented distributed authentication system that I can continue using to explore distributed systems concepts, scalability patterns, and performance engineering.&lt;/p&gt;
&lt;h3 id=&quot;my-experience-with-github-copilot&quot;&gt;My Experience with GitHub Copilot&lt;/h3&gt;
&amp;lt;!-- Explain how GitHub Copilot supported your process. --&amp;gt;
&lt;p&gt;GitHub Copilot acted as an implementation and exploration partner throughout the revival of this project.&lt;/p&gt;
&lt;p&gt;For many components, I first designed the architecture and wrote the interfaces, function signatures, and high-level implementation plan. I then used Copilot to generate boilerplate code, suggest implementations, and accelerate repetitive development tasks.&lt;/p&gt;
&lt;p&gt;Copilot was particularly useful for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Generating service scaffolding and repetitive CRUD logic&lt;/li&gt;
&lt;li&gt;Assisting with Docker Compose configuration&lt;/li&gt;
&lt;li&gt;Helping configure Prometheus metrics collection&lt;/li&gt;
&lt;li&gt;Suggesting Kafka consumer and producer implementations&lt;/li&gt;
&lt;li&gt;Writing integration and infrastructure tests&lt;/li&gt;
&lt;li&gt;Explaining configuration options for Debezium and Kafka Connect&lt;/li&gt;
&lt;li&gt;Speeding up refactoring and cleanup work&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One workflow I found especially effective was defining the architecture and API contracts myself, leaving implementation placeholders, and then using Copilot to generate an initial implementation. This allowed me to focus more on system design decisions and less on repetitive coding.&lt;/p&gt;
&lt;p&gt;I also used Copilot while setting up and validating the distributed architecture. It helped me troubleshoot configuration issues, understand service interactions, and quickly iterate on infrastructure changes during development.&lt;/p&gt;
&lt;p&gt;The biggest benefit wasn't code generation itself—it was the ability to move from an idea to an experiment much faster. Since this project is intended as a distributed systems learning playground, that rapid feedback loop allowed me to spend more time investigating architectural trade-offs, performance bottlenecks, and scalability challenges.&lt;/p&gt;
&lt;h3 id=&quot;feedback-welcome&quot;&gt;Feedback Welcome&lt;/h3&gt;
&lt;p&gt;This project started as a learning exercise and has evolved into my personal distributed systems playground.&lt;/p&gt;
&lt;p&gt;If you're an experienced backend or distributed systems engineer and happen to read this submission, I'd genuinely appreciate any feedback on the architecture, design decisions, bottlenecks, or trade-offs discussed throughout the project.&lt;/p&gt;
&lt;p&gt;Some areas I'm currently thinking about include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Improving the CDC pipeline under higher load&lt;/li&gt;
&lt;li&gt;Reducing replication lag and read amplification&lt;/li&gt;
&lt;li&gt;Better approaches to profile materialization&lt;/li&gt;
&lt;li&gt;Scaling strategies beyond the current setup&lt;/li&gt;
&lt;li&gt;Observability improvements and production-readiness considerations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Constructive criticism, alternative approaches, and architecture suggestions are all welcome. One of the main goals of this project is to learn from engineers who have solved these problems in real systems.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4ftryfxkkmystybk64m8.png&quot; alt=&quot;Cover Image&quot;&gt;&lt;/p&gt;
&amp;lt;!-- Don't forget to add a cover image (if you want). --&amp;gt;
&amp;lt;!-- I am a Solo Contributor --&amp;gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20838/simple-auth-service-distributed-authentication-platform-kafka-debezium-observability</guid>
<pubDate>Wed, 17 Jun 2026 18:53:34 +0000</pubDate>
</item>
<item>
<title>Reconciling P2P Collaborative States via WebRTC Data Channels</title>
<link>https://coderlegion.com/20015/reconciling-p2p-collaborative-states-via-webrtc-data-channels</link>
<description>&lt;hr&gt;
&lt;p&gt;title: &quot;Reconciling P2P Collaborative States via WebRTC Data Channels&quot;&lt;br&gt;
published: true&lt;br&gt;
description: &quot;Mastering peer-to-peer signaling, NAT traversal mechanics (STUN/TURN), and robust client-side state synchronization using WebRTC Data Channels.&quot;&lt;br&gt;
tags: webdev, javascript, typescript, distributedsystems&lt;br&gt;
canonical_url: &lt;a rel=&quot;nofollow&quot; href=&quot;https://akinseinde.netlify.app/articles/webrtc-sync&quot;&gt;https://akinseinde.netlify.app/articles/webrtc-sync&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;series-web-engineering-series-&quot;&gt;series: &quot;Web Engineering Series&quot;&lt;/h3&gt;
&lt;p&gt;&amp;gt;&lt;strong&gt;This is an excerpt.&lt;/strong&gt; The full article includes a &lt;strong&gt;live client-to-client WebRTC simulator&lt;/strong&gt; — walk step-by-step through a simulated P2P handshake (Offer, STUN check, Signaling relay, Answer, Connection) and watch direct message sync bypass the central server entirely. &lt;a rel=&quot;nofollow&quot; href=&quot;https://akinseinde.netlify.app/articles/webrtc-sync&quot;&gt;Read the full interactive version →&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-peer-to-peer-paradigm&quot;&gt;The Peer-to-Peer Paradigm&lt;/h3&gt;
&lt;p&gt;Modern web architectures almost universally rely on client-server protocols (HTTP, WebSockets). While this simplifies state coordination, it introduces overhead: latency, server infrastructure costs, and single-point-of-failure bottlenecks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;WebRTC (Web Real-Time Communication)&lt;/strong&gt; allows browsers to establish direct &lt;strong&gt;peer-to-peer (P2P)&lt;/strong&gt; channels. Once connected, data flows directly between user devices with sub-10ms latencies, completely bypassing intermediate servers.&lt;/p&gt;
&lt;p&gt;While WebRTC is widely known for audio/video streaming, its most powerful asset for application engineering is the &lt;strong&gt;&lt;code&gt;RTCDataChannel&lt;/code&gt;&lt;/strong&gt; API — allowing arbitrary, high-throughput, low-latency binary or text transfer directly between browsers.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-p2p-paradox-nats-and-signaling&quot;&gt;The P2P Paradox: NATs and Signaling&lt;/h3&gt;
&lt;p&gt;Two browsers cannot simply talk directly to each other out of the box. Security boundaries and network topologies get in the way. Specifically, browsers face two barriers:&lt;/p&gt;
&lt;h4 id=&quot;1-the-nat-firewall-barrier&quot;&gt;1. The NAT / Firewall Barrier&lt;/h4&gt;
&lt;p&gt;Almost all consumer devices sit behind &lt;strong&gt;Network Address Translators (NATs)&lt;/strong&gt; and firewalls. A device knows its internal private IP (e.g. &lt;code&gt;192.168.1.45&lt;/code&gt;), but has no idea what its public-facing IP is, nor can it accept unsolicited incoming connections from the open web.&lt;/p&gt;
&lt;p&gt;To cross this barrier, WebRTC utilizes &lt;strong&gt;ICE (Interactive Connectivity Establishment)&lt;/strong&gt; alongside two types of auxiliary servers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;STUN (Session Traversal Utilities for NAT)&lt;/strong&gt;: A lightweight server that simply tells a requesting client: &lt;em&gt;&quot;Here is your public-facing IP address and port.&quot;&lt;/em&gt; This is cheap, fast, and succeeds in ~85% of standard residential network setups.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TURN (Traversal Using Relays around NAT)&lt;/strong&gt;: If symmetric NATs or strict corporate firewalls block direct connection, a TURN server acts as a fallback relay. All traffic is routed through it. This guarantees connection but incurs bandwidth costs.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;2-the-signaling-bottleneck&quot;&gt;2. The Signaling Bottleneck&lt;/h4&gt;
&lt;p&gt;To negotiate a direct connection, peers must exchange configuration details — specifically &lt;strong&gt;Session Description Protocol (SDP)&lt;/strong&gt; objects (containing codecs, encryption keys, and connection profiles) and &lt;strong&gt;ICE Candidates&lt;/strong&gt; (discovered IP/port routing paths).&lt;/p&gt;
&lt;p&gt;Since they don't have a direct connection yet, they must use an out-of-band &lt;strong&gt;Signaling Server&lt;/strong&gt; (typically running WebSockets or SSE) to exchange these initial handshakes.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Peer A                     Signaling Server                     Peer B
  │                                │                                │
  │ ─── 1. Send SDP Offer ───────&amp;gt; │ ─────────────────────────────&amp;gt; │
  │                                │                                │
  │ &amp;lt;── 2. Send SDP Answer ─────── │ &amp;lt;───────────────────────────── │
  │                                │                                │
  │ ─── 3. Exchange ICE Candidates │ ─────────────────────────────&amp;gt; │
  │                                │                                │
  │ &amp;lt;── [DIRECT P2P DATA CHANNEL ESTABLISHED (BYPASSES SERVER)] ──&amp;gt; │
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;implementing-webrtc-data-channels-in-typescript&quot;&gt;Implementing WebRTC Data Channels in TypeScript&lt;/h3&gt;
&lt;p&gt;Here is a clean, dependency-free implementation of a WebRTC initiator client:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;class P2PPeer {
  private pc: RTCPeerConnection;
  private dataChannel: RTCDataChannel | null = null;
  private signalingSocket: WebSocket;

  constructor(signalingUrl: string) {
    this.signalingSocket = new WebSocket(signalingUrl);
    
    // Configure standard public STUN servers
    this.pc = new RTCPeerConnection({
      iceServers: [
        { urls: &quot;stun:stun.l.google.com:19302&quot; },
        { urls: &quot;stun:stun1.l.google.com:19302&quot; }
      ]
    });

    this.setupIceGathering();
  }

  // Initiator: Create Offer and send to Peer
  async initiateConnection(targetPeerId: string) {
    // 1. Create the data channel on the local connection
    this.dataChannel = this.pc.createDataChannel(&quot;p2p-sync-channel&quot;, {
      ordered: true // Guarantees in-order delivery of packets
    });
    this.bindChannelEvents(this.dataChannel);

    // 2. Generate SDP Offer
    const offer = await this.pc.createOffer();
    await this.pc.setLocalDescription(offer);

    // 3. Dispatch to signaling server
    this.signalingSocket.send(JSON.stringify({
      type: &quot;offer&quot;,
      target: targetPeerId,
      sdp: offer
    }));
  }

  // Receiver: Accept Offer, create Answer
  async handleOffer(offerSdp: RTCSessionDescriptionInit, senderId: string) {
    // 1. Set remote description
    await this.pc.setRemoteDescription(new RTCSessionDescription(offerSdp));

    // 2. Listen for the channel incoming event
    this.pc.ondatachannel = (event) =&amp;gt; {
      this.dataChannel = event.channel;
      this.bindChannelEvents(this.dataChannel);
    };

    // 3. Generate SDP Answer
    const answer = await this.pc.createAnswer();
    await this.pc.setLocalDescription(answer);

    // 4. Send back via signaling
    this.signalingSocket.send(JSON.stringify({
      type: &quot;answer&quot;,
      target: senderId,
      sdp: answer
    }));
  }

  private setupIceGathering() {
    // Broadcast discovered local ICE routes to remote peer
    this.pc.onicecandidate = (event) =&amp;gt; {
      if (event.candidate) {
        this.signalingSocket.send(JSON.stringify({
          type: &quot;ice-candidate&quot;,
          candidate: event.candidate
        }));
      }
    };
  }

  private bindChannelEvents(channel: RTCDataChannel) {
    channel.onopen = () =&amp;gt; console.log(&quot;Direct P2P Data Channel Opened!&quot;);
    channel.onmessage = (event) =&amp;gt; {
      const data = JSON.parse(event.data);
      console.log(&quot;Received P2P Message:&quot;, data);
    };
    channel.onclose = () =&amp;gt; console.log(&quot;Data Channel Closed.&quot;);
  }

  send(message: any) {
    if (this.dataChannel &amp;amp;&amp;amp; this.dataChannel.readyState === &quot;open&quot;) {
      this.dataChannel.send(JSON.stringify(message));
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;state-reconciliations-over-p2p&quot;&gt;State Reconciliations over P2P&lt;/h3&gt;
&lt;p&gt;Direct connections introduce concurrency challenges. If both clients edit a shared local state concurrently, how do they sync without a server?&lt;/p&gt;
&lt;p&gt;The standard solution is pairing &lt;strong&gt;&lt;code&gt;RTCDataChannel&lt;/code&gt;&lt;/strong&gt; with a &lt;strong&gt;CRDT (Conflict-free Replicated Data Type)&lt;/strong&gt; engine (like Y.js or Automerge). The data channel acts as a raw transport layer, while the CRDT handles deterministic state updates.&lt;/p&gt;
&lt;p&gt;Because the data channel supports binary buffers (&lt;code&gt;arraybuffer&lt;/code&gt;), you can transmit highly compact, compressed state updates directly:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;channel.send(Y.encodeStateAsUpdate(ydoc));
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;engineering-takeaways&quot;&gt;Engineering Takeaways&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;WebRTC enables direct P2P connection&lt;/strong&gt;, dropping transport latency to the raw physical limits of the network.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Signaling servers are only required for connection setup&lt;/strong&gt; (SDP/ICE exchange) — once connected, the signaling server can crash and the P2P connection remains active.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;STUN servers are free and easy to host&lt;/strong&gt;, but always configure &lt;strong&gt;TURN servers&lt;/strong&gt; as relays for symmetric or enterprise networks.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;RTCDataChannel&lt;/code&gt; handles ordered or unordered data transmission&lt;/strong&gt;, making it highly customizable for games, file sharing, or collaborative documents.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&amp;gt;&lt;strong&gt;The full article features a step-by-step WebRTC handshake visualizer&lt;/strong&gt; — walk through the signaling architecture with a live interactive simulation of Offer generation, STUN query routing, and P2P connection establishment.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://akinseinde.netlify.app/articles/webrtc-sync&quot;&gt;Read the full interactive article →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Written by &lt;strong&gt;Ebenezer Akinseinde&lt;/strong&gt; — Software Developer &amp;amp; AI Automations Engineer.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://akinseinde.netlify.app&quot;&gt;Portfolio&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ebendttl&quot;&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20015/reconciling-p2p-collaborative-states-via-webrtc-data-channels</guid>
<pubDate>Tue, 09 Jun 2026 07:16:53 +0000</pubDate>
</item>
<item>
<title>Distributed Systems: Implementing the Raft Consensus Protocol from Scratch</title>
<link>https://coderlegion.com/20014/distributed-systems-implementing-the-raft-consensus-protocol-from-scratch</link>
<description>&lt;hr&gt;
&lt;p&gt;title: &quot;Distributed Systems: Implementing the Raft Consensus Protocol from Scratch&quot;&lt;br&gt;
published: true&lt;br&gt;
description: &quot;Deconstructing distributed consensus, state machine replication, leader election algorithms, and split-brain mitigation in distributed clusters.&quot;&lt;br&gt;
tags: computerscience, javascript, typescript, distributedsystems&lt;br&gt;
canonical_url: &lt;a rel=&quot;nofollow&quot; href=&quot;https://akinseinde.netlify.app/articles/raft-consensus&quot;&gt;https://akinseinde.netlify.app/articles/raft-consensus&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;series-systems-engineering-series-&quot;&gt;series: &quot;Systems Engineering Series&quot;&lt;/h3&gt;
&lt;p&gt;&amp;gt;&lt;strong&gt;This is an excerpt.&lt;/strong&gt; The full article includes a &lt;strong&gt;live interactive Raft consensus cluster simulator&lt;/strong&gt; — control a 5-node distributed cluster, trigger leader elections, partition nodes to simulate network splits, commit client state updates, and watch the term-clock and replication logs resolve conflicts in real time. &lt;a rel=&quot;nofollow&quot; href=&quot;https://akinseinde.netlify.app/articles/raft-consensus&quot;&gt;Read the full interactive version →&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-distributed-state-problem&quot;&gt;The Distributed State Problem&lt;/h3&gt;
&lt;p&gt;How do you get multiple independent computer nodes to agree on a single sequence of events, especially when individual nodes can crash, experience network delays, or drop packets?&lt;/p&gt;
&lt;p&gt;This is the core challenge of &lt;strong&gt;Distributed Consensus&lt;/strong&gt;. In a distributed database, all replicas must execute incoming commands in identical order to maintain a consistent state.&lt;/p&gt;
&lt;p&gt;For years, the industry standard was &lt;strong&gt;Paxos&lt;/strong&gt; — an algorithm famously powerful but incredibly complex to understand and implement without error.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Raft&lt;/strong&gt; was designed as an alternative. It breaks down the consensus problem into three modular sub-problems: &lt;strong&gt;Leader Election&lt;/strong&gt;, &lt;strong&gt;Log Replication&lt;/strong&gt;, and &lt;strong&gt;Safety Invariants&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-three-raft-node-states&quot;&gt;The Three Raft Node States&lt;/h3&gt;
&lt;p&gt;At any given moment, every node in a Raft cluster operates in one of three distinct roles:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;        ┌───────────────────────────────────────┐
        │                FOLLOWER               │
        └───────────────────────────────────────┘
          │ (Times out, starts election)   ▲
          ▼                                │ (Discovers current leader)
        ┌───────────────────────────────────────┐
        │               CANDIDATE               │
        └───────────────────────────────────────┘
          │ (Receives majority votes)      
          ▼                                
        ┌───────────────────────────────────────┐
        │                 LEADER                │
        └───────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Follower&lt;/strong&gt;: Completely passive. Responds to incoming RPC calls from Candidates and Leaders. If a follower receives no communication for a randomized timeout period, it assumes the leader has crashed and transitions to a Candidate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Candidate&lt;/strong&gt;: Increments the cluster &quot;term&quot;, votes for itself, and broadcasts requests for votes to all other nodes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Leader&lt;/strong&gt;: Manages all client writes. Coordinates log replication and broadcasts periodic &quot;heartbeat&quot; RPCs to assert authority and reset follower timeout clocks.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;sub-problem-1-leader-election&quot;&gt;Sub-Problem 1: Leader Election&lt;/h3&gt;
&lt;p&gt;To prevent split votes, followers wait for a &lt;strong&gt;randomized election timeout&lt;/strong&gt; (typically between &lt;code&gt;150ms&lt;/code&gt; and &lt;code&gt;300ms&lt;/code&gt;) before initiating an election.&lt;/p&gt;
&lt;p&gt;Once a node's timeout expires, it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Increments its local &lt;strong&gt;&lt;code&gt;currentTerm&lt;/code&gt;&lt;/strong&gt; counter.&lt;/li&gt;
&lt;li&gt;Transitions to the &lt;strong&gt;Candidate&lt;/strong&gt; state.&lt;/li&gt;
&lt;li&gt;Votes for itself.&lt;/li&gt;
&lt;li&gt;Sends a &lt;strong&gt;&lt;code&gt;RequestVote&lt;/code&gt;&lt;/strong&gt; RPC to all other nodes.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If a candidate receives votes from a &lt;strong&gt;majority of nodes&lt;/strong&gt; in the cluster (e.g., 3 out of 5), it immediately transitions to &lt;strong&gt;Leader&lt;/strong&gt; and starts broadcasting heartbeats.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;sub-problem-2-log-replication&quot;&gt;Sub-Problem 2: Log Replication&lt;/h3&gt;
&lt;p&gt;Once a Leader is elected, it acts as the single gateway for all client write requests.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Client ─── Write &quot;X = 5&quot; ───&amp;gt; Leader
                                │
          ┌─────────────────────┴─────────────────────┐
          │ (Append entry, broadcast AppendEntries)   │
          ▼                                           ▼
      Follower A                                  Follower B
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-replication-steps-&quot;&gt;The Replication Steps:&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;The client sends a command (e.g., &lt;code&gt;set x=5&lt;/code&gt;) to the Leader.&lt;/li&gt;
&lt;li&gt;The Leader appends the command to its local log.&lt;/li&gt;
&lt;li&gt;The Leader broadcasts an &lt;strong&gt;&lt;code&gt;AppendEntries&lt;/code&gt;&lt;/strong&gt; RPC containing the new log entry to all followers.&lt;/li&gt;
&lt;li&gt;Followers verify the entry and append it to their logs, sending a success confirmation back.&lt;/li&gt;
&lt;li&gt;Once a majority of followers acknowledge the write, the Leader &lt;strong&gt;commits&lt;/strong&gt; the entry to its state machine and returns a success response to the client.&lt;/li&gt;
&lt;li&gt;In subsequent heartbeats, the Leader notifies followers of the newly committed entry, prompting them to commit it to their local state machines.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;typescript-raft-node-implementation&quot;&gt;TypeScript Raft Node Implementation&lt;/h3&gt;
&lt;p&gt;Here is the baseline state container and interface schema for a Raft consensus node in TypeScript:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;type NodeRole = &quot;Follower&quot; | &quot;Candidate&quot; | &quot;Leader&quot;;

interface LogEntry {
  term: number;
  command: string;
}

interface RequestVoteArgs {
  term: number;
  candidateId: string;
  lastLogIndex: number;
  lastLogTerm: number;
}

interface RequestVoteReply {
  term: number;
  voteGranted: boolean;
}

class RaftNode {
  public id: string;
  public currentTerm = 0;
  public role: NodeRole = &quot;Follower&quot;;
  public votedFor: string | null = null;
  public log: LogEntry[] = [];
  
  private commitIndex = 0;
  private electionTimeout: NodeJS.Timeout | null = null;
  private peers: string[] = [];

  constructor(id: string, peers: string[]) {
    this.id = id;
    this.peers = peers;
    this.resetElectionTimeout();
  }

  private resetElectionTimeout() {
    if (this.electionTimeout) clearTimeout(this.electionTimeout);
    
    // Randomized timeout between 150ms and 300ms prevents split votes
    const timeout = 150 + Math.floor(Math.random() * 150);
    this.electionTimeout = setTimeout(() =&amp;gt; this.startElection(), timeout);
  }

  // Handle incoming vote request from Candidate node
  public handleRequestVote(args: RequestVoteArgs): RequestVoteReply {
    this.resetElectionTimeout();

    // 1. Term check: reject candidates with stale terms
    if (args.term &amp;lt; this.currentTerm) {
      return { term: this.currentTerm, voteGranted: false };
    }

    if (args.term &amp;gt; this.currentTerm) {
      this.currentTerm = args.term;
      this.role = &quot;Follower&quot;;
      this.votedFor = null;
    }

    // 2. Log completeness check: candidate log must be at least as up-to-date as ours
    const lastIndex = this.log.length - 1;
    const lastTerm = lastIndex &amp;gt;= 0 ? this.log[lastIndex].term : 0;
    const logIsUpToDate =
      args.lastLogTerm &amp;gt; lastTerm ||
      (args.lastLogTerm === lastTerm &amp;amp;&amp;amp; args.lastLogIndex &amp;gt;= lastIndex);

    // 3. Vote check
    const canVote = this.votedFor === null || this.votedFor === args.candidateId;

    if (canVote &amp;amp;&amp;amp; logIsUpToDate) {
      this.votedFor = args.candidateId;
      return { term: this.currentTerm, voteGranted: true };
    }

    return { term: this.currentTerm, voteGranted: false };
  }

  private startElection() {
    this.role = &quot;Candidate&quot;;
    this.currentTerm++;
    this.votedFor = this.id;
    this.resetElectionTimeout();

    console.log(`Node ${this.id} starting election for Term ${this.currentTerm}`);
    
    let votesReceived = 1; // Vote for self
    const lastLogIndex = this.log.length - 1;
    const lastLogTerm = lastLogIndex &amp;gt;= 0 ? this.log[lastLogIndex].term : 0;

    for (const peerId of this.peers) {
      // Simulate dispatching network RPC calls to peer cluster
      this.sendRequestVoteRPC(peerId, {
        term: this.currentTerm,
        candidateId: this.id,
        lastLogIndex,
        lastLogTerm
      }).then((reply) =&amp;gt; {
        if (this.role !== &quot;Candidate&quot;) return;

        if (reply.voteGranted) {
          votesReceived++;
          if (votesReceived &amp;gt; (this.peers.length + 1) / 2) {
            this.becomeLeader();
          }
        }
      });
    }
  }

  private becomeLeader() {
    this.role = &quot;Leader&quot;;
    if (this.electionTimeout) clearTimeout(this.electionTimeout);
    console.log(`Node ${this.id} elected Leader for Term ${this.currentTerm}!`);
    this.startHeartbeats();
  }

  private startHeartbeats() {
    // Send AppendEntries heartbeats at 50ms intervals to maintain authority
    setInterval(() =&amp;gt; {
      if (this.role !== &quot;Leader&quot;) return;
      for (const peerId of this.peers) {
        this.sendAppendEntriesRPC(peerId);
      }
    }, 50);
  }

  // Mock network transporters for simulation environment
  private async sendRequestVoteRPC(peerId: string, args: RequestVoteArgs): Promise&amp;lt;RequestVoteReply&amp;gt; {
    return { term: this.currentTerm, voteGranted: Math.random() &amp;gt; 0.3 };
  }

  private async sendAppendEntriesRPC(peerId: string) {
    // Heartbeat logic
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;split-brain-and-minority-partitions&quot;&gt;Split-Brain and Minority Partitions&lt;/h3&gt;
&lt;p&gt;What happens when a network partition cuts a 5-node cluster into two segments: a &lt;strong&gt;majority partition&lt;/strong&gt; (3 nodes) and a &lt;strong&gt;minority partition&lt;/strong&gt; (2 nodes)?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  MAJORITY PARTITION (Can commit)         MINORITY PARTITION (Cannot commit)
   [Node A] ─── [Node B (Leader 1)]         [Node D] ─── [Node E (Leader 2)]
      │                                        │
   [Node C]                                    X (Network Partition cut)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The minority partition might elect a new leader because they can no longer hear from the original leader. However, this minority leader &lt;strong&gt;cannot commit any new client writes&lt;/strong&gt; because they can never achieve the required cluster majority (3 nodes).&lt;/p&gt;
&lt;p&gt;Once the partition heals:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The minority leader receives a heartbeat containing a &lt;strong&gt;higher term clock&lt;/strong&gt; from the majority leader.&lt;/li&gt;
&lt;li&gt;The minority leader immediately steps down back to a Follower.&lt;/li&gt;
&lt;li&gt;The minority nodes discard their uncommitted local log differences and synchronize their states with the majority leader's canonical log.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Raft guarantees that the cluster always converges safely back to a single source of truth.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;engineering-takeaways-1&quot;&gt;Engineering Takeaways&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Raft guarantees Safety and Liveness&lt;/strong&gt; in distributed environments with randomized timeouts.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Commitment requires absolute majority consensus&lt;/strong&gt; — protecting clusters from split-brain state divergence.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Term clocks act as global logical time&lt;/strong&gt; — higher term numbers override all stale cluster leaders.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&amp;gt;&lt;strong&gt;The full article features a live 5-node Raft consensus cluster simulator&lt;/strong&gt; — trigger network cuts to form minority and majority partitions, watch node roles transition, commit client writes, and view live log conflict resolution directly in your browser.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://akinseinde.netlify.app/articles/raft-consensus&quot;&gt;Read the full interactive article →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Written by &lt;strong&gt;Ebenezer Akinseinde&lt;/strong&gt; — Software Developer &amp;amp; AI Automations Engineer.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://akinseinde.netlify.app&quot;&gt;Portfolio&lt;/a&gt; · &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ebendttl&quot;&gt;GitHub&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20014/distributed-systems-implementing-the-raft-consensus-protocol-from-scratch</guid>
<pubDate>Tue, 09 Jun 2026 07:14:44 +0000</pubDate>
</item>
<item>
<title>Monolithic Thinking Is the Biggest Bug in Your Microservice Testing</title>
<link>https://coderlegion.com/18341/monolithic-thinking-is-the-biggest-bug-in-your-microservice-testing</link>
<description>&lt;h2 id=&quot;monolithic-thinking-is-the-biggest-bug-in-your-microservice-testing&quot;&gt;Monolithic Thinking Is the Biggest Bug in Your Microservice Testing&lt;/h2&gt;
&lt;p&gt;If you started your QA career testing a monolithic application, you already have a foundation. But that foundation can quietly work against you in a microservice system — because the rules have changed, and most junior QA engineers don't realize it until something breaks in a way they can't explain.&lt;/p&gt;
&lt;p&gt;This article will help you understand how microservice systems are built, why they behave differently, and what you need to start thinking about as a QA engineer working in one.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-even-is-a-microservice-architecture-&quot;&gt;What Even Is a Microservice Architecture?&lt;/h3&gt;
&lt;p&gt;In a traditional monolithic application, everything lives in one place. The user interface, the business logic, the database — all bundled together. When you test a login feature, you're testing one system.&lt;/p&gt;
&lt;p&gt;Microservice architecture breaks that single system into smaller, independently running services. Each service:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Handles one specific business capability (e.g., payments, user accounts, notifications)&lt;/li&gt;
&lt;li&gt;Manages its own database&lt;/li&gt;
&lt;li&gt;Communicates with other services through APIs or events&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's what a simplified e-commerce system might look like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;API Gateway
              │ 
  ┌───────────┼───────────┐
  ▼           ▼           ▼
User       Order       Payment
Service    Service     Service
  │           │           │
  ▼           ▼           ▼
User DB    Order DB   Payment DB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each of those services is developed, deployed, and scaled by a separate team — independently of the others.&lt;/p&gt;
&lt;p&gt;Flexible? Yes.&lt;br&gt;
Complex to test? Absolutely.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-your-old-testing-approach-breaks-here&quot;&gt;Why Your Old Testing Approach Breaks Here&lt;/h3&gt;
&lt;p&gt;In a monolith, a login flow looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;User enters credentials → Business logic validates → Database checks → Response returned
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's one system. One place to look when something breaks.&lt;/p&gt;
&lt;p&gt;In a microservice system, the exact same login might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Request hits API Gateway
       ↓
  Auth Service (validates credentials)
       ↓
  User Service (fetches user profile)
       ↓
  Session Service (creates session token)
       ↓
  Response returned
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Four services involved in a single login.&lt;/p&gt;
&lt;p&gt;If anything fails — a timeout in the Auth Service, a slow response from the Session Service, a data mismatch in the User Service — the whole flow breaks. And the error message the user sees might give you zero clue about which service actually caused it.&lt;/p&gt;
&lt;p&gt;This is why junior QA engineers often feel lost in microservice environments. They're testing the output without understanding the system producing it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-mindset-shift-from-feature-tester-to-system-observer&quot;&gt;The Mindset Shift: From Feature Tester to System Observer&lt;/h3&gt;
&lt;p&gt;In a monolith, the right question is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Does this feature work correctly?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In microservices, the right question is:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“Which services are involved, and what happens when one of them fails?”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is the core shift.&lt;/p&gt;
&lt;p&gt;You're no longer just validating a response — you're observing a system.&lt;/p&gt;
&lt;p&gt;When you pick up a test scenario, train yourself to ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which services participate in this flow?&lt;/li&gt;
&lt;li&gt;What data does each service depend on?&lt;/li&gt;
&lt;li&gt;What happens if one service responds slowly or not at all?&lt;/li&gt;
&lt;li&gt;Is the data consistent across services after the transaction completes?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You don't need to know how to build microservices to ask these questions. You just need to understand the architecture well enough to think through the flow.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-1-get-the-architecture-diagram-and-actually-read-it-&quot;&gt;Step 1: Get the Architecture Diagram (And Actually Read It)&lt;/h3&gt;
&lt;p&gt;This is the most underrated habit in QA.&lt;/p&gt;
&lt;p&gt;Before you write a single test case for a feature, ask your team for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A service interaction diagram (which services talk to which)&lt;/li&gt;
&lt;li&gt;An event flow diagram (what triggers what)&lt;/li&gt;
&lt;li&gt;A deployment overview (how services are grouped)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These diagrams tell you things that no test case document will:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Where does the data originate?&lt;/li&gt;
&lt;li&gt;Which service owns a piece of data?&lt;/li&gt;
&lt;li&gt;How do services notify each other when something happens?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you're testing a payment flow but you don't know that the Payment Service sends an event to the Notification Service after a transaction, you'll miss testing whether the user gets their confirmation email — because that's not the Payment Service's job.&lt;/p&gt;
&lt;p&gt;Without the diagram, you're testing in the dark.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-2-understand-how-services-talk-to-each-other&quot;&gt;Step 2: Understand How Services Talk to Each Other&lt;/h3&gt;
&lt;p&gt;Microservices don't communicate the same way in all situations. There are two main patterns, and you need to test them differently.&lt;/p&gt;
&lt;h4 id=&quot;synchronous-communication-http-grpc-&quot;&gt;Synchronous Communication (HTTP / gRPC)&lt;/h4&gt;
&lt;p&gt;One service directly calls another and waits for a response.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Service A ──── REST API call ───→ Service B
               (waits for response)
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;what-to-test-&quot;&gt;What to test:&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;Does the API return the correct data when everything works?&lt;/li&gt;
&lt;li&gt;What happens when Service B is slow? Does Service A handle the timeout?&lt;/li&gt;
&lt;li&gt;What error does the user see if Service B is down?&lt;/li&gt;
&lt;li&gt;Are the API contracts (request/response formats) being respected?&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;asynchronous-communication-events-message-queues-&quot;&gt;Asynchronous Communication (Events / Message Queues)&lt;/h4&gt;
&lt;p&gt;One service publishes an event. Another service picks it up later — independently.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Service A → publishes event to Kafka → Service B consumes it → Service C is triggered
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is trickier to test because there's no instant response. The flow happens over time.&lt;/p&gt;
&lt;h5 id=&quot;what-to-test--1&quot;&gt;What to test:&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;Is the event published in the correct format?&lt;/li&gt;
&lt;li&gt;Does Service B actually consume the event and process it correctly?&lt;/li&gt;
&lt;li&gt;What happens if the event is published but Service B is temporarily down — does the message get processed later?&lt;/li&gt;
&lt;li&gt;Are there duplicate events being sent? How does the system handle that?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As a junior QA engineer, asynchronous flows are where most real bugs hide. If you're not thinking about them, you're missing a big part of your system.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-3-understand-who-owns-the-data&quot;&gt;Step 3: Understand Who Owns the Data&lt;/h3&gt;
&lt;p&gt;In a monolith, there's usually one database. You can query it directly to verify that data was saved correctly.&lt;/p&gt;
&lt;p&gt;In microservices, every service has its own database:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Order Service   → Order Database
Payment Service → Payment Database
User Service    → User Database
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a testing challenge called &lt;strong&gt;eventual consistency&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;When an order is placed, the Order Service records it immediately — but the Payment Service might take a few milliseconds (or longer) to update its records. During that window, the data across the two databases is temporarily inconsistent.&lt;/p&gt;
&lt;h4 id=&quot;what-junior-qa-engineers-often-miss-&quot;&gt;What junior QA engineers often miss:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Checking the Order Database but forgetting to verify the Payment Database&lt;/li&gt;
&lt;li&gt;Not accounting for the delay in asynchronous updates&lt;/li&gt;
&lt;li&gt;Assuming that if one service confirms success, all services have updated&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A good habit: after any cross-service transaction, check the relevant database in each service that was involved. Don't just trust the API response.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-4-learn-to-follow-a-request-through-the-system&quot;&gt;Step 4: Learn to Follow a Request Through the System&lt;/h3&gt;
&lt;p&gt;When a bug happens in a microservice system, “it's broken” tells you almost nothing. You need to know where it broke.&lt;/p&gt;
&lt;p&gt;Teams handle this with observability tools — tools that track what a request does as it moves through services:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Centralized logging&lt;/strong&gt; (e.g., ELK Stack, Datadog): collects logs from all services in one place&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Distributed tracing&lt;/strong&gt; (e.g., Jaeger, Zipkin): shows you the full path of a request across services, with timing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monitoring dashboards&lt;/strong&gt;: give you a real-time view of service health&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's what a trace might show you:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Request ID: abc-123
API Gateway        → 5ms
Auth Service       → 12ms
User Service       → 340ms  ← slow!
Session Service    → 8ms
Total: 365ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That trace tells you the User Service is the bottleneck — not the Auth Service, not the gateway. Without tracing, you'd have no idea.&lt;/p&gt;
&lt;p&gt;As a QA engineer, you don't need to set up these tools — but you do need to learn how to read them. Ask your team to walk you through how they trace requests. It will make you significantly more effective at diagnosing failures.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;step-5-test-what-happens-when-things-go-wrong&quot;&gt;Step 5: Test What Happens When Things Go Wrong&lt;/h3&gt;
&lt;p&gt;This is where microservice testing gets interesting — and where most junior QA engineers don't spend enough time.&lt;/p&gt;
&lt;p&gt;In a monolith, you mostly test happy paths and a few negative scenarios. In microservices, failure is a normal part of the system.&lt;/p&gt;
&lt;p&gt;Services go down.&lt;br&gt;
Networks get slow.&lt;br&gt;
Events get delayed.&lt;/p&gt;
&lt;p&gt;The system is supposed to handle these gracefully. Your job is to verify that it does.&lt;/p&gt;
&lt;h4 id=&quot;failure-scenarios-worth-testing-&quot;&gt;Failure scenarios worth testing:&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Scenario &lt;/th&gt;&lt;th&gt;What to verify&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;A dependent service is down &lt;/td&gt;&lt;td&gt;Does the system return a clear error? Does it retry?&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Network latency is high &lt;/td&gt;&lt;td&gt;Does the service timeout correctly?&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;An event is published twice &lt;/td&gt;&lt;td&gt;Does the consumer handle duplicates without creating duplicate records?&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;A service restarts mid-transaction &lt;/td&gt;&lt;td&gt;Is the transaction completed, rolled back, or left in a broken state?&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;concrete-example-&quot;&gt;Concrete example:&lt;/h4&gt;
&lt;p&gt;Your team builds a checkout flow. The Payment Service is unavailable.&lt;/p&gt;
&lt;p&gt;Questions to test:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Does the Order Service still create an order record? Should it?&lt;/li&gt;
&lt;li&gt;Does the user see “Payment failed, please try again” — or a generic 500 error?&lt;/li&gt;
&lt;li&gt;Is there a retry mechanism? How many times does it retry?&lt;/li&gt;
&lt;li&gt;If the payment eventually succeeds on retry, does the order status update correctly?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are not edge cases.&lt;br&gt;
These are real production scenarios.&lt;/p&gt;
&lt;p&gt;Test them.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-testing-pyramid-for-microservices&quot;&gt;The Testing Pyramid for Microservices&lt;/h3&gt;
&lt;p&gt;Effective microservice testing doesn't happen at one level — it happens at four:&lt;/p&gt;
&lt;h4 id=&quot;1-unit-testing&quot;&gt;1. Unit Testing&lt;/h4&gt;
&lt;p&gt;Developers test individual functions in isolation.&lt;br&gt;
You won't own this, but you should know it exists.&lt;/p&gt;
&lt;h4 id=&quot;2-service-testing&quot;&gt;2. Service Testing&lt;/h4&gt;
&lt;p&gt;Each microservice is tested independently, with its dependencies mocked.&lt;br&gt;
You verify that the service behaves correctly on its own.&lt;/p&gt;
&lt;h4 id=&quot;3-integration-testing&quot;&gt;3. Integration Testing&lt;/h4&gt;
&lt;p&gt;Two or more services are tested together.&lt;br&gt;
You verify that Service A and Service B communicate correctly under real conditions.&lt;/p&gt;
&lt;h4 id=&quot;4-end-to-end-testing&quot;&gt;4. End-to-End Testing&lt;/h4&gt;
&lt;p&gt;You test the complete user workflow across all services.&lt;br&gt;
This is where most QA engineers spend their time, and where most real-world failures are caught.&lt;/p&gt;
&lt;p&gt;The further up the pyramid you go, the slower and more expensive the tests become — but also the more realistic.&lt;/p&gt;
&lt;p&gt;Balance matters.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-this-means-for-your-career&quot;&gt;What This Means for Your Career&lt;/h3&gt;
&lt;p&gt;The QA role is evolving.&lt;/p&gt;
&lt;p&gt;In modern teams, a QA engineer who only knows how to write UI test scripts has a ceiling.&lt;/p&gt;
&lt;p&gt;A QA engineer who understands distributed systems, can read traces, and thinks in flows rather than features — that person is genuinely hard to replace.&lt;/p&gt;
&lt;p&gt;You don't need to become a software architect. But you do need to understand the system you're testing well enough to ask the right questions and catch the bugs that only appear at the seams between services.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thought&quot;&gt;Final Thought&lt;/h3&gt;
&lt;p&gt;The hardest bugs in microservice systems don't live inside a single service.&lt;/p&gt;
&lt;p&gt;They live in the space between services — in the assumptions each one makes about the others.&lt;/p&gt;
&lt;p&gt;If you're only testing the API response in front of you, you're seeing one part of a much larger picture.&lt;/p&gt;
&lt;p&gt;Start with the architecture diagram.&lt;br&gt;
Follow the flow.&lt;br&gt;
Test the failures.&lt;/p&gt;
&lt;p&gt;That's where the real testing work is.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Shazzadur Rahman&lt;/strong&gt;&lt;br&gt;
Senior QA Engineer | AI Dashcam &amp;amp; Telematics Testing&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18341/monolithic-thinking-is-the-biggest-bug-in-your-microservice-testing</guid>
<pubDate>Tue, 19 May 2026 18:19:07 +0000</pubDate>
</item>
<item>
<title>CAP Theorem Explained: What I Learned Setting Up MySQL Replication</title>
<link>https://coderlegion.com/17423/cap-theorem-explained-what-i-learned-setting-up-mysql-replication</link>
<description>&lt;hr&gt;
&lt;p&gt;title: CAP Theorem Explained: What I Learned Setting Up MySQL Replication&lt;br&gt;
published: true&lt;/p&gt;
&lt;h3 id=&quot;tags-systemdesign-mysql-distributedsystems-beginners&quot;&gt;tags: systemdesign, mysql, distributedsystems, beginners&lt;/h3&gt;
&lt;p&gt;I recently started my journey into system design and data-intensive applications. This experience has completely changed how I look at software — especially around the hard tradeoffs of consistency, availability, and what happens when things go wrong at scale.&lt;/p&gt;
&lt;p&gt;Let me start with a scenario that surprised me when I first encountered it:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Imagine you just inserted a record in your app. A millisecond later, a read on a different server returns the old data. How? Welcome to distributed systems.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-is-cap-theorem-&quot;&gt;What is CAP Theorem?&lt;/h3&gt;
&lt;p&gt;CAP stands for &lt;strong&gt;Consistency&lt;/strong&gt;, &lt;strong&gt;Availability&lt;/strong&gt;, and &lt;strong&gt;Partition Tolerance&lt;/strong&gt;. The theorem states that a distributed system can only guarantee 2 out of these 3 properties at any given time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Consistency&lt;/strong&gt; — Every read returns the most recent write, or an error. All nodes in the system see the same data at the same time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Availability&lt;/strong&gt; — Every request gets a response (not an error), even if some nodes are down. The system is always up.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Partition Tolerance&lt;/strong&gt; — The system keeps running even when network communication between nodes is lost or delayed.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-real-choice-cp-vs-ap&quot;&gt;The Real Choice: CP vs AP&lt;/h3&gt;
&lt;p&gt;Here is the most important thing to understand: &lt;strong&gt;you cannot actually drop Partition Tolerance.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Network partitions are not a design choice — they are a reality. Hardware fails, packets drop, data centers lose connectivity. Any system that runs across multiple machines will eventually face a partition.&lt;/p&gt;
&lt;p&gt;So the real decision every distributed system makes is: &lt;em&gt;when a partition happens, do you stay consistent or stay available?&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;a-simple-analogy-the-bank-branch&quot;&gt;A Simple Analogy: The Bank Branch&lt;/h3&gt;
&lt;p&gt;Imagine a bank with its HQ in New York and a branch in Boston. The branch always calls HQ before confirming any transaction.&lt;/p&gt;
&lt;p&gt;A customer in Boston deposits $500. Right at that moment, the network between Boston and New York goes down.&lt;/p&gt;
&lt;p&gt;The Boston branch now has two options:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Option 1 — CP (Consistency + Partition Tolerance):&lt;/strong&gt;&lt;br&gt;
The branch refuses to process any transactions until it can reach HQ. The data stays consistent, but the service is unavailable during the outage. If you walk up to the counter, you get turned away.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Option 2 — AP (Availability + Partition Tolerance):&lt;/strong&gt;&lt;br&gt;
The branch processes the deposit locally and syncs with HQ once the network recovers. The service stays available, but if someone checks the account balance at the New York branch before the sync, they see the old amount — temporarily inconsistent data.&lt;/p&gt;
&lt;p&gt;Neither option is wrong. The right choice depends entirely on what your application can tolerate.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;hands-on-mysql-replication-as-an-ap-system&quot;&gt;Hands-On: MySQL Replication as an AP System&lt;/h3&gt;
&lt;p&gt;To make this concrete, I built a small master-slave replication setup using Docker to see these tradeoffs in action.&lt;/p&gt;
&lt;p&gt;You can find the full setup in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/g3x-gauransh/MySQL--Master-slave-replication&quot;&gt;this repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One MySQL container acts as the &lt;strong&gt;master&lt;/strong&gt; — it accepts all writes&lt;/li&gt;
&lt;li&gt;A second container acts as the &lt;strong&gt;slave&lt;/strong&gt; — it monitors the master and replays every change&lt;/li&gt;
&lt;li&gt;The master records every write to a &lt;strong&gt;binary log (binlog)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;The slave runs two threads: an &lt;strong&gt;IO thread&lt;/strong&gt; that pulls the binlog from the master, and a &lt;strong&gt;SQL thread&lt;/strong&gt; that replays those events locally&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What I observed:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When I inserted rows on the master, they showed up on the slave within milliseconds under normal conditions. But when I inserted 10,000 rows rapidly, I could watch the &lt;code&gt;Seconds_Behind_Master&lt;/code&gt; value in &lt;code&gt;SHOW SLAVE STATUS&lt;/code&gt; grow and then shrink back to zero as the slave caught up.&lt;/p&gt;
&lt;p&gt;That gap — &lt;strong&gt;replication lag&lt;/strong&gt; — is the tradeoff in action. During those seconds, a read on the slave returns stale data. The slave is always available to serve reads, but it cannot guarantee it has the latest write.&lt;/p&gt;
&lt;p&gt;This makes the setup an &lt;strong&gt;AP system&lt;/strong&gt;: Available and Partition Tolerant, but not strongly Consistent.&lt;/p&gt;
&lt;p&gt;If you wanted CP behaviour, you would switch to synchronous replication — the master waits for the slave to confirm every write before acknowledging it to the client. You get consistency, but every write now pays the latency cost of that round-trip.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;when-to-choose-cp-vs-ap&quot;&gt;When to Choose CP vs AP&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Prefer CP when a wrong answer is worse than no answer:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Bank balances and financial transactions&lt;/li&gt;
&lt;li&gt;Seat reservation systems (cinema, flights)&lt;/li&gt;
&lt;li&gt;Distributed locks and coordination&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Prefer AP when brief staleness is acceptable:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Social media feeds&lt;/li&gt;
&lt;li&gt;Product catalogs and shopping carts&lt;/li&gt;
&lt;li&gt;DNS resolution&lt;/li&gt;
&lt;li&gt;Caching layers&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;beyond-cap-consistency-is-a-spectrum&quot;&gt;Beyond CAP: Consistency is a Spectrum&lt;/h3&gt;
&lt;p&gt;CAP theorem is a useful mental model, but it is a simplification. In practice, consistency is not binary — there is a whole spectrum:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Linearizability&lt;/strong&gt; — the strongest guarantee; reads always reflect the latest write&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sequential consistency&lt;/strong&gt; — operations appear in the same order across all nodes, but may lag&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Eventual consistency&lt;/strong&gt; — all nodes will converge to the same value, given enough time (this is what MySQL async replication gives you)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want to go deeper, &lt;em&gt;Designing Data-Intensive Applications&lt;/em&gt; by Martin Kleppmann covers replication in Chapter 5 and consistency models in Chapter 9. It is genuinely the best resource I have found on this topic.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;CAP is not a theorem you apply once and move on — it is a lens for thinking about tradeoffs. Every distributed system you design makes a CAP choice, whether you make it explicitly or not. Understanding that choice, and being able to justify it, is what separates a system that works in production from one that only works in theory.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17423/cap-theorem-explained-what-i-learned-setting-up-mysql-replication</guid>
<pubDate>Mon, 11 May 2026 11:37:29 +0000</pubDate>
</item>
<item>
<title>Why Your &quot;Fail-Fast&quot; Strategy is Killing Your Distributed System (and How to Fix It)</title>
<link>https://coderlegion.com/16782/why-your-fail-fast-strategy-is-killing-your-distributed-system-and-how-to-fix-it</link>
<description>&lt;p&gt;It's 2 AM. PagerDuty fires. Redis master is down. Your application, trained to fail fast, dutifully fails — every single request, all at once. By the time Sentinel promotes a new master 12 seconds later, you've already generated 40,000 errors and three escalation calls. The system recovered on its own. Your application didn't let it.&lt;/p&gt;
&lt;p&gt;This is the story of how &quot;good engineering&quot; can make a 12-second infrastructure event into a 12-minute outage — and how to design boundaries that prevent it.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt; — During infrastructure failovers (Redis, Kafka, etcd), blind fail-fast amplifies instability. Bounded retry — centralized, time-boxed, invisible to business logic — absorbs the 10–15 second recovery window without leaking infrastructure noise to users. Resilience is not a library. It is a contract between layers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-core-question&quot;&gt;The Core Question&lt;/h3&gt;
&lt;p&gt;When your session storage — Redis, Memcached, or any stateful dependency — goes temporarily unavailable, you face a fundamental architectural choice:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Should you fail fast? Or should you retry?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We all learned fail-fast as gospel. And it is — until it isn't. During transient infrastructure events like leader elections, blind fail-fast propagates instability instead of containing it. The response you choose determines whether the incident resolves itself in 12 seconds or snowballs into a 12-minute outage with three bridge calls.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-actually-happens-during-failover&quot;&gt;What Actually Happens During Failover&lt;/h3&gt;
&lt;p&gt;To understand why fail-fast can backfire, look at the mechanics of a Redis Sentinel failover:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Phase &lt;/th&gt;&lt;th&gt;Duration &lt;/th&gt;&lt;th&gt;What Happens&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Detection&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;~10–12s &lt;/td&gt;&lt;td&gt;Sentinel quorum detects master is down&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Election&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;~1–2s &lt;/td&gt;&lt;td&gt;Sentinels agree on a new master&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Promotion&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;~1s &lt;/td&gt;&lt;td&gt;Replica promoted, clients notified&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Reconnection&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;~1–3s &lt;/td&gt;&lt;td&gt;Clients re-establish connections&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;em&gt;Note: these phases overlap. Total failover typically completes in 12–15 seconds, not the sum of individual phases. Reconnection time also depends heavily on your client library — a Sentinel-aware client with topology refresh (e.g., Lettuce, go-redis with Sentinel support) reconnects in under a second, while a naive connection pool can take 30s+.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;During this window, your application sees TCP dial timeouts and connection resets. Nothing is broken. No data is lost. The system is doing exactly what it was designed to do — electing a new leader. Your application just needs to not panic for 12 seconds.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-blind-fail-fast-is-dangerous&quot;&gt;Why Blind Fail-Fast Is Dangerous&lt;/h3&gt;
&lt;p&gt;If your application fails immediately on the first connection timeout during this window, four things happen in rapid succession:&lt;/p&gt;
&lt;h4 id=&quot;1-instability-amplification&quot;&gt;1. Instability Amplification&lt;/h4&gt;
&lt;p&gt;A 3-second infrastructure blip becomes a user-visible outage. Every request during the failover window returns an error, even though the system would have recovered on its own.&lt;/p&gt;
&lt;h4 id=&quot;2-infrastructure-semantics-leak-upward&quot;&gt;2. Infrastructure Semantics Leak Upward&lt;/h4&gt;
&lt;p&gt;Your business layer now exposes raw infrastructure details — &quot;Redis connection refused&quot; — to clients that have no idea what Redis is or why it matters.&lt;/p&gt;
&lt;h4 id=&quot;3-uncontrolled-client-retries&quot;&gt;3. Uncontrolled Client Retries&lt;/h4&gt;
&lt;p&gt;Clients receiving errors start retrying independently. If you have 1,000 concurrent users and each retries 3 times, you just turned 1,000 QPS into 3,000 QPS — hitting an infrastructure layer that's already struggling to stabilize.&lt;/p&gt;
&lt;h4 id=&quot;4-retry-storms&quot;&gt;4. Retry Storms&lt;/h4&gt;
&lt;p&gt;This is the catastrophic outcome. Unbounded retries create cascading load amplification. CPU spikes prevent recovery. The system enters an instability feedback loop where the act of trying to recover keeps the system down. I've seen retry storms take down entire regions.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;&quot;Your timeout config was technically correct. Your system was functionally down. That's not a timeout problem — that's a design problem.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here's the distinction that actually matters in production: &lt;strong&gt;the failure TYPE must determine your recovery strategy.&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Infrastructure-Level &lt;/th&gt;&lt;th&gt;Business-Level&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Examples&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Network jitter, leader election, connection reset, &lt;code&gt;READONLY&lt;/code&gt; replica response &lt;/td&gt;&lt;td&gt;Validation error, permission denial, domain rule violation&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Nature&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Transient — will resolve on its own &lt;/td&gt;&lt;td&gt;Permanent — retrying won't help&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Strategy&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;strong&gt;ABSORB&lt;/strong&gt; — retry within bounds &lt;/td&gt;&lt;td&gt;&lt;strong&gt;FAIL FAST&lt;/strong&gt; — return error immediately&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Treating a leader election timeout the same as a schema validation error is an architectural mistake. One will resolve in seconds; the other will never succeed no matter how many times you retry.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-failure-boundary-model&quot;&gt;The Failure Boundary Model&lt;/h3&gt;
&lt;p&gt;This is the architectural pattern that makes everything work:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mermaid.ink/img/Z3JhcGggVEQKICAgIHN1YmdyYXBoIENsaWVudF9MYXllciBbQ2xpZW50IExheWVyXQogICAgICAgIENbUmV0cmllcyBvbmx5IHdoZW4gc2lnbmFsZWQgcmV0cnlhYmxlXQogICAgZW5kCgogICAgc3ViZ3JhcGggQnVzaW5lc3NfTGF5ZXIgW0J1c2luZXNzIExheWVyXQogICAgICAgIEJbUHJlc2VydmVzIHNlbWFudGljIGludGVncml0eTxici8-RkFJTC1GQVNUIEJPVU5EQVJZXQogICAgZW5kCgogICAgc3ViZ3JhcGggSW5mcmFzdHJ1Y3R1cmVfQm91bmRhcnkgW0luZnJhc3RydWN0dXJlIEJvdW5kYXJ5XQogICAgICAgIElbQWJzb3JicyB0cmFuc2llbnQgaW5zdGFiaWxpdHk8YnIvPlJFVFJZIEJPVU5EQVJZXQogICAgZW5kCgogICAgc3ViZ3JhcGggRGVwZW5kZW5jeSBbRGVwZW5kZW5jeV0KICAgICAgICBEW1JlZGlzIC8gTkFUUyAvIEthZmthIC8gREJdCiAgICBlbmQKCiAgICBDIC0tPiBCCiAgICBCIC0tICJCdXNpbmVzcyBFcnJvcnM6IEZhaWwgSW1tZWRpYXRlbHkiIC0tPiBDCiAgICBCIC0tPiBJCiAgICBJIC0tICJCb3VuZGVkIFJldHJ5OiBBYnNvcmJzIDEwLTE1cyBub2lzZSIgLS0-IEIKICAgIEkgLS0-IEQ=&quot; alt=&quot;graph TD&quot;&gt;&lt;/p&gt;
&lt;p&gt;The retry boundary sits in the &lt;strong&gt;infrastructure client wrapper&lt;/strong&gt; — the thin layer between your business code and the dependency client. Not in HTTP middleware, not in individual service handlers, not in a sidecar. In the client wrapper itself.&lt;/p&gt;
&lt;p&gt;Why does this matter? Because if retry logic exists at multiple layers, you get retry amplification. I've seen teams with retry in the HTTP handler, the service layer, AND the Redis client — producing 3 × 3 × 3 = 27 attempts per original request. That's not resilience. That's a DDoS against your own infrastructure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key principles:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Retry belongs at the infrastructure boundary&lt;/strong&gt; — one place, one policy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Business logic must remain fail-fast&lt;/strong&gt; — semantic errors should never be retried.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;By the time an error reaches the client, it has been vetted and classified.&lt;/strong&gt; We are designing for predictability.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;bounded-retry-implementation&quot;&gt;Bounded Retry: Implementation&lt;/h3&gt;
&lt;p&gt;If we're going to retry, we must do it with discipline. Four pillars:&lt;/p&gt;
&lt;h4 id=&quot;1-centralized&quot;&gt;1. Centralized&lt;/h4&gt;
&lt;p&gt;Retry logic lives in one place — the infrastructure client wrapper. Not in individual handlers, not in middleware, not in the business layer. One retry boundary per dependency, one policy, one set of metrics.&lt;/p&gt;
&lt;h4 id=&quot;2-time-bounded&quot;&gt;2. Time-Bounded&lt;/h4&gt;
&lt;p&gt;We define a &lt;strong&gt;retry budget&lt;/strong&gt; — for example, 15 seconds. Why 15? Because it encapsulates the 10–12 second Sentinel detection window plus a margin for stabilization and reconnection. Time-based budgets are superior to pure attempt counts because they normalize across different failure modes — a retry that takes 5s per attempt behaves very differently from one that takes 100ms.&lt;/p&gt;
&lt;h4 id=&quot;3-attempt-limited-with-jitter&quot;&gt;3. Attempt-Limited with Jitter&lt;/h4&gt;
&lt;p&gt;Maximum 2–3 retry attempts within the budget window, with exponential backoff and &lt;strong&gt;jitter&lt;/strong&gt;. Without jitter, synchronized retries from multiple application instances create a thundering herd — everyone hits the new master at exactly the same moment.&lt;/p&gt;
&lt;h4 id=&quot;4-invisible-to-business-logic&quot;&gt;4. Invisible to Business Logic&lt;/h4&gt;
&lt;p&gt;If the retry succeeds within the budget, the business layer never knew there was a problem. If it fails, the business layer receives a clean, classified error — not a raw TCP stack trace that means nothing to anyone above the infrastructure layer.&lt;/p&gt;
&lt;p&gt;Here's what this looks like in practice:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;// Bounded retry wrapper — lives in the infrastructure client layer
func withBoundedRetry(ctx context.Context, budget time.Duration, maxAttempts int, op func() error) error {
    deadline := time.Now().Add(budget)
    var lastErr error

    for attempt := 0; attempt &amp;lt; maxAttempts; attempt++ {
        if time.Now().After(deadline) {
            break
        }

        lastErr = op()
        if lastErr == nil {
            return nil // success — business layer never knew
        }

        if !isRetryable(lastErr) {
            return normalizeError(lastErr) // permanent failure — fail fast
        }

        // Exponential backoff with jitter
        backoff := time.Duration(1&amp;lt;&amp;lt;attempt) * 500 * time.Millisecond
        jitter := time.Duration(rand.Int63n(int64(backoff / 2)))
        select {
        case &amp;lt;-time.After(backoff + jitter):
        case &amp;lt;-ctx.Done():
            return ctx.Err()
        }
    }

    return normalizeError(lastErr) // budget exhausted — fail deterministically
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;┌─────────────────────────────────────────────┐
│          Retry Budget: 15 seconds           │
│                                             │
│  Attempt 1  →  timeout (5s)  →  backoff     │
│  Attempt 2  →  timeout (5s)  →  backoff     │
│  Attempt 3  →  success                      │
│                                             │
│  Total elapsed: ~11s                        │
│  Application impact: ZERO                   │
│                                             │
│  ─── OR ───                                 │
│                                             │
│  Budget exhausted → FAIL DETERMINISTICALLY  │
│  Clean, classified error to business layer  │
└─────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;&quot;Retry is not infinite. Retry is time-boxed. Once the budget is exhausted, we fail deterministically.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;error-normalization&quot;&gt;Error Normalization&lt;/h3&gt;
&lt;p&gt;This is where most teams get it wrong. They retry everything — or nothing. The retry decision must be driven by error classification:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Raw Error &lt;/th&gt;&lt;th&gt;Normalized To &lt;/th&gt;&lt;th&gt;Retryable? &lt;/th&gt;&lt;th&gt;Why&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;TCP dial timeout&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;UNAVAILABLE&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Yes &lt;/td&gt;&lt;td&gt;Connection not established, may recover&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Connection reset&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;UNAVAILABLE&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Yes &lt;/td&gt;&lt;td&gt;Transient network disruption&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;READONLY&lt;/code&gt; (replica) &lt;/td&gt;&lt;td&gt;&lt;code&gt;UNAVAILABLE&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Yes &lt;/td&gt;&lt;td&gt;Sentinel failover in progress — replica not yet promoted&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Leader election in progress&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;UNAVAILABLE&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Yes &lt;/td&gt;&lt;td&gt;Raft/consensus transition&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;OOM command not allowed&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;RESOURCE_EXHAUSTED&lt;/code&gt; &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;Backpressure — retrying makes it worse&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;WRONGTYPE&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;INVALID_ARGUMENT&lt;/code&gt; &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;Schema error — will never succeed&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;NOPERM&lt;/code&gt; / &lt;code&gt;Permission denied&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;PERMISSION_DENIED&lt;/code&gt; &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;Auth failure — will never succeed&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;NOT_FOUND&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;NOT_FOUND&lt;/code&gt; &lt;/td&gt;&lt;td&gt;No &lt;/td&gt;&lt;td&gt;Semantic absence — retry won't create the resource&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The &lt;code&gt;READONLY&lt;/code&gt; case deserves special attention. During Sentinel failover, a replica that hasn't been promoted yet responds with &lt;code&gt;READONLY&lt;/code&gt; to write commands. If your retry layer treats this as a permanent error, your circuit breaker trips, clients get errors, and a 12-second failover becomes a 5-minute outage while someone manually resets the breaker. Classify &lt;code&gt;READONLY&lt;/code&gt; as &lt;code&gt;UNAVAILABLE&lt;/code&gt; — it will resolve when the new master is promoted.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The rule is simple:&lt;/strong&gt; you cannot leak internal implementation details up the stack. Your retry layer must inspect and reclassify errors — not just map them 1:1. Error semantics must align across every layer.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-relationship-with-circuit-breakers&quot;&gt;The Relationship with Circuit Breakers&lt;/h3&gt;
&lt;p&gt;Bounded retry is the &lt;strong&gt;inner loop&lt;/strong&gt; — it handles transient failures within a known recovery window. But what if the dependency is truly down, not just transitioning?&lt;/p&gt;
&lt;p&gt;That's where &lt;strong&gt;circuit breakers&lt;/strong&gt; serve as the &lt;strong&gt;outer loop&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://mermaid.ink/img/Z3JhcGggTFIKICAgIFJlcSgoUmVxdWVzdCkpIC0tPiBDQntDaXJjdWl0IEJyZWFrZXI8YnIvPidPdXRlciBMb29wJ30KICAgIENCIC0tICJIZWFsdGh5IiAtLT4gQlJbQm91bmRlZCBSZXRyeTxici8-J0lubmVyIExvb3AnXQogICAgQlIgLS0-IERlcFsoRGVwZW5kZW5jeSldCgogICAgQ0IgLS0gIk9wZW46IEZhaWx1cmUgUmF0ZSBIaWdoIiAtLT4gRkZbRmFzdCBGYWlsXQogICAgQlIgLS0gIkJ1ZGdldCBFeGhhdXN0ZWQiIC0tPiBFcnJbTm9ybWFsaXplZCBFcnJvcl0KCiAgICBzdHlsZSBDQiBmaWxsOiNmOWYsc3Ryb2tlOiMzMzMsc3Ryb2tlLXdpZHRoOjJweAogICAgc3R5bGUgQlIgZmlsbDojYmJmLHN0cm9rZTojMzMzLHN0cm9rZS13aWR0aDoycHg=&quot; alt=&quot;graph LR&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bounded retry&lt;/strong&gt; absorbs transient events (leader election, network jitter) — seconds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Circuit breaker&lt;/strong&gt; protects against sustained outages (dependency truly dead) — minutes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without a circuit breaker, sustained failures chew through retry budgets on every request, wasting resources. Without bounded retry, every transient blip trips the circuit breaker unnecessarily. They are complementary, not redundant.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;observability-instrument-the-boundary&quot;&gt;Observability: Instrument the Boundary&lt;/h3&gt;
&lt;p&gt;A production retry boundary must emit metrics. Without them, you're flying blind:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;retry_attempt_total&lt;/code&gt;&lt;/strong&gt; — how often retries fire (by dependency, by error type)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;retry_budget_exhausted_total&lt;/code&gt;&lt;/strong&gt; — how often the full budget is consumed without success&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;retry_success_on_attempt&lt;/code&gt;&lt;/strong&gt; — which attempt number succeeds (histogram)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;error_classification&lt;/code&gt;&lt;/strong&gt; — distribution of retryable vs non-retryable errors&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The key alert:&lt;/strong&gt; if retry budget exhaustion rate exceeds ~5%, either your budget is too tight or your dependency is degraded beyond transient. This is the signal that distinguishes a leader election from a real outage — and it's the signal that should trigger your circuit breaker.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;beyond-redis-a-universal-pattern&quot;&gt;Beyond Redis: A Universal Pattern&lt;/h3&gt;
&lt;p&gt;If this looks Redis-specific, zoom out. The bounded retry pattern applies to any stateful dependency with leader election:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Redis Sentinel&lt;/strong&gt; — master failover with quorum detection, 10–15s window&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NATS JetStream&lt;/strong&gt; — stream leader election in the Raft group, typically 2–5s with default election timeout&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;etcd / Consul&lt;/strong&gt; — Raft leader election, ~1–2s with default settings, but watch streams may buffer longer&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kafka&lt;/strong&gt; — partition leader election via controller, typically 5–15s depending on &lt;code&gt;replica.lag.time.max.ms&lt;/code&gt; and ISR size&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CockroachDB / TiKV&lt;/strong&gt; — range leader election, similar Raft mechanics&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The mechanics are the same everywhere: a detection window, a brief period of unavailability, and then recovery. Design your retry budget to absorb that window. Calibrate the budget to the specific system — 15s for Redis Sentinel, 5s for NATS, 20s for Kafka.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-cross-layer-contract&quot;&gt;The Cross-Layer Contract&lt;/h3&gt;
&lt;p&gt;Resilience is not a library you &lt;code&gt;import&lt;/code&gt;. It is a &lt;strong&gt;contract between layers&lt;/strong&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Layer &lt;/th&gt;&lt;th&gt;Responsibility&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Absorbs transient instability via bounded retry&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Business&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Remains fail-fast for semantic integrity&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Client&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;Retries only when signaled retryable&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;When failure is bounded and classified, the system becomes &lt;strong&gt;predictable&lt;/strong&gt;. And predictability is the foundation of operational confidence.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;resilience-checklist&quot;&gt;Resilience Checklist&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Retry Budget:&lt;/strong&gt; Is my retry window matched to the dependency's failover time (e.g., 15s for Redis)?&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Jitter:&lt;/strong&gt; Do my retries have randomized sleep to avoid the &quot;Thundering Herd&quot;?&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Error Classification:&lt;/strong&gt; Does my code distinguish between &lt;code&gt;READONLY&lt;/code&gt; (retryable) and &lt;code&gt;PERMISSION_DENIED&lt;/code&gt; (not retryable)?&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Centralization:&lt;/strong&gt; Is my retry logic in the client wrapper, not leaked across handlers?&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Observability:&lt;/strong&gt; Do I have an alert if &quot;Retry Budget Exhausted&quot; exceeds 5%?&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;key-takeaways&quot;&gt;Key Takeaways&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Fail fast — but not during transient infrastructure events.&lt;/strong&gt; A leader election is not a business error. Don't treat it like one.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retry must be bounded.&lt;/strong&gt; Time-boxed, attempt-limited, with jitter. No open-ended retry loops.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Retry must be centralized.&lt;/strong&gt; One retry boundary per dependency, at the infrastructure layer. Retry in multiple layers = retry amplification.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Failure semantics must be normalized.&lt;/strong&gt; Retryable vs non-retryable must be explicit. Watch for &lt;code&gt;READONLY&lt;/code&gt; — the most common Sentinel failover gotcha.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Resilience requires cross-layer alignment.&lt;/strong&gt; Bounded retry (inner loop) + circuit breaker (outer loop) + observability = production-grade resilience.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;frequently-asked-questions&quot;&gt;Frequently Asked Questions&lt;/h3&gt;
&lt;h4 id=&quot;should-distributed-systems-always-fail-fast-&quot;&gt;Should distributed systems always fail fast?&lt;/h4&gt;
&lt;p&gt;No. Fail fast for &lt;strong&gt;business-level errors&lt;/strong&gt; (validation, permission, domain rules), but use &lt;strong&gt;bounded retry&lt;/strong&gt; for transient infrastructure failures like leader election and temporary network instability.&lt;/p&gt;
&lt;h4 id=&quot;what-is-a-reasonable-retry-budget-for-redis-sentinel-failover-&quot;&gt;What is a reasonable retry budget for Redis Sentinel failover?&lt;/h4&gt;
&lt;p&gt;In many production setups, &lt;strong&gt;12-15 seconds&lt;/strong&gt; is a practical starting point because it usually covers Sentinel detection, promotion, and client reconnection. Calibrate with your own failover timings and SLOs.&lt;/p&gt;
&lt;h4 id=&quot;if-the-service-already-retries-should-the-client-also-retry-&quot;&gt;If the service already retries, should the client also retry?&lt;/h4&gt;
&lt;p&gt;Only when explicitly signaled retryable. Blind retries at both layers often create retry amplification and can trigger a retry storm.&lt;/p&gt;
&lt;h4 id=&quot;how-is-bounded-retry-different-from-a-circuit-breaker-&quot;&gt;How is bounded retry different from a circuit breaker?&lt;/h4&gt;
&lt;p&gt;Bounded retry handles short transient windows (inner loop). Circuit breaker handles sustained dependency failure and stops repeated expensive attempts (outer loop).&lt;/p&gt;
&lt;h4 id=&quot;why-not-use-a-service-mesh-istio-for-retries-&quot;&gt;Why not use a Service Mesh (Istio) for retries?&lt;/h4&gt;
&lt;p&gt;While Mesh can retry, the application layer has better &quot;semantic awareness.&quot; Only the app knows if a specific error is safe to retry based on idempotency.&lt;/p&gt;
&lt;h4 id=&quot;when-should-i-not-use-bounded-retry-&quot;&gt;When should I NOT use Bounded Retry?&lt;/h4&gt;
&lt;p&gt;For non-idempotent operations unless you have a robust request-ID tracking system. For business errors (400s), always fail fast.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;further-reading&quot;&gt;Further Reading&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://harrisonsec.com/blog/rust-vs-c-assembly-performance-safety-analysis/&quot;&gt;Rust vs C Assembly: Complete Performance and Safety Analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://harrisonsec.com/blog/legacy-compatibility-lab-full-stack/&quot;&gt;Legacy Compatibility Lab: My Full Stack for Reviving Dead Software&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thought-1&quot;&gt;Final Thought&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;Distributed systems are not about avoiding failure.&lt;br&gt;
They are about &lt;strong&gt;designing boundaries&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If retry is everywhere, the system becomes unpredictable.&lt;br&gt;
If retry is nowhere, transient instability leaks upward.&lt;/p&gt;
&lt;p&gt;The goal is not infinite retry.&lt;br&gt;
&lt;strong&gt;The goal is bounded retry.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That boundary is what keeps systems stable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Resilience is not a library. It is a contract between layers.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Based on a talk I gave on failure boundary design in distributed systems.&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published at &lt;a rel=&quot;nofollow&quot; href=&quot;https://harrisonsec.com/blog/fail-fast-bounded-resilience-distributed-systems/&quot;&gt;harrisonsec.com&lt;/a&gt;. Listen to the &lt;a rel=&quot;nofollow&quot; href=&quot;https://harrisonsec.com/audio/why-failing-fast-triggers-cascading-failures.m4a&quot;&gt;deep dive audio&lt;/a&gt; for a detailed walkthrough.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16782/why-your-fail-fast-strategy-is-killing-your-distributed-system-and-how-to-fix-it</guid>
<pubDate>Tue, 05 May 2026 19:11:27 +0000</pubDate>
</item>
<item>
<title>Building a Microservices Ecosystem: Stock Brokerage Simulator (My Broker B3)</title>
<link>https://coderlegion.com/16327/building-a-microservices-ecosystem-stock-brokerage-simulator-my-broker-b3</link>
<description>&lt;p&gt;Hey folks!&lt;/p&gt;
&lt;p&gt;I'm starting a series of articles to document the development of &lt;strong&gt;My Broker B3&lt;/strong&gt; — a personal project where I apply advanced software engineering concepts, distributed systems and messaging to simulate the real-world operation of a stock brokerage integrated with B3 (the Brazilian stock exchange).&lt;/p&gt;
&lt;p&gt;The goal isn't to build a production system, but rather a &lt;strong&gt;POC (Proof of Concept)&lt;/strong&gt; that validates real-time order processing, financial custody management and market data orchestration using an event-driven architecture.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-the-ecosystem-overview&quot;&gt;️ The Ecosystem Overview&lt;/h2&gt;
&lt;p&gt;The project was designed following the microservices philosophy with a hybrid stack — each service uses the technology that best fits its function:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;                    [broker-gateway-api]
                           │
          ┌────────────────┼────────────────┐
          ▼                ▼                ▼
   [broker-order-api] [broker-wallet] [broker-asset]
          │                │
     RabbitMQ           Kafka (order-events-v1)
          │
   [b3-matching-engine]
          │
       Redis ◀── [b3-market-sync-api] ◀── [brapi.dev]
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-services&quot;&gt;The Services&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Broker Side&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Service &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Language &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Responsibility&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;broker-identity-api&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Java &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Authentication, JWT, user management&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;broker-gateway-api&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Java &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;API Gateway, routing, JWT validation&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;broker-order-api&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Java &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Order lifecycle orchestration&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;broker-wallet-api&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Java &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Financial custody, balance and positions&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;broker-asset-api&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Java &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Asset catalog and internal market data&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;broker-market-data-api&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Python &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Quote ingestion from Brapi → MongoDB + Kafka&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;broker-report-api&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Java &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Reports and history&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;B3 Side (Simulator)&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Service &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Language &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Responsibility&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;b3-matching-engine-api&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Java &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Order execution, price matching&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;code&gt;b3-market-sync-api&lt;/code&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Java &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Price synchronization to Redis&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-communication-strategy&quot;&gt;⚙️ Communication Strategy&lt;/h2&gt;
&lt;p&gt;One of the most important decisions in the project was choosing &lt;strong&gt;when to use synchronous communication and when to use asynchronous&lt;/strong&gt;. The rule was simple: if the response is needed now to continue the flow, use REST. If it can wait, use messaging.&lt;/p&gt;
&lt;h3 id=&quot;synchronous-rest-feign-&quot;&gt;Synchronous (REST/Feign)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;broker-order-api&lt;/code&gt; → &lt;code&gt;broker-asset-api&lt;/code&gt;: validates ticker before creating order&lt;/li&gt;
&lt;li&gt;&lt;code&gt;broker-order-api&lt;/code&gt; → &lt;code&gt;broker-wallet-api&lt;/code&gt;: validates balance before processing buy&lt;/li&gt;
&lt;li&gt;&lt;code&gt;broker-identity-api&lt;/code&gt; → &lt;code&gt;broker-wallet-api&lt;/code&gt;: creates wallet account on registration&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;asynchronous-rabbitmq-&quot;&gt;Asynchronous (RabbitMQ)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;broker-order-api&lt;/code&gt; → &lt;code&gt;b3-matching-engine-api&lt;/code&gt;: sends orders for execution&lt;/li&gt;
&lt;li&gt;&lt;code&gt;b3-matching-engine-api&lt;/code&gt; → &lt;code&gt;broker-order-api&lt;/code&gt;: execution feedback (FILLED/REJECTED)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;asynchronous-kafka-&quot;&gt;Asynchronous (Kafka)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;broker-market-data-api&lt;/code&gt; → topic &lt;code&gt;assets-market-data-v1&lt;/code&gt; → &lt;code&gt;broker-asset-api&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;broker-order-api&lt;/code&gt; → topic &lt;code&gt;order-events-v1&lt;/code&gt; → &lt;code&gt;broker-wallet-api&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;persistence-strategy&quot;&gt;Persistence Strategy&lt;/h2&gt;
&lt;p&gt;Each service uses the database that best fits its nature:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Technology &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Service(s) &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Why&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;MySQL&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;identity, wallet, order, asset &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Transactional data with strong consistency&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;PostgreSQL&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;b3-matching-engine &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;B3 core with execution history&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;MongoDB&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;broker-market-data &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Quote history — flexible schema&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Redis&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;broker-asset, b3-matching-engine &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Real-time price cache for fast decisions&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-order-flow-end-to-end&quot;&gt;The Order Flow — End to End&lt;/h2&gt;
&lt;p&gt;To materialize the architecture, here's what happens when a user places a buy order:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;1. POST /api/v1/orders
        │
        ├─ Validate balance → broker-wallet-api (REST)
        ├─ Persist PENDING → MySQL
        ├─ Publish PENDING → Kafka (broker-wallet blocks balance)
        └─ Send to B3 → RabbitMQ

2. B3 processes, looks up price in Redis
        │
        ├─ FILLED/REJECTED → RabbitMQ → broker-order-api
        ├─ Update status → MySQL
        └─ Publish final status → Kafka (broker-wallet settles/refunds)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From the user's click to the updated wallet balance, through validation, price matching and eventual consistency.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-you-ll-find-in-the-series&quot;&gt;What You'll Find in the Series&lt;/h2&gt;
&lt;p&gt;Each post documents the construction of a service, focusing on technical decisions, problems encountered and how they were solved:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Overview&lt;/strong&gt; ← you are here&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt; — Docker Compose with 12+ containers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;broker-market-data-api&lt;/strong&gt; — Python, Brapi, MongoDB, Kafka&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;b3-market-sync-api&lt;/strong&gt; — Price synchronization to Redis&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;b3-matching-engine-api&lt;/strong&gt; — The B3 execution engine&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;broker-asset-api&lt;/strong&gt; — Asset catalog and cache&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;broker-wallet-api&lt;/strong&gt; — Financial custody&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;broker-order-api&lt;/strong&gt; — The order conductor&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;broker-identity-api&lt;/strong&gt; — Authentication and JWT&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;broker-gateway-api&lt;/strong&gt; — API Gateway&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Feel free to leave feedback or questions in the comments!&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;about-the-series&quot;&gt;About the Series&lt;/h2&gt;
&lt;p&gt; Series Index: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/rvneto/series-roadmap-building-a-stock-brokerage-simulator-with-microservices-kgh&quot;&gt;Series Roadmap&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/roberto-de-vargas/&quot;&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rvneto&quot;&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16327/building-a-microservices-ecosystem-stock-brokerage-simulator-my-broker-b3</guid>
<pubDate>Fri, 01 May 2026 13:50:40 +0000</pubDate>
</item>
<item>
<title>Master-Class: Scaling Databases with Sharding, Partitioning, and Consistent Hashing</title>
<link>https://coderlegion.com/15191/master-class-scaling-databases-with-sharding-partitioning-and-consistent-hashing</link>
<description>&lt;hr&gt;
&lt;p&gt;title: &quot;Master-Class: Scaling Databases with Sharding, Partitioning, and Consistent Hashing&quot;&lt;br&gt;
published: true&lt;br&gt;
description: &quot;Learn the three pillars of horizontal scaling: Sharding, Partitioning, and Consistent Hashing, and how they relate to replication.&quot;&lt;/p&gt;
&lt;h3 id=&quot;tags-database-systemdesign-backend-distributedsystems&quot;&gt;tags: database, systemdesign, backend, distributedsystems&lt;/h3&gt;
&lt;p&gt;Scaling a database is often the &quot;final boss&quot; of system design. While scaling your application layer is as simple as spinning up more containers, databases are stateful, making them significantly harder to distribute.&lt;/p&gt;
&lt;p&gt;When vertical scaling hits a physical limit, you have no choice but to &lt;strong&gt;scale horizontally&lt;/strong&gt;. In this guide, we break down the three pillars of horizontal scaling and how they intersect with replication strategies to build truly resilient systems.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#sharding-vs-partitioning-the-core-difference&quot;&gt;Sharding vs. Partitioning: The Core Difference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#partitioning-strategies-horizontal-vs-vertical&quot;&gt;Partitioning Strategies: Horizontal vs. Vertical&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#the-modulo-problem-why-simple-hashing-fails&quot;&gt;The &quot;Modulo&quot; Problem: Why Simple Hashing Fails&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#consistent-hashing-the-secret-to-rebalancing&quot;&gt;Consistent Hashing: The Secret to Rebalancing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#the-hybrid-model-relating-replication-to-sharding&quot;&gt;The Hybrid Model: Relating Replication to Sharding&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#the-complexity-trade-offs&quot;&gt;The Complexity Trade-offs&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;sharding-vs-partitioning-the-core-difference&quot;&gt;Sharding vs. Partitioning: The Core Difference&lt;/h3&gt;
&lt;p&gt;While often used interchangeably, they operate at different layers of the stack:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Partitioning:&lt;/strong&gt; This is a &lt;strong&gt;logical&lt;/strong&gt; split of your data. It’s about taking a giant dataset and breaking it into smaller, manageable chunks (e.g., splitting a 1TB table into ten 100GB chunks).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sharding:&lt;/strong&gt; This is a &lt;strong&gt;physical&lt;/strong&gt; split across machines. It involves distributing those partitions across different database instances (shards) to distribute the CPU, RAM, and I/O load.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;The Golden Rule:&lt;/strong&gt; You shard the &lt;em&gt;database&lt;/em&gt; and you partition the &lt;em&gt;data&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;partitioning-strategies&quot;&gt;Partitioning Strategies&lt;/h3&gt;
&lt;p&gt;How do you decide which data goes into which partition? It must be deterministic.&lt;/p&gt;
&lt;h4 id=&quot;a-horizontal-partitioning-sharding-&quot;&gt;A. Horizontal Partitioning (Sharding)&lt;/h4&gt;
&lt;p&gt;You split the table by &lt;strong&gt;rows&lt;/strong&gt;. For example, users with IDs 1–1000 go to Partition A, and 1001–2000 go to Partition B.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Distributing massive datasets across nodes for write/read throughput.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;b-vertical-partitioning&quot;&gt;B. Vertical Partitioning&lt;/h4&gt;
&lt;p&gt;You split the table by &lt;strong&gt;columns&lt;/strong&gt;. A &lt;code&gt;User&lt;/code&gt; table might be split into &lt;code&gt;User_Profile&lt;/code&gt; (Name, Email) and &lt;code&gt;User_Metadata&lt;/code&gt; (Bio, Settings, Blob data).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Reducing I/O for queries that only need specific, frequently accessed columns.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-modulo-problem-why-simple-hashing-fails&quot;&gt;The &quot;Modulo&quot; Problem: Why Simple Hashing Fails&lt;/h3&gt;
&lt;p&gt;When sharding, we need to map a key (like &lt;code&gt;user_id&lt;/code&gt;) to a specific server. The simplest way is &lt;strong&gt;Modulo Hashing&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;{% katex %}&lt;br&gt;
server_index = hash(key) \pmod n&lt;br&gt;
{% endkatex %}&lt;/p&gt;
&lt;p&gt;Where {% katex inline %} n {% endkatex %} is the number of servers. This works until you need to scale. If you have 3 servers ({% katex inline %} n=3 {% endkatex %}) and you add a 4th ({% katex inline %} n=4 {% endkatex %}), the result of the modulo operation changes for almost &lt;strong&gt;every single key&lt;/strong&gt;. This forces a massive data migration that can bring your system down.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;consistent-hashing-the-secret-to-rebalancing&quot;&gt;Consistent Hashing: The Secret to Rebalancing&lt;/h3&gt;
&lt;p&gt;Consistent Hashing solves the &quot;Modulo Problem&quot; by decoupling the keys from the number of servers using a &lt;strong&gt;Hash Ring&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id=&quot;how-it-works-&quot;&gt;How it Works:&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The Ring:&lt;/strong&gt; Both servers and data keys are hashed onto the same ring (range {% katex inline %} 0 {% endkatex %} to {% katex inline %} 2^{32}-1 {% endkatex %}).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Clockwise Assignment:&lt;/strong&gt; To find a key's server, you move clockwise on the ring until you hit the first server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minimal Migration:&lt;/strong&gt; When you add a new server, it only takes over a small portion of the ring. &lt;strong&gt;Only the keys between the new server and its counter-clockwise neighbor need to move.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-hybrid-model-relating-replication-to-sharding&quot;&gt;The Hybrid Model: Relating Replication to Sharding&lt;/h3&gt;
&lt;p&gt;Sharding handles &lt;strong&gt;capacity&lt;/strong&gt;, but Replication handles &lt;strong&gt;availability&lt;/strong&gt;. In a production system, every &quot;Shard&quot; is actually a &quot;Replication Group.&quot;&lt;/p&gt;
&lt;h4 id=&quot;a-sharded-single-leader-replication&quot;&gt;A. Sharded + Single-Leader Replication&lt;/h4&gt;
&lt;p&gt;Each shard is a replica set with one Leader and multiple Followers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Workflow:&lt;/strong&gt; Consistent hashing sends you to &lt;strong&gt;Shard 4&lt;/strong&gt;. Within Shard 4, all writes go to the Leader, and reads can be distributed to Followers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Example:&lt;/strong&gt; Vitess (MySQL) or MongoDB.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;b-sharded-multi-leader-replication&quot;&gt;B. Sharded + Multi-Leader Replication&lt;/h4&gt;
&lt;p&gt;Common in multi-region deployments. You shard by geography (e.g., EU users vs. US users), but within each shard, you have multiple leaders (e.g., London and Paris) to reduce local latency.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Example:&lt;/strong&gt; Global Spanner configurations.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;c-sharded-leaderless-replication-the-dynamo-way-&quot;&gt;C. Sharded + Leaderless Replication (The &quot;Dynamo&quot; Way)&lt;/h4&gt;
&lt;p&gt;In systems like Cassandra, sharding and replication are unified on the ring.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The Workflow:&lt;/strong&gt; A key is hashed to the ring. Instead of going to one node, it is replicated to the &lt;strong&gt;next N nodes&lt;/strong&gt; clockwise.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Quorum:&lt;/strong&gt; You use {% katex inline %} W + R &amp;gt; N {% endkatex %} to ensure consistency across those sharded replicas.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-complexity-trade-offs&quot;&gt;The Complexity Trade-offs&lt;/h3&gt;
&lt;p&gt;Sharding is powerful, but it comes with a high &quot;architectural tax&quot;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Cross-Shard Joins:&lt;/strong&gt; Joining data from Shard A and Shard B is incredibly expensive. You often have to perform the join in the application layer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hotspots:&lt;/strong&gt; If one user (e.g., a celebrity) gets 100x more traffic than others, the shard they reside on will become a bottleneck.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Operational Overhead:&lt;/strong&gt; Managing backups, monitoring, and failover for 100 shards is significantly more complex than a single instance.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;summary-comparison&quot;&gt;Summary Comparison&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Feature &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Modulo Hashing &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Consistent Hashing&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Scaling Impact&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;~100% data migration &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;~$1/n$ data migration&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Fault Tolerance&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Low &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;High&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Replication Choice&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Usually Single-Leader &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Often Leaderless / Multi-Leader&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thought-2&quot;&gt;Final Thought&lt;/h3&gt;
&lt;p&gt;Sharding is how we find the &quot;house&quot; for our data; Replication is how we ensure that house doesn't burn down. Don't shard until you hit the physical limits of your hardware, but when you do, use &lt;strong&gt;Consistent Hashing&lt;/strong&gt; to ensure your growth is sustainable.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;{% link &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/piyush6348/master-class-understanding-database-replication-single-multi-and-leaderless-hhm&quot;&gt;https://dev.to/piyush6348/master-class-understanding-database-replication-single-multi-and-leaderless-hhm&lt;/a&gt; %}&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15191/master-class-scaling-databases-with-sharding-partitioning-and-consistent-hashing</guid>
<pubDate>Sun, 19 Apr 2026 19:53:01 +0000</pubDate>
</item>
<item>
<title>Master-Class: Understanding Database Replication (Single, Multi, and Leaderless)</title>
<link>https://coderlegion.com/15190/master-class-understanding-database-replication-single-multi-and-leaderless</link>
<description>&lt;hr&gt;
&lt;p&gt;title: &quot;Master-Class: Understanding Database Replication (Single, Multi, and Leaderless)&quot;&lt;br&gt;
published: true&lt;br&gt;
description: A deep dive into Single Leader, Multi-Leader, and Leaderless replication algorithms for distributed systems.&lt;/p&gt;
&lt;h3 id=&quot;tags-systemdesign-backend-database-distributedsystems&quot;&gt;tags: systemdesign, backend, database, distributedsystems&lt;/h3&gt;
&lt;p&gt;Database Replication is the process of keeping a copy of the same data on multiple nodes. Whether you are aiming for high availability, reduced latency, or horizontal scalability, choosing the right replication algorithm is critical.&lt;/p&gt;
&lt;p&gt;In this guide, we will explore the three primary algorithms used in modern distributed systems: &lt;strong&gt;Single Leader&lt;/strong&gt;, &lt;strong&gt;Multi-Leader&lt;/strong&gt;, and &lt;strong&gt;Leaderless&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;table-of-contents-1&quot;&gt;Table of Contents&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#1-single-leader-replication&quot;&gt;Single Leader Replication&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#2-multi-leader-replication&quot;&gt;Multi-Leader Replication&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#3-leaderless-replication&quot;&gt;Leaderless Replication&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#the-replication-lag-problem&quot;&gt;The Replication Lag Problem&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;#summary-comparison&quot;&gt;Summary Comparison&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-single-leader-replication&quot;&gt;1. Single Leader Replication&lt;/h3&gt;
&lt;p&gt;This is the most common approach (used by MySQL, PostgreSQL, and MongoDB). One node is designated as the &lt;strong&gt;leader&lt;/strong&gt; (master), and all other nodes are &lt;strong&gt;followers&lt;/strong&gt; (read replicas).&lt;/p&gt;
&lt;h4 id=&quot;how-it-works&quot;&gt;How it Works&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Writes:&lt;/strong&gt; All write requests must be sent to the leader. The leader writes the data locally and sends the change to all followers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reads:&lt;/strong&gt; Clients can read from the leader or any follower.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;synchronous-vs-asynchronous&quot;&gt;Synchronous vs. Asynchronous&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Synchronous:&lt;/strong&gt; The leader waits for followers to confirm the write.&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Pros:&lt;/em&gt; Guaranteed consistency.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Cons:&lt;/em&gt; High latency; if one node fails, the whole write pipeline blocks.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Asynchronous:&lt;/strong&gt; The leader confirms the write immediately.&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Pros:&lt;/em&gt; High performance.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Cons:&lt;/em&gt; Risk of data loss if the leader fails before followers sync.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;handling-failures&quot;&gt;Handling Failures&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Follower Failure:&lt;/strong&gt; A follower &quot;catches up&quot; by using its local log to request missing data from the leader.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Leader Failure (Failover):&lt;/strong&gt; Requires detecting failure via timeouts, electing a new leader, and reconfiguring the system.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-multi-leader-replication&quot;&gt;2. Multi-Leader Replication&lt;/h3&gt;
&lt;p&gt;In this setup, more than one node can accept writes. This is typically used for applications spread across multiple geographic data centers.&lt;/p&gt;
&lt;h4 id=&quot;use-cases&quot;&gt;Use Cases&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Multi-Data Center Operation:&lt;/strong&gt; Users write to the nearest data center to reduce latency.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Offline Operation:&lt;/strong&gt; Apps like calendars or note-taking tools act as local &quot;leaders&quot; that sync with a server later.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;the-challenge-conflict-resolution&quot;&gt;The Challenge: Conflict Resolution&lt;/h4&gt;
&lt;p&gt;If two users edit the same data in different data centers simultaneously, a conflict occurs.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Conflict Avoidance:&lt;/strong&gt; Routing all writes for a specific record to the same leader.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Convergence:&lt;/strong&gt; Using &lt;strong&gt;Last Write Wins (LWW)&lt;/strong&gt; or &lt;strong&gt;Conflict-free Replicated Data Types (CRDTs)&lt;/strong&gt; to merge changes.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-leaderless-replication&quot;&gt;3. Leaderless Replication&lt;/h3&gt;
&lt;p&gt;Popularized by Amazon’s Dynamo, this approach allows &lt;strong&gt;any node&lt;/strong&gt; to accept writes and reads. Systems like Cassandra and Riak use this model.&lt;/p&gt;
&lt;h4 id=&quot;quorums-n-w-r-&quot;&gt;Quorums ($n, w, r$)&lt;/h4&gt;
&lt;p&gt;To maintain consistency without a leader, these systems use quorums:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$n$: Total number of replicas.&lt;/li&gt;
&lt;li&gt;$w$: Nodes that must confirm a write.&lt;/li&gt;
&lt;li&gt;$r$: Nodes that must be queried for a read.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Rule:&lt;/strong&gt; For a successful read of the latest data, $w + r &amp;gt; n$.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;fixing-stale-data&quot;&gt;Fixing Stale Data&lt;/h4&gt;
&lt;p&gt;Since nodes can go down, systems fix stale data via:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Read Repair:&lt;/strong&gt; When a client detects an old version during a read, it pushes the newer value back to that node.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Anti-Entropy:&lt;/strong&gt; A background process that constantly syncs data between replicas.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-replication-lag-problem&quot;&gt;The Replication Lag Problem&lt;/h3&gt;
&lt;p&gt;Regardless of the algorithm, asynchronous replication often results in &quot;replication lag.&quot; To maintain a good user experience, developers should implement:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Read-Your-Own-Writes:&lt;/strong&gt; Ensures a user always sees the updates they just made.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monotonic Reads:&lt;/strong&gt; Ensures a user doesn't see data &quot;disappear&quot; when querying different replicas.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consistent Prefix Reads:&lt;/strong&gt; Guarantees that if writes happen in a specific order, they are read in that same order.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;summary-comparison-1&quot;&gt;Summary Comparison&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Algorithm &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Best For &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Main Downside&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Single Leader&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Read-heavy apps, general simplicity &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Leader is a single point of failure for writes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Multi-Leader&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Multi-region apps, offline capabilities &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Extremely complex conflict resolution&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Leaderless&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;High write throughput, high availability &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Complexities in eventual consistency&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15190/master-class-understanding-database-replication-single-multi-and-leaderless</guid>
<pubDate>Sun, 19 Apr 2026 19:52:19 +0000</pubDate>
</item>
<item>
<title>Transaction Orchestration in Distributed Financial Systems: Coordination, Idempotency, and Eventual Consistency</title>
<link>https://coderlegion.com/14993/transaction-orchestration-distributed-financial-coordination-idempotency-consistency</link>
<description>&lt;h2 id=&quot;abstract&quot;&gt;Abstract&lt;/h2&gt;
&lt;p&gt;Distributed financial systems are composed of multiple subsystems, each responsible for enforcing a specific invariant. Ledger systems preserve correctness, custody systems enforce authority, compliance systems constrain allowed behavior, and smart contracts provide deterministic settlement.&lt;/p&gt;
&lt;p&gt;However, real systems must coordinate these components under conditions of latency, partial failure, and inconsistent state visibility. This coordination problem is often underestimated and is responsible for many of the most subtle and dangerous production failures.&lt;/p&gt;
&lt;p&gt;This article explores transaction orchestration in distributed financial systems, focusing on coordination strategies, idempotency guarantees, failure handling, and the realities of eventual consistency.&lt;/p&gt;
&lt;p&gt;Correct components do not guarantee correct execution.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-illusion-of-a-single-transaction&quot;&gt;The illusion of a single transaction&lt;/h2&gt;
&lt;p&gt;When designing systems, it is tempting to think in terms of a single operation.&lt;/p&gt;
&lt;p&gt;A withdrawal.&lt;br&gt;
A transfer.&lt;br&gt;
A settlement.&lt;/p&gt;
&lt;p&gt;In reality, what appears as a single transaction is a sequence of distributed operations across multiple services.&lt;/p&gt;
&lt;p&gt;A typical flow may involve:&lt;/p&gt;
&lt;p&gt;ledger validation&lt;br&gt;
compliance evaluation&lt;br&gt;
risk checks&lt;br&gt;
custody signing&lt;br&gt;
settlement broadcast&lt;/p&gt;
&lt;p&gt;Each step runs in a different context. Each step may fail independently.&lt;/p&gt;
&lt;p&gt;The system does not execute one transaction.&lt;/p&gt;
&lt;p&gt;It orchestrates a sequence of state transitions.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;coordination-under-uncertainty&quot;&gt;Coordination under uncertainty&lt;/h2&gt;
&lt;p&gt;Distributed systems do not operate under perfect conditions.&lt;/p&gt;
&lt;p&gt;Messages may arrive late.&lt;br&gt;
Services may retry operations.&lt;br&gt;
Nodes may crash mid execution.&lt;/p&gt;
&lt;p&gt;Two services may have different views of the same transaction at the same time.&lt;/p&gt;
&lt;p&gt;This creates a fundamental challenge.&lt;/p&gt;
&lt;p&gt;There is no global clock.&lt;br&gt;
There is no perfectly synchronized state.&lt;/p&gt;
&lt;p&gt;And yet, the system must behave as if there were.&lt;/p&gt;
&lt;p&gt;Coordination is the mechanism that creates this illusion.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;idempotency-as-a-safety-guarantee&quot;&gt;Idempotency as a safety guarantee&lt;/h2&gt;
&lt;p&gt;In financial systems, retries are inevitable.&lt;/p&gt;
&lt;p&gt;If a request times out, it will be retried.&lt;br&gt;
If a service crashes, operations may be replayed.&lt;/p&gt;
&lt;p&gt;Without protection, this leads to duplication.&lt;/p&gt;
&lt;p&gt;A withdrawal could be executed twice.&lt;br&gt;
A settlement could be broadcast multiple times.&lt;/p&gt;
&lt;p&gt;This is unacceptable.&lt;/p&gt;
&lt;p&gt;Idempotency ensures that applying the same operation multiple times produces the same result.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;text id=l7wq2r
apply(operation, state) multiple times
=&amp;gt; same final state
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This property must exist across service boundaries, not just within a single component.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;eventual-consistency-and-controlled-divergence&quot;&gt;Eventual consistency and controlled divergence&lt;/h2&gt;
&lt;p&gt;Strong consistency across all components is rarely achievable in distributed systems.&lt;/p&gt;
&lt;p&gt;Instead, systems operate under eventual consistency.&lt;/p&gt;
&lt;p&gt;Different services may temporarily disagree on state.&lt;/p&gt;
&lt;p&gt;The critical requirement is not immediate agreement.&lt;/p&gt;
&lt;p&gt;It is bounded and controlled convergence.&lt;/p&gt;
&lt;p&gt;The system must guarantee that all components eventually reach a consistent view of the transaction outcome.&lt;/p&gt;
&lt;p&gt;Unbounded divergence leads to reconciliation problems and operational uncertainty.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;orchestration-models&quot;&gt;Orchestration models&lt;/h2&gt;
&lt;p&gt;There are multiple ways to coordinate distributed transactions.&lt;/p&gt;
&lt;p&gt;Centralized orchestration relies on a coordinator service that drives execution.&lt;/p&gt;
&lt;p&gt;Choreography relies on event driven interaction between services.&lt;/p&gt;
&lt;p&gt;Each model has tradeoffs.&lt;/p&gt;
&lt;p&gt;Centralized orchestration simplifies reasoning but introduces a control dependency.&lt;br&gt;
Choreography increases decoupling but makes reasoning about global state more complex.&lt;/p&gt;
&lt;p&gt;Financial systems often use hybrid approaches, combining explicit coordination with event driven propagation.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;failure-handling-and-partial-execution&quot;&gt;Failure handling and partial execution&lt;/h2&gt;
&lt;p&gt;Failures rarely occur at clean boundaries.&lt;/p&gt;
&lt;p&gt;A transaction may pass compliance and fail during custody signing.&lt;br&gt;
A signature may be produced but not broadcast.&lt;br&gt;
A broadcast may succeed but not be recorded internally.&lt;/p&gt;
&lt;p&gt;The system must handle these partial states.&lt;/p&gt;
&lt;p&gt;This requires:&lt;/p&gt;
&lt;p&gt;clear state modeling&lt;br&gt;
explicit transition tracking&lt;br&gt;
safe retry mechanisms&lt;br&gt;
reconciliation processes&lt;/p&gt;
&lt;p&gt;Failure handling is not an edge case. It is the dominant execution path.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-danger-of-implicit-sequencing&quot;&gt;The danger of implicit sequencing&lt;/h2&gt;
&lt;p&gt;One of the most common sources of bugs is implicit sequencing.&lt;/p&gt;
&lt;p&gt;Assuming that because step A happened before step B in code, it also happened before in the system.&lt;/p&gt;
&lt;p&gt;In distributed environments, this assumption does not hold.&lt;/p&gt;
&lt;p&gt;Messages can be reordered.&lt;br&gt;
Events can be delayed.&lt;/p&gt;
&lt;p&gt;Sequencing must be explicit.&lt;/p&gt;
&lt;p&gt;Each step must validate that its preconditions are still valid at execution time.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;orchestration-defines-system-behavior&quot;&gt;Orchestration defines system behavior&lt;/h2&gt;
&lt;p&gt;Ledger enforces correctness.&lt;br&gt;
Custody enforces authority.&lt;br&gt;
Compliance enforces constraints.&lt;/p&gt;
&lt;p&gt;But orchestration defines how the system behaves under real conditions.&lt;/p&gt;
&lt;p&gt;It determines:&lt;/p&gt;
&lt;p&gt;how failures propagate&lt;br&gt;
how retries are handled&lt;br&gt;
how state converges&lt;br&gt;
how inconsistencies are resolved&lt;/p&gt;
&lt;p&gt;This is where most production issues originate.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;conclusion-2&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Distributed financial systems do not execute transactions in a single step. They orchestrate sequences of operations across multiple services, each with its own state and failure modes.&lt;/p&gt;
&lt;p&gt;Correctness at the component level is necessary but insufficient. Systems must coordinate execution under uncertainty, ensuring that retries, delays, and partial failures do not violate global invariants.&lt;/p&gt;
&lt;p&gt;Transaction orchestration is the layer that transforms correct components into a functioning system.&lt;/p&gt;
&lt;p&gt;Without it, correctness remains theoretical.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14993/transaction-orchestration-distributed-financial-coordination-idempotency-consistency</guid>
<pubDate>Thu, 16 Apr 2026 15:38:23 +0000</pubDate>
</item>
<item>
<title>Financial Systems as Composed State Machines: Correctness, Authority, and System Integrity</title>
<link>https://coderlegion.com/14992/financial-systems-composed-state-machines-correctness-authority-and-system-integrity</link>
<description>&lt;h2 id=&quot;abstract-1&quot;&gt;Abstract&lt;/h2&gt;
&lt;p&gt;Modern financial systems are often described in terms of individual components. Ledgers enforce correctness. Custody systems control asset authority. Compliance systems constrain behavior. Orchestration layers coordinate execution.&lt;/p&gt;
&lt;p&gt;Each of these components can be designed correctly in isolation.&lt;/p&gt;
&lt;p&gt;Yet production failures continue to occur in systems that are individually sound.&lt;/p&gt;
&lt;p&gt;This article argues that financial infrastructure must be understood not as a collection of services, but as a composition of interacting state machines. We examine how correctness, authority, and policy enforcement interact across subsystem boundaries, and why system integrity emerges only when these components are composed under strict constraints.&lt;/p&gt;
&lt;p&gt;Financial systems do not fail because components are incorrect. They fail because composition is undisciplined.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-illusion-of-correct-systems&quot;&gt;The illusion of correct systems&lt;/h2&gt;
&lt;p&gt;It is possible to build a ledger that enforces conservation of value.&lt;/p&gt;
&lt;p&gt;It is possible to design custody systems that eliminate single key compromise through threshold cryptography.&lt;/p&gt;
&lt;p&gt;It is possible to implement compliance engines that correctly evaluate regulatory constraints.&lt;/p&gt;
&lt;p&gt;It is possible to orchestrate transactions across distributed services.&lt;/p&gt;
&lt;p&gt;Each of these can be done correctly.&lt;/p&gt;
&lt;p&gt;And yet, the system can still fail.&lt;/p&gt;
&lt;p&gt;This is the point where most engineering intuition breaks.&lt;/p&gt;
&lt;p&gt;Correctness at the component level does not imply correctness at the system level.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;from-services-to-state-machines&quot;&gt;From services to state machines&lt;/h2&gt;
&lt;p&gt;The shift in perspective is subtle but critical.&lt;/p&gt;
&lt;p&gt;Financial systems are not services exchanging messages.&lt;/p&gt;
&lt;p&gt;They are state machines interacting through transitions.&lt;/p&gt;
&lt;p&gt;Each subsystem can be modeled as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S_i = (State_i, Transition_i)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;State_i represents the internal state of subsystem i&lt;/li&gt;
&lt;li&gt;Transition_i represents the allowed state transitions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;p&gt;Ledger enforces:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;sum(entries(T)) = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Custody enforces:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;signature(T) requires threshold participation
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Compliance enforces:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;policy(T, state) = allowed or rejected
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Orchestration enforces:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;execution(T) follows valid transition sequence
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Individually, these systems define local invariants.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;composition-defines-global-behavior&quot;&gt;Composition defines global behavior&lt;/h2&gt;
&lt;p&gt;The full financial system can be modeled as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;S = Compose(S_ledger, S_custody, S_compliance, S_orchestration)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The critical property is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Global correctness != sum of local correctness
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Global correctness = correctness of composition
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is where failures emerge.&lt;/p&gt;
&lt;p&gt;If transitions across subsystems are not properly constrained, global invariants can be violated even when each subsystem behaves correctly.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;boundary-violations-and-implicit-coupling&quot;&gt;Boundary violations and implicit coupling&lt;/h2&gt;
&lt;p&gt;Most real world failures originate at boundaries.&lt;/p&gt;
&lt;p&gt;A custody system signs a transaction that was valid at evaluation time but invalid at execution time.&lt;/p&gt;
&lt;p&gt;A compliance check passes based on stale state.&lt;/p&gt;
&lt;p&gt;An orchestration layer retries an operation without ensuring idempotency across services.&lt;/p&gt;
&lt;p&gt;A settlement completes externally but is not reflected internally.&lt;/p&gt;
&lt;p&gt;Each subsystem behaves according to its own rules.&lt;/p&gt;
&lt;p&gt;The failure occurs in how they interact.&lt;/p&gt;
&lt;p&gt;This is not a bug in a component.&lt;/p&gt;
&lt;p&gt;It is a failure of composition.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;temporal-inconsistency-and-state-divergence&quot;&gt;Temporal inconsistency and state divergence&lt;/h2&gt;
&lt;p&gt;Distributed systems introduce temporal uncertainty.&lt;/p&gt;
&lt;p&gt;Different subsystems observe state transitions at different times.&lt;/p&gt;
&lt;p&gt;This creates divergence.&lt;/p&gt;
&lt;p&gt;A transaction may be:&lt;/p&gt;
&lt;p&gt;valid in the ledger&lt;br&gt;
approved by compliance&lt;br&gt;
signed by custody&lt;/p&gt;
&lt;p&gt;But still inconsistent globally because these decisions were made against different versions of state.&lt;/p&gt;
&lt;p&gt;Without explicit coordination, the system operates on partially ordered events.&lt;/p&gt;
&lt;p&gt;Correctness requires more than local validation.&lt;/p&gt;
&lt;p&gt;It requires agreement on the context in which validation occurs.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-role-of-invariants-across-boundaries&quot;&gt;The role of invariants across boundaries&lt;/h2&gt;
&lt;p&gt;Local invariants are necessary but insufficient.&lt;/p&gt;
&lt;p&gt;Systems must enforce cross boundary invariants.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;p&gt;A transaction must not be signed if it violates compliance constraints at execution time.&lt;/p&gt;
&lt;p&gt;A transaction must not be executed if its ledger state has changed since validation.&lt;/p&gt;
&lt;p&gt;A transaction must not be retried if it has already been committed externally.&lt;/p&gt;
&lt;p&gt;These invariants do not belong to a single subsystem.&lt;/p&gt;
&lt;p&gt;They exist across the composition.&lt;/p&gt;
&lt;p&gt;This is where most systems are weakest.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;failure-is-systemic-not-local&quot;&gt;Failure is systemic, not local&lt;/h2&gt;
&lt;p&gt;Failures in financial systems are rarely isolated.&lt;/p&gt;
&lt;p&gt;A retry in one service propagates to another.&lt;/p&gt;
&lt;p&gt;A delayed message triggers inconsistent decisions.&lt;/p&gt;
&lt;p&gt;A partial execution leads to duplicated operations.&lt;/p&gt;
&lt;p&gt;The system fails as a whole.&lt;/p&gt;
&lt;p&gt;This is why focusing only on component correctness is insufficient.&lt;/p&gt;
&lt;p&gt;Engineers must reason about failure propagation across the entire system.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;compositional-integrity&quot;&gt;Compositional integrity&lt;/h2&gt;
&lt;p&gt;A system has compositional integrity when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;state boundaries are explicit&lt;/li&gt;
&lt;li&gt;transitions are constrained across subsystems&lt;/li&gt;
&lt;li&gt;sequencing is enforced&lt;/li&gt;
&lt;li&gt;invariants are preserved globally&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This requires discipline.&lt;/p&gt;
&lt;p&gt;Not just in code, but in architecture.&lt;/p&gt;
&lt;p&gt;Interfaces must encode constraints.&lt;/p&gt;
&lt;p&gt;Events must carry sufficient context.&lt;/p&gt;
&lt;p&gt;Operations must be idempotent across boundaries.&lt;/p&gt;
&lt;p&gt;State must be versioned or validated at execution time.&lt;/p&gt;
&lt;p&gt;Without these properties, composition becomes fragile.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;observability-as-proof-of-composition&quot;&gt;Observability as proof of composition&lt;/h2&gt;
&lt;p&gt;One way to evaluate compositional integrity is through observability.&lt;/p&gt;
&lt;p&gt;If a system cannot reconstruct the full lifecycle of a transaction across all subsystems, then its composition is not well defined.&lt;/p&gt;
&lt;p&gt;Observability provides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;traceability across boundaries&lt;/li&gt;
&lt;li&gt;visibility into sequencing&lt;/li&gt;
&lt;li&gt;evidence of invariant preservation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It acts as a verification layer for system behavior.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-final-model&quot;&gt;The final model&lt;/h2&gt;
&lt;p&gt;We can describe a financial system as:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Global State S

Transitions:
  LedgerTransition
  CustodyTransition
  ComplianceTransition
  OrchestrationTransition

System integrity requires:

For all transitions T:
  invariants(S, T) are preserved across composition
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is not enforced by any single component.&lt;/p&gt;
&lt;p&gt;It emerges from the architecture.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;conclusion-3&quot;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Financial systems are often engineered as collections of services, each responsible for a specific concern. This perspective is incomplete.&lt;/p&gt;
&lt;p&gt;These systems are composed state machines whose correctness depends on the interaction of their components under real world conditions.&lt;/p&gt;
&lt;p&gt;Ledger ensures correctness of value.&lt;br&gt;
Custody ensures control of authority.&lt;br&gt;
Compliance ensures validity of behavior.&lt;br&gt;
Orchestration ensures coordination of execution.&lt;/p&gt;
&lt;p&gt;But system integrity exists only when these are composed under strict constraints.&lt;/p&gt;
&lt;p&gt;Without disciplined composition, even correct components produce incorrect systems.&lt;/p&gt;
&lt;p&gt;Financial infrastructure is not defined by its parts.&lt;/p&gt;
&lt;p&gt;It is defined by how those parts behave together.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14992/financial-systems-composed-state-machines-correctness-authority-and-system-integrity</guid>
<pubDate>Thu, 16 Apr 2026 15:37:45 +0000</pubDate>
</item>
<item>
<title>Inside Airbnb’s Mussel v2: Rebuilding a Petabyte-Scale Key-Value Store</title>
<link>https://coderlegion.com/14753/inside-airbnbs-mussel-v2-rebuilding-a-petabyte-scale-key-value-store</link>
<description>&lt;p&gt;Airbnb recently published a detailed write-up on Mussel v2, the next generation of their internal key-value store for serving derived data. The system sits between offline data processing and online applications, turning large batch outputs and streaming updates into low-latency lookups used by systems like fraud detection, recommendations, and pricing.&lt;/p&gt;
&lt;p&gt;The new architecture is a substantial redesign. It introduces a stateless dispatch layer, range-based sharding, stronger consistency controls, and a new storage backend designed to handle both large bulk loads and high-volume streaming updates in the same cluster. Migrating to it required moving more than a petabyte of data across thousands of tables, all while keeping production traffic online.&lt;/p&gt;
&lt;p&gt;This article walks through both the original Mussel design and the new architecture, focusing on the engineering decisions behind the redesign.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-mussel-is---and-why-it-s-a-hard-problem&quot;&gt;What Mussel is - And Why it's a Hard Problem&lt;/h3&gt;
&lt;p&gt;Airbnb's fraud detection, recommendation engine, pricing models, and personalization systems all share a common need: take data computed offline - in warehouses and batch pipelines - and make it readable online at millisecond latency.&lt;/p&gt;
&lt;p&gt;This is what engineers call the derived data problem. You can't query a data warehouse in real time. You can't fit 100TB into Redis. You need a system in the middle: one that can absorb large batch loads from offline compute, accept real-time streaming updates, and serve all of it under low-latency read paths.&lt;/p&gt;
&lt;p&gt;Most teams end up with three separate systems for this: an object store for batch ingestion, a cache for reads, and a message queue for streaming writes. Each is optimized for its job, but keeping them in sync adds complexity and operational overhead.&lt;/p&gt;
&lt;p&gt;By the time v1 was showing its limits, Mussel wasn't a small system. It was already serving over 4,000 client tables, managing roughly 130TB of data, handling 800k+ read QPS and 35k write QPS, with p95 read latency under 8ms. This was a mature, production-critical system - not a prototype being replaced by something grown-up.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-was-breaking-in-v1&quot;&gt;What Was Breaking in v1&lt;/h3&gt;
&lt;p&gt;V1's problems weren't dramatic. They were the kind that build up slowly until they become a ceiling.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Operations were too manual.&lt;/strong&gt; v1's storage layer was built on HRegion - the region server component from Apache HBase, powered by LSM trees, running locally on each Mussel storage node. HBase is capable, but it carries significant operational weight. Scaling v1 meant running multi-step Chef scripts on EC2 instances and carefully coordinating node additions in a system that wasn't designed for elastic scaling. None of this was automated. Every scaling event was a manual process that required institutional knowledge and took hours.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hash partitioning created hotspots.&lt;/strong&gt; v1 split data across 1024 logical shards by hashing primary keys. Apache Helix managed the mapping of those shards to physical nodes, and a leaderless replication model meant any replica of a shard could serve reads - which was good for read throughput. But hash partitioning gives you no control over where data actually lands. When access patterns aren't uniform - and they never are in production - certain nodes end up overloaded. Latency spikes follow. And fixing it requires rebalancing, which is more manual work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Resource usage was invisible.&lt;/strong&gt; v1 had no per-namespace quotas, no cost dashboards, no way for teams to see what their tables were consuming. At scale that becomes a reliability problem, not just a billing one. One team's runaway bulk load job can silently degrade the read latency of another team's fraud detection system.&lt;/p&gt;
&lt;p&gt;These three problems - manual operations, uncontrollable hotspots, and opaque resource usage - aren't unique to Airbnb.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-v2-changed-and-why&quot;&gt;What v2 Changed and Why&lt;/h3&gt;
&lt;p&gt;Here's how the full v2 system is laid out:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;┌──────────────────────────────────────────────────────┐
│                       Clients                        │
└──────────────────────────┬───────────────────────────┘
                           │
                           ▼
┌──────────────────────────────────────────────────────┐
│                Dispatcher (stateless)                │
│       routing · rate limiting · throttling           │
│   dual-write · shadow-read · per-table migration     │
└────────┬─────────────────┬──────────────────┬────────┘
         │                 │                  │
         ▼                 ▼                  ▼
  ┌─────────────┐   ┌─────────────┐   ┌───────────────────────┐
  │    Reads    │   │  Write path │   │      Bulk Load        │
  └──────┬──────┘   └──────┬──────┘   │  Airflow → S3         │
         │                 │          │  → StatefulSet Workers│
         │                 ▼          └──────────┬────────────┘
         │          ┌─────────────┐              │
         │          │    Kafka    │              │
         │          └──────┬──────┘              │
         │                 │                     │
         │                 ▼                     │
         │      ┌─────────────────────┐          │
         │      │  Replayer + Write   │          │
         │      │  Dispatcher         │          │
         │      └──────────┬──────────┘          │
         │                 │                     │
         └────────┬─────────┘                    │
                  │          ┌───────────────────┘
                  ▼          ▼
       ┌──────────────────────────────────────────┐
       │              NewSQL Backend              │
       │     range sharding · replicas · TTL      │
       └──────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;a-stateless-dispatcher-as-the-central-gateway&quot;&gt;A Stateless Dispatcher as the Central Gateway&lt;/h3&gt;
&lt;p&gt;The most consequential architectural decision in v2 is the Dispatcher - a stateless, horizontally scalable Kubernetes service that sits between all clients and the storage backend. Every read and write goes through it.&lt;/p&gt;
&lt;p&gt;Making the gateway stateless means you can scale it independently of the storage layer. Features like dual-write, shadow reads, rate limiting, and throttling all live in one place rather than being scattered across clients. And crucially, the team can control exactly where reads are served from - per table, dynamically - without changing a single line of code in any application that uses Mussel. That last capability turned out to be central to how the migration worked.&lt;/p&gt;
&lt;h3 id=&quot;kafka-s-role---old-and-new&quot;&gt;Kafka's Role - Old and New&lt;/h3&gt;
&lt;p&gt;One thing the v2 article doesn't make explicit: Kafka was already in v1. It wasn't a new addition. In v1, each storage node consumed from the Kafka partitions corresponding to its shards. This gave the system consistent write ordering across leaderless replicas without needing a leader node - nodes handling the same shard consume from the same partitions in the same order, so they all converge to the same state.&lt;/p&gt;
&lt;p&gt;What changed in v2 is what Kafka is used for. In v1, it was primarily a replication mechanism between storage nodes. In v2, it's a durability and decoupling layer between the Dispatcher and the NewSQL backend. Writes land in Kafka first for durability, then a Replayer and Write Dispatcher apply them to the backend in order. This decoupling means the system can absorb write bursts without putting direct pressure on storage, and keeps writes safe even if the backend temporarily slows down. The tradeoff is that routing writes through Kafka adds some latency before they're visible.&lt;/p&gt;
&lt;h3 id=&quot;range-based-sharding&quot;&gt;Range-Based Sharding&lt;/h3&gt;
&lt;p&gt;Moving from hash-based to range-based partitioning is the foundational storage change in v2. With range sharding, each shard owns a known range of keys, which means tables can be pre-split before loading data into them. This distributes write pressure evenly across nodes and prevents hotspots during bulk ingestion - something hash partitioning gave you no control over. The tradeoff is that poorly chosen shard boundaries can still create hotspots. Airbnb's solution is to sample the source data before creating a table in v2, understand the key distribution, and pre-split accordingly. More work upfront, but it results in predictable, controllable data distribution.&lt;/p&gt;
&lt;h3 id=&quot;ttl-without-relying-on-compaction&quot;&gt;TTL Without Relying on Compaction&lt;/h3&gt;
&lt;p&gt;Data expiration in v1 was handled by HRegion's compaction cycle. When HRegion's in-memory store fills up, it flushes to disk. Compaction periodically merges those files and cleans up expired records in the process. It works at moderate scale, but at petabyte scale the compaction process can't keep up - data accumulates faster than it gets cleaned.&lt;/p&gt;
&lt;p&gt;v2 replaces this with a dedicated expiration service. It splits data namespaces into range-based subtasks and processes them concurrently across multiple workers, scanning and deleting expired records in parallel. Jobs are scheduled to avoid peak read traffic, and write-heavy tables get max-version enforcement with targeted deletes to prevent unbounded growth. The result is the same retention behavior as v1, but implemented as a purpose-built service rather than a side effect of storage compaction.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-migration-moving-a-petabyte-without-downtime&quot;&gt;The Migration: Moving a Petabyte Without Downtime&lt;/h3&gt;
&lt;p&gt;Migrating data across storage engines while keeping production traffic running is where the interesting engineering happens. Airbnb's approach was methodical.&lt;/p&gt;
&lt;h3 id=&quot;the-bootstrapping-challenge&quot;&gt;The Bootstrapping Challenge&lt;/h3&gt;
&lt;p&gt;The first obstacle: v1 didn't have table-level snapshots or Change Data Capture (CDC) streams. CDC is typically how you seed a new database during a migration - stream every change from the old system into the new one until they're in sync. Without it, Airbnb had to build a custom bootstrap pipeline for each table.&lt;/p&gt;
&lt;p&gt;The process for each table: download v1 backup data, sample it to understand key distribution, create a pre-split v2 table based on that distribution, load the backup data into v2, verify via checksum, then replay any Kafka messages that accumulated during the load to catch up on writes that happened while bootstrapping was running.&lt;/p&gt;
&lt;p&gt;For large tables, the bootstrap phase alone took hours or days. To handle interruptions gracefully, the team used Kubernetes StatefulSets - which provide persistent local state across pod restarts - to checkpoint progress periodically. If a job died partway through, it could resume from the last checkpoint rather than starting over.&lt;/p&gt;
&lt;p&gt;The tradeoff is operational complexity - StatefulSets are harder to manage than stateless jobs. Each worker maintains local state that must be monitored, and failed pods need careful handling to avoid replaying already-completed work.&lt;/p&gt;
&lt;h3 id=&quot;four-stages-all-reversible&quot;&gt;Four Stages, All Reversible&lt;/h3&gt;
&lt;p&gt;The migration moved through four stages:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Blue&lt;/strong&gt; - all traffic on v1. The stable baseline while bootstrapping happens in the background.&lt;br&gt;
&lt;strong&gt;Shadow&lt;/strong&gt; - v1 continues serving traffic, but v2 processes reads and writes in parallel, silently. Responses from v2 are checked for correctness but never returned to callers. This lets the team verify v2's behavior without any risk to production.&lt;br&gt;
&lt;strong&gt;Reverse&lt;/strong&gt; - v2 takes over live traffic, with v1 kept on standby. Automatic circuit breakers monitor v2's error rate and replication lag; if either crosses a threshold, traffic snaps back to v1 instantly.&lt;br&gt;
&lt;strong&gt;Cutover&lt;/strong&gt; - v1 is decommissioned for that table.&lt;/p&gt;
&lt;p&gt;Every stage could be reversed. And migration happened one table at a time, so each table could be at a different stage simultaneously. Lower-risk tables went first, building operational confidence before the team tackled high-criticality ones.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;
                         ┌──────────────┐
                         │   Clients    │
                         └──────┬───────┘
                                │
                                ▼
                     ┌────────────────────┐
                     │     Dispatcher     │
                     │--------------------│
                     │ • read routing     │
                     │ • dual-write ctrl  │
                     │ • stage config     │
                     └─────────┬──────────┘
                               │ writes
                               ▼
                        ┌───────────────┐
                        │     Kafka     │
                        │───────────────│
                        │ replayable    │
                        │ write log     │
                        └───────┬───────┘
                                │
                    ┌───────────┴───────────┐
                    │                       │
                    ▼                       ▼
            ┌───────────────┐       ┌───────────────┐
            │   Mussel v1   │       │   Mussel v2   │
            │  (HRegion)    │       │  (NewSQL)     │
            └───────────────┘       └───────────────┘
                    ▲                       ▲
                    └───────────┬───────────┘
                                │
                         read routing
                       (Dispatcher above
                        controls which
                        system responds)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;* Each table is bootstrapped into v2 before entering Shadow stage.
┌──────────────────────────────────────────────────────────────────┐
│ Stage    │ Write Path              │ Read Path                      
├──────────┼─────────────────────────┼─────────────────────────────┤
│ BLUE     │ Kafka → v1              │ v1 serves                      
│          │                         │ v2 bootstrapping in backgroud 
├──────────┼─────────────────────────┼─────────────────────────────┤
│ SHADOW   │ Kafka → v1 + v2         │ v1 serves                      
│          │ (dual-write)            │ v2 responses checked,          
│          │                         │ not returned to callers        
├──────────┼─────────────────────────┼─────────────────────────────┤
│ REVERSE  │ Kafka → v1 + v2         │ v2 serves                      
│          │                         │ v1 on standby,                 
│          │                         │ circuit breaker active         
├──────────┼─────────────────────────┼─────────────────────────────┤
│ CUTOVER  │ Kafka → v2 only         │ v2 serves                      
│          │                         │ v1 decommissioned              
└──────────────────────────────────────────────────────────────────┘

&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;read-traffic-migration---the-underappreciated-half&quot;&gt;Read Traffic Migration - The Underappreciated Half&lt;/h3&gt;
&lt;p&gt;Most migration stories focus on moving the data. Moving read traffic is a separate problem, and Airbnb's approach here is worth understanding on its own.&lt;/p&gt;
&lt;p&gt;Once dual-write was established and data consistency confirmed, the Dispatcher handled read traffic migration in three phases. First, reads were served from v1 while shadow requests were simultaneously sent to v2 - responses compared but v2 never returned results to callers. Second, the roles flipped: v2 served reads while reverse shadow requests went to v1, providing a safety net to catch any instability. Third, full cutover with v1 decommissioned per table.&lt;/p&gt;
&lt;p&gt;What makes read traffic migration more manageable than write migration is that reads are ephemeral. If a read returns a wrong result during shadowing, there's no lasting consequence - you catch it, investigate, and fix it. Writes are durable; if something goes wrong in a write path, you need a recovery story. Airbnb's pipeline handled these separately for this reason.&lt;/p&gt;
&lt;p&gt;Running shadow reads effectively doubles the read load on both systems during the shadow and reverse phases. At an scale - where Mussel v1 already handles over 800k read QPS - this means the infrastructure must temporarily sustain roughly twice the total read traffic across both systems. That additional load is a non-trivial operational cost that must be accounted for during migration planning.&lt;/p&gt;
&lt;p&gt;The Dispatcher made this fine-grained control possible. Because all read routing decisions lived in one stateless service, the team could change where any table's reads were served from - in real time, per table - without touching the applications consuming Mussel. Without a centralized routing layer, this would have required coordinating changes across every service using Mussel, which is a much harder problem across hundreds of tables.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;lessons-worth-taking-away&quot;&gt;Lessons Worth Taking Away&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Consistency flexibility has a cost.&lt;/strong&gt; v1 offered only eventual consistency. v2 introduced the option for stronger consistency guarantees per namespace. Airbnb had to build write deduplication, hotkey blocking, and lazy write repair to handle the edge cases that surfaced. If you're planning a similar migration, the storage layer change is usually the easier part. The surprises tend to show up in application behavior.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Presplitting isn't optional with range sharding.&lt;/strong&gt; Migrating to a range-based backend without understanding your key distribution first will produce hotspots that look identical to the hash-partitioning hotspots you were trying to escape. Sampling the source data and pre-splitting tables accordingly isn't overhead - it's what makes the migration safe.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Architecture decisions today shape migration options tomorrow.&lt;/strong&gt; Because Kafka was already the write path in v1, Airbnb could leverage the same stream for dual-write and catch-up replay during migration. This wasn't planned specifically for the migration - it was a property of v1's design that turned out to be the right foundation for v2. The decisions you make today directly affect how much flexibility you'll have when you need to change the system later.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-mussel-v2-actually-delivers&quot;&gt;What Mussel v2 Actually Delivers&lt;/h3&gt;
&lt;p&gt;At the end of their article, Airbnb makes a specific claim: Mussel v2 can simultaneously ingest tens of terabytes in bulk, sustain 100k+ streaming writes per second, and keep p99 reads under 25ms - all in the same cluster, with a single API for callers.&lt;/p&gt;
&lt;p&gt;What makes this possible is that all three workloads share a common access contract: writes are eventually applied, reads are point or range lookups, and callers can tolerate a configurable staleness window. That shared contract is what lets one system serve all three patterns without them interfering with each other. Engineers no longer need to stitch together separate systems for each ingestion pattern and spend energy keeping them in sync - Mussel provides those guarantees out of the box.&lt;/p&gt;
&lt;p&gt;What strengthens the claim most is that Mussel v2 has been running in production for a year at this scale. Engineering blog posts are full of capability claims. A year of production evidence at Airbnb's traffic levels is harder to dismiss.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-the-story-is-really-about&quot;&gt;What the Story is Really About&lt;/h3&gt;
&lt;p&gt;Pull back from the specific technology choices - the NewSQL backend, the Kubernetes manifests, the topology-aware TTL service - and the most transferable part of this story comes into focus.&lt;/p&gt;
&lt;p&gt;Airbnb's ability to migrate a petabyte without downtime was shaped not just by what they built in v2, but by how v1 was designed years earlier. Kafka as the write path. Leaderless replication with topic-based ordering. Those decisions were made to solve different problems at the time. But the properties they created - a durable, replayable log at the center of every write, consistent ordering without a leader - turned out to be exactly what the v2 migration needed. The dual-write pipeline, the catch-up replay, the consistency guarantees during transition: all of it was more tractable because of how v1 worked.&lt;/p&gt;
&lt;p&gt;Good infrastructure tends to have this quality. It solves the problem in front of you, but it also leaves options open for when the problem changes. The v1 engineers weren't building migration infrastructure. But the choices they made left those options available.&lt;/p&gt;
&lt;p&gt;A useful question to ask about any storage system you own: not &quot;can we migrate this?&quot; but &quot;if we needed to migrate this, what properties do we already have that would help?&quot; Most teams only ask that question once a migration is already underway. Asking it earlier - and making deliberate architectural choices based on the answer - is what separates systems that are easy to evolve from systems that aren't.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;References:[ Building a Next-Generation Key-Valu&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14753/inside-airbnbs-mussel-v2-rebuilding-a-petabyte-scale-key-value-store</guid>
<pubDate>Tue, 14 Apr 2026 07:32:38 +0000</pubDate>
</item>
<item>
<title>CI/CD for Licensed Software You Don't Host</title>
<link>https://coderlegion.com/14419/ci-cd-for-licensed-software-you-dont-host</link>
<description>&lt;p&gt;We are three weeks away from shipping &lt;a rel=&quot;nofollow&quot; href=&quot;https://navengine.encha.cloud/&quot;&gt;NavEngine v4&lt;/a&gt;, an echo from the previous piece on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.marvinkweyu.net/indulge/business-minded-development&quot;&gt;Business Driven development&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I say &quot;&lt;em&gt;shipping&lt;/em&gt;&quot; loosely. There is no deployment script I run, no SSH session I open and no Kubernetes rollout I watch. The software lives on infrastructure I have never seen, behind firewalls I cannot reach, on machines whose specs I do not know. Shipping, in this context, means pushing an image to a registry and trusting that a process running inside a container on a customer's server will eventually notice and do something about it.&lt;/p&gt;
&lt;p&gt;That gap - between what I push and what the customer runs - is what this piece is about.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-assumption-collapse&quot;&gt;The Assumption Collapse&lt;/h4&gt;
&lt;p&gt;Every CI/CD tool I have ever used was built on a premise so foundational that nobody thought to state it: you control the deployment target. You own the server. You have the keys. Deployment, in the conventional sense, is just automation wrapped around access you already have.&lt;/p&gt;
&lt;p&gt;NavEngine is quite the opposite. It exists as a custom image - qcow2 - shipped onto the customer's infrastructure. The customer owns the machine. I do not have SSH access unless I go through DWService and even then, that is a support channel, not a deployment one. Yet somehow, we need to continuously deliver updates to machines we cannot reach, across connections we cannot guarantee, without breaking software that is actively in use.&lt;/p&gt;
&lt;p&gt;So the question became: if you cannot push, how do you deliver?&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Indulge&lt;/strong&gt; &lt;em&gt;The moment you decide to ship software you don't host, you have made a decision with consequences that will follow you for the life of the product. Not just operationally. Architecturally. Every assumption your codebase makes about the environment it runs in now belongs to someone else's infrastructure. That is not a deployment problem. It is a design problem that shows up at deployment time.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-answer-is-pull&quot;&gt;The Answer is Pull&lt;/h4&gt;
&lt;p&gt;Watchtower. It runs as a container alongside the rest of the stack, polls the Docker registry on a configured interval, and when it detects a new image digest on the tag it is watching, it pulls and restarts the relevant containers. No webhook, no push, no SSH. The installation phones home for updates and takes what it finds.&lt;/p&gt;
&lt;p&gt;The key design decision here was the floating tag. Every customer's Butane config ships with &lt;code&gt;core:stable&lt;/code&gt;. Not &lt;code&gt;core:v3.0.39&lt;/code&gt;. Not a digest pin. &lt;code&gt;stable&lt;/code&gt;. When Watchtower polls the registry and sees that &lt;code&gt;stable&lt;/code&gt; now resolves to a different digest than what is currently running, it pulls. What &quot;stable&quot; points to is entirely under my control, from the registry side, without touching anything on the customer's machine.&lt;/p&gt;
&lt;p&gt;This sounds obvious once you say it. It took longer than I would like to admit to get there.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;two-registries-two-floating-tags-one-gate&quot;&gt;Two Registries, Two floating tags, One gate&lt;/h4&gt;
&lt;p&gt;Here is the full pipeline as it actually runs.&lt;/p&gt;
&lt;p&gt;Every push to &lt;code&gt;main&lt;/code&gt; triggers a build on the dev registry. The image gets tagged with a version identifier and a floating tag - &lt;code&gt;staging&lt;/code&gt;. A staging environment - running the same Compose stack, same Butane configuration, same structure as a customer installation - pulls from &lt;code&gt;staging&lt;/code&gt;. This is where the image lives until I am satisfied it works.&lt;/p&gt;
&lt;p&gt;When staging looks good, I create a release. The production registry builds from that release, tags the image with the version (&lt;code&gt;v3.0.40&lt;/code&gt;) and overwrites the floating &lt;code&gt;stable&lt;/code&gt; tag. Customer installations, on their next Watchtower poll interval, see a new digest on &lt;code&gt;stable&lt;/code&gt; and update.&lt;/p&gt;
&lt;p&gt;The critical detail: &lt;code&gt;stable&lt;/code&gt; is never overwritten by a push to &lt;code&gt;main&lt;/code&gt;. Only by a release. The staging gate is the only thing standing between a broken image and a customer's running installation. There is no automated rollout percentage, no canary fleet, no gradual traffic shift. The gate is a human decision, made after watching staging run and deciding it is ready.&lt;/p&gt;
&lt;p&gt;For a solo-operated product at this stage, that is the right call. Complexity in release infrastructure that you do not need is just surface area for things to go wrong.&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Indulge&lt;/strong&gt; &lt;em&gt;A staging environment that does not accurately reflect production is a very expensive placebo. It gives you confidence without giving you information. The hardest thing about shipping self-hosted software is that your staging environment runs on infrastructure you understand, with data you created, on a network you control. Your customer's environment is none of those things. No pipeline fully closes that gap. The best you can do is know exactly where your confidence ends.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h4 id=&quot;what-happens-when-stable-is-broken&quot;&gt;What Happens When stable is broken&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=4562556149110474775&quot; alt=&quot;Backups&quot;&gt;&lt;/p&gt;
&lt;p&gt;It will happen. An image that passes staging will break in a customer environment for a reason that staging did not surface - a schema migration that assumed a clean database, a dependency that behaves differently on older hardware, a configuration value that was present in staging and absent in the field.&lt;/p&gt;
&lt;p&gt;The recovery flow is: fix on &lt;code&gt;main&lt;/code&gt;, watch it pass staging, cut a new release. &lt;code&gt;v3.0.41&lt;/code&gt; overwrites &lt;code&gt;stable&lt;/code&gt;. Watchtower picks it up on the next poll interval. The customer, who may or may not have noticed anything, is now running the fixed image.&lt;/p&gt;
&lt;p&gt;The window between the broken image landing and the fix arriving is real. Depending on how fast the hotfix moves through staging and how long the Watchtower poll interval is, a customer could be running broken software for anywhere from minutes to hours. There is no remote kill switch. There is no way to reach in and restart a service. There is DWService if the situation is bad enough to warrant it, but that is a support escalation, not a deployment tool.&lt;/p&gt;
&lt;p&gt;This is the honest cost of not controlling the deployment target. You accept a recovery latency that you cannot compress below a certain floor. The mitigation is not a cleverer pipeline. It is investing deeply in staging fidelity and in making sure the image fails loudly rather than silently - health checks that surface problems immediately, startup validation that refuses to run on bad configuration rather than running badly.&lt;/p&gt;
&lt;p&gt;A system that fails loudly is a system that can be fixed. A system that degrades quietly is a system that erodes trust before anyone knows there is a problem.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;enterprise-and-standard-different-cadences-same-pattern&quot;&gt;Enterprise and standard: different cadences, same pattern&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;How do we manage customers that diverge from the main product line with an enterprise license?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;NavEngine has two license tiers. Enterprise customers are on a separate release cadence from standard customers. The mechanism is straightforward: separate floating tags on the production registry. &lt;code&gt;core-enterprise:stable&lt;/code&gt; and &lt;code&gt;core-standard:stable&lt;/code&gt;. The Butane config shipped to each customer points at the appropriate tag. Enterprise releases can go out on a different schedule, carry different feature sets, and move more cautiously than standard releases.&lt;/p&gt;
&lt;p&gt;What prevents a standard customer from pointing Watchtower at the enterprise tag? Mostly friction. The Compose file is baked into the Butane config at provisioning time. There is no SSH access to change it. A customer would need console access and the motivation to go looking - unlikely for most, impossible to rule out for all.&lt;/p&gt;
&lt;p&gt;The proper answer is registry-level access control: pull tokens scoped to the tags each customer is entitled to, issued at license activation and revoked at expiry. This means the registry enforces entitlement, not just the application. An expired license means an expired pull token means no updates, enforced at the point of delivery rather than after the fact.&lt;/p&gt;
&lt;p&gt;This is on the roadmap. For v4, the answer is friction and trust.&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Indulge&lt;/strong&gt; &lt;em&gt;License enforcement in self-hosted software is a negotiation between what you can technically control and what you have to trust. You cannot fully control what runs on a machine you do not own. At some point, a sufficiently motivated customer can circumvent almost any enforcement mechanism you build. The goal is not to make circumvention impossible. It is to make compliance easier than circumvention, and to make the value proposition strong enough that the question rarely comes up.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-license-server-is-not-in-the-update-path&quot;&gt;The license server is not in the update path&lt;/h4&gt;
&lt;p&gt;One decision I am glad we made early: the licensing server and the Docker registry are separate infrastructure. They do not share a failure domain.&lt;/p&gt;
&lt;p&gt;Watchtower polls the registry. The license server is called from within one of the running containers as part of normal application operation. If the registry is unreachable, the software keeps running. If the license server is unreachable, the backend falls back to its last known state - persisted to disk, not held in memory, so it survives a container restart. The check runs periodically. The grace window is generous enough that a license server outage does not immediately affect customers, but not so generous that expired licenses can run indefinitely.&lt;/p&gt;
&lt;p&gt;This matters because the failure modes compound. A product update that requires a license check to proceed has just made the license server a dependency of your deployment pipeline. Any outage that hits your license infrastructure also hits your ability to ship updates to paying customers. Keeping these paths separate means they fail independently, and independent failures are recoverable in ways that cascading ones are not.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;What the pipeline actually looks like&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=16529364623777112074&quot; alt=&quot;ci/cd flow diagram&quot;&gt;&lt;/p&gt;
&lt;p&gt;The immutable version tags are not just for auditing. They are the rollback reference. If &lt;code&gt;v3.0.40&lt;/code&gt; breaks everything, &lt;code&gt;v3.0.39&lt;/code&gt; still exists in the registry. I can retag it as &lt;code&gt;stable&lt;/code&gt; manually and customers will roll back on the next poll. This has not been needed yet. It is there for the day it is.&lt;/p&gt;
&lt;hr&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Indulge&lt;/strong&gt; &lt;em&gt;Most CI/CD writing treats deployment as the end of the story. Ship it, watch the metrics, move on. Self-hosted software inverts this. Deployment is the beginning of a period during which software you cannot reach is running in an environment you cannot see, on behalf of a customer whose experience you will only hear about if something goes wrong. The pipeline is not a delivery mechanism. It is a trust mechanism. Every decision in it is a decision about how much you trust the image before it leaves your hands.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h4 id=&quot;three-weeks-out&quot;&gt;&lt;strong&gt;Three Weeks Out&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;NavEngine v4 is three weeks away. The pipeline is running. Staging has held. The tags are in place.&lt;/p&gt;
&lt;p&gt;None of that answers the only question that matters: &lt;em&gt;what happens when the software leaves you?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It will run on machines you have never touched, against data you have never seen, in environments that do not care about your assumptions. By the time it fails, if it fails, it will already be someone else’s problem - and still entirely yours. The customer notices before you do. That is the thing. I am shit scared.&lt;/p&gt;
&lt;p&gt;I suppose that this is the essence of &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.marvinkweyu.net/indulge/why-distributed-systems-field-notes&quot;&gt;these notes&lt;/a&gt; - to document real systems in real time. This is the inversion self-hosted systems force on you: treating deployment not as the end of control but as the beginning of its absence. &lt;/p&gt;
&lt;p&gt;So you design for that absence.&lt;/p&gt;
&lt;p&gt;You design for recovery over prevention.&lt;/p&gt;
&lt;p&gt;For visibility over certainty and trust over control.&lt;/p&gt;
&lt;p&gt;Everything else is just what it takes to make that possible.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14419/ci-cd-for-licensed-software-you-dont-host</guid>
<pubDate>Fri, 10 Apr 2026 08:46:09 +0000</pubDate>
</item>
<item>
<title>Building a Production-Ready NDVI Pipeline: Normalization Fixes, WAF Resilience, and System Stability</title>
<link>https://coderlegion.com/14008/building-production-ready-ndvi-pipeline-normalization-fixes-resilience-system-stability</link>
<description>&lt;hr&gt;
&lt;h2 id=&quot;from-it-works-to-production-fixing-ndvi-rendering-surviving-waf-blocks-and-stabilizing-a-failing-server&quot;&gt;From “It Works” to Production: Fixing NDVI Rendering, Surviving WAF Blocks, and Stabilizing a Failing Server&lt;/h2&gt;
&lt;p&gt;Most backend systems don’t fail because of obvious bugs.&lt;br&gt;
They fail quietly—through misleading data, unstable infrastructure, and assumptions that don’t hold under load.&lt;/p&gt;
&lt;p&gt;This week, I pushed an NDVI (Normalized Difference Vegetation Index) pipeline through a full transition:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;from &lt;em&gt;“it renders images”&lt;/em&gt; → to &lt;em&gt;a production-grade, resilient system&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This article breaks down what actually went wrong—and how each fix improved the system holistically.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-the-illusion-of-correctness-when-green-lies&quot;&gt;1. The Illusion of Correctness: When “Green” Lies&lt;/h3&gt;
&lt;p&gt;The first issue looked harmless:&lt;br&gt;
NDVI raster PNGs were rendering—but everything was green.&lt;/p&gt;
&lt;h4 id=&quot;root-cause&quot;&gt;Root Cause&lt;/h4&gt;
&lt;p&gt;The normalization formula:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;normalized = (ndvi + 1.0) / 2.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This assumes NDVI values span &lt;strong&gt;[-1, 1]&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;But real-world farm data was tighter:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;NDVI range: 0.42 → 0.63
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After normalization:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0.71 → 0.81 → Entirely “green zone”
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So the system was technically correct—but &lt;strong&gt;visually misleading&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-the-fix-dual-normalization-strategy&quot;&gt;2. The Fix: Dual Normalization Strategy&lt;/h3&gt;
&lt;p&gt;Instead of replacing one approach with another, I introduced &lt;strong&gt;two modes&lt;/strong&gt;:&lt;/p&gt;
&lt;h4 id=&quot;histogram-based-normalization-default-&quot;&gt;Histogram-Based Normalization (Default)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;normalized = (ndvi - ndvi_min) / (ndvi_max - ndvi_min)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Expands contrast per image&lt;/li&gt;
&lt;li&gt;Reveals actual variation&lt;/li&gt;
&lt;li&gt;Best for analysis&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;fixed-normalization-legacy-&quot;&gt;Fixed Normalization (Legacy)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;normalized = (ndvi + 1.0) / 2.0
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Keeps consistency across images&lt;/li&gt;
&lt;li&gt;Useful for comparisons&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;why-this-matters&quot;&gt;Why This Matters&lt;/h4&gt;
&lt;p&gt;This isn’t just a rendering fix—it’s a &lt;strong&gt;data integrity decision&lt;/strong&gt;:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Never force a single interpretation when the data has multiple valid contexts.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-external-systems-don-t-like-you-stac-waf-blocks-&quot;&gt;3. External Systems Don’t Like You (STAC WAF Blocks)&lt;/h3&gt;
&lt;p&gt;The next issue wasn’t in my system—it was in how it behaved externally.&lt;/p&gt;
&lt;h4 id=&quot;symptom&quot;&gt;Symptom&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Requests to Copernicus STAC API started failing&lt;/li&gt;
&lt;li&gt;Response: empty HTML or “Request Rejected”&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Classic &lt;strong&gt;WAF (Web Application Firewall)&lt;/strong&gt; behavior.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;root-cause-1&quot;&gt;Root Cause&lt;/h4&gt;
&lt;p&gt;The system behaved like a bot:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rapid consecutive requests&lt;/li&gt;
&lt;li&gt;Predictable timing patterns&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-fix-controlled-request-behavior&quot;&gt;The Fix: Controlled Request Behavior&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;sleep(base_interval + random.uniform(jitter_min, jitter_max))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Implemented:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Minimum delay between requests&lt;/li&gt;
&lt;li&gt;Random jitter (1–5 seconds)&lt;/li&gt;
&lt;li&gt;Graceful handling of invalid responses&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;result&quot;&gt;Result&lt;/h4&gt;
&lt;blockquote&gt;&lt;p&gt;The system stopped being “blocked” and started being &lt;strong&gt;tolerated&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is a key distributed systems lesson:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;You don’t just integrate with APIs—you adapt to their defensive behavior.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-architecture-maturity-preparing-for-redis-streams&quot;&gt;4. Architecture Maturity: Preparing for Redis Streams&lt;/h3&gt;
&lt;p&gt;The NDVI pipeline originally used &lt;strong&gt;Celery&lt;/strong&gt; for async jobs.&lt;/p&gt;
&lt;p&gt;Instead of rewriting everything, I introduced a &lt;strong&gt;routing abstraction&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;if settings.NDVI_QUEUE_BACKEND == &quot;celery&quot;:
    dispatch_celery_job(...)
elif settings.NDVI_QUEUE_BACKEND == &quot;stream&quot;:
    raise NotImplementedError
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;why-this-is-important&quot;&gt;Why This Is Important&lt;/h4&gt;
&lt;p&gt;This small change enables:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Seamless migration to Redis Streams&lt;/li&gt;
&lt;li&gt;Zero disruption to current workflows&lt;/li&gt;
&lt;li&gt;Incremental architecture evolution&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;principle&quot;&gt;Principle&lt;/h4&gt;
&lt;blockquote&gt;&lt;p&gt;The best migrations don’t start with new systems—they start with &lt;strong&gt;abstractions&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;5-the-real-problem-server-crashes-every-10-hours&quot;&gt;5. The Real Problem: Server Crashes Every 10 Hours&lt;/h3&gt;
&lt;p&gt;This was the most critical issue.&lt;/p&gt;
&lt;h4 id=&quot;symptoms&quot;&gt;Symptoms&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;System crashes after ~10 hours&lt;/li&gt;
&lt;li&gt;No single obvious failure point&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;root-cause-2&quot;&gt;Root Cause&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Unbounded resource usage across multiple services:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MySQL buffer pool growing unchecked&lt;/li&gt;
&lt;li&gt;PHP-FPM workers accumulating memory&lt;/li&gt;
&lt;li&gt;Redis using unlimited memory&lt;/li&gt;
&lt;li&gt;Celery workers never restarting&lt;/li&gt;
&lt;li&gt;Monitoring stack filling disk&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;6-the-fix-system-wide-resource-governance&quot;&gt;6. The Fix: System-Wide Resource Governance&lt;/h3&gt;
&lt;p&gt;Instead of patching one component, I enforced limits everywhere:&lt;/p&gt;
&lt;h4 id=&quot;database&quot;&gt;Database&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;innodb_buffer_pool_size = 512M
max_connections = 50
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;php-fpm&quot;&gt;PHP-FPM&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-ini&quot;&gt;pm.max_children = 15
pm.max_requests = 500
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;redis&quot;&gt;Redis&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;mem_limit: 512mb
maxmemory-policy: allkeys-lru
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;celery&quot;&gt;Celery&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;worker_max_tasks_per_child = 100
worker_max_memory_per_child = 512000  # KB
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;observability-stack&quot;&gt;Observability Stack&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Prometheus: 7-day retention&lt;/li&gt;
&lt;li&gt;Loki: compaction + retention limits&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;result-1&quot;&gt;Result&lt;/h4&gt;
&lt;blockquote&gt;&lt;p&gt;The system moved from &lt;strong&gt;eventual failure → controlled lifecycle stability&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;7-observability-and-documentation-the-often-ignored-layer-&quot;&gt;7. Observability and Documentation (The Often Ignored Layer)&lt;/h3&gt;
&lt;p&gt;Alongside code changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Added alerts for system health&lt;/li&gt;
&lt;li&gt;Documented all NDVI configuration settings&lt;/li&gt;
&lt;li&gt;Created deployment + rollback guides&lt;/li&gt;
&lt;li&gt;Expanded test coverage&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;why-this-matters-1&quot;&gt;Why This Matters&lt;/h4&gt;
&lt;p&gt;Without this layer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fixes don’t scale&lt;/li&gt;
&lt;li&gt;Bugs repeat&lt;/li&gt;
&lt;li&gt;Systems become tribal knowledge&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;8-security-mistake-worth-mentioning-&quot;&gt;8. Security Mistake (Worth Mentioning)&lt;/h3&gt;
&lt;p&gt;During the process, a sudo password was exposed in plain text.&lt;/p&gt;
&lt;p&gt;No system is “production-ready” if secrets are handled casually.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;immediate-takeaways&quot;&gt;Immediate Takeaways&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Never expose credentials in logs or chats&lt;/li&gt;
&lt;li&gt;Rotate immediately if leaked&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Move toward:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;environment-based configs&lt;/li&gt;
&lt;li&gt;secret management systems&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thoughts-2&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;This wasn’t about fixing a bug.&lt;/p&gt;
&lt;p&gt;It was about addressing three deeper problems:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Data correctness&lt;/strong&gt; (NDVI visualization)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;System behavior under external constraints&lt;/strong&gt; (WAF)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Infrastructure stability&lt;/strong&gt; (resource limits)&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-real-transformation&quot;&gt;The Real Transformation&lt;/h4&gt;
&lt;p&gt;Before:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“The system works.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;After:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;“The system is predictable, resilient, and ready for scale.”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;key-lessons&quot;&gt;Key Lessons&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Correct output is not always &lt;strong&gt;truthful output&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;External APIs require &lt;strong&gt;behavioral adaptation&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Systems fail through &lt;strong&gt;unbounded growth&lt;/strong&gt;, not sudden errors&lt;/li&gt;
&lt;li&gt;Architecture evolves through &lt;strong&gt;abstraction, not rewrites&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Stability is a &lt;strong&gt;system-wide property&lt;/strong&gt;, not a single fix&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;If you’re building data pipelines or distributed systems, this pattern will repeat.&lt;/p&gt;
&lt;p&gt;The earlier you design for it—the less painful it becomes.&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14008/building-production-ready-ndvi-pipeline-normalization-fixes-resilience-system-stability</guid>
<pubDate>Sun, 05 Apr 2026 11:21:57 +0000</pubDate>
</item>
<item>
<title>Why Your System Fails on the Most Predictable Day of the Year</title>
<link>https://coderlegion.com/13904/why-your-system-fails-on-the-most-predictable-day-of-the-year</link>
<description>&lt;h3 id=&quot;the-problem-hiding-in-plain-sight&quot;&gt;The Problem Hiding in Plain Sight&lt;/h3&gt;
&lt;p&gt;Most applications don't fail because of bad code. They fail because of bad architecture decisions made early that nobody questioned until it was too late.&lt;/p&gt;
&lt;p&gt;Here's what actually breaks systems at scale:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Everything talking to everything (no clear boundaries)&lt;/li&gt;
&lt;li&gt;The database doing work the application should do&lt;/li&gt;
&lt;li&gt;Synchronous processing where async was needed&lt;/li&gt;
&lt;li&gt;One giant service that owns too much responsibility&lt;/li&gt;
&lt;li&gt;No separation between reads and writes under heavy load&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these are framework problems. None of these are language problems. They are thinking problems.&lt;/p&gt;
&lt;p&gt;To make this concrete, let's look at one example that millions of people have personally experienced and that engineers have failed to solve for decades.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-university-enrollment-system&quot;&gt;The University Enrollment System&lt;/h3&gt;
&lt;p&gt;Think about course enrollment day at any university.&lt;/p&gt;
&lt;p&gt;Every semester, thousands of students across every department flood the portal at the same time, each trying to register for their own offered courses. The load is not a surprise. The date is on the calendar. It happens every single semester like clockwork.&lt;/p&gt;
&lt;p&gt;But the system was never designed for it.&lt;/p&gt;
&lt;p&gt;Every request hits the same flow: check eligibility, check seat availability, write enrollment, update seat count, all synchronously, all at once. No queue. No cache. No separation between reads and writes. Just a database choking under the weight of an entirely predictable moment.&lt;/p&gt;
&lt;p&gt;Students get timeout errors. Duplicate enrollments. Lost seats they were fully eligible for. Everyone refreshes in panic. And every semester, someone calls IT to &quot;increase the server capacity&quot; and nothing really changes.&lt;/p&gt;
&lt;p&gt;The code isn't broken. The thinking is.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-5-architectural-problems-hiding-inside-one-example&quot;&gt;The 5 Architectural Problems Hiding Inside One Example&lt;/h3&gt;
&lt;p&gt;Most teams look at this scenario and see one problem: too much traffic. So they throw more servers at it. It helps a little, then fails again next semester.&lt;/p&gt;
&lt;p&gt;The reality is there are 5 separate problems here, each requiring a different solution. Solving one without the others just moves the failure to a different place.&lt;/p&gt;
&lt;h4 id=&quot;problem-1-performance-the-spike&quot;&gt;Problem 1: Performance, the Spike&lt;/h4&gt;
&lt;p&gt;Thousands of requests hitting the database simultaneously will bring any system to its knees regardless of how powerful the hardware is.&lt;/p&gt;
&lt;p&gt;The fixes here are well understood but rarely implemented together:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A queue absorbs the spike so the database is not flooded all at once&lt;/li&gt;
&lt;li&gt;A cache layer serves seat availability reads without hitting the database on every refresh&lt;/li&gt;
&lt;li&gt;Separating reads from writes so checking availability and writing enrollment don't compete on the same database connection pool&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This solves the crash. But it does not solve correctness. And that is where most teams stop.&lt;/p&gt;
&lt;h4 id=&quot;problem-2-correctness-the-race-condition&quot;&gt;Problem 2: Correctness, the Race Condition&lt;/h4&gt;
&lt;p&gt;Even after adding a queue, if multiple workers process requests in parallel, two students can still read &quot;1 seat available&quot; at the same time, both pass the eligibility check, and both get enrolled into the same last seat. The queue serialized the intake but not the processing.&lt;/p&gt;
&lt;p&gt;This is a race condition, and a queue alone does not fix it.&lt;/p&gt;
&lt;p&gt;The real solutions:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pessimistic locking&lt;/strong&gt; locks the seat row at the database level while one transaction is processing it, forcing all others to wait. It is safe and guarantees correctness but creates a bottleneck under extreme concurrent load and carries the risk of deadlocks if not implemented carefully.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Optimistic locking&lt;/strong&gt; takes the opposite approach. Allow concurrent reads, but before writing the enrollment, check a version number on the seat record. If someone else already changed it, the transaction fails and must retry. Better for scale but generates retries under heavy conflict, which adds its own overhead.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Distributed locking with Redis&lt;/strong&gt; places a temporary hold on a seat while it is being processed, similar to a reservation window on a ticket booking site, preventing any other request from touching that record at the same time. This is the approach production-grade booking systems actually use.&lt;/p&gt;
&lt;p&gt;The right choice depends on your conflict rate, your scale, and your tolerance for complexity. But the wrong choice is to pick none of them and assume the queue is enough.&lt;/p&gt;
&lt;h4 id=&quot;problem-3-idempotency-the-double-click&quot;&gt;Problem 3: Idempotency, the Double Click&lt;/h4&gt;
&lt;p&gt;A student hits Enroll and the page is slow. They click again. Now two identical requests are in flight to the server.&lt;/p&gt;
&lt;p&gt;Even if you have solved the race condition with locking, without idempotency both clicks can create two enrollment records for the same student in the same course. The lock protects the seat count. It does not protect against the same student submitting twice.&lt;/p&gt;
&lt;p&gt;The fix is an idempotency key, a unique identifier generated by the client and attached to each enrollment request. When the server receives a request, it checks whether that key has already been processed. If yes, it returns the original result. If no, it processes it and stores the key. The second click gets the same response as the first without triggering a second enrollment.&lt;/p&gt;
&lt;p&gt;This is not a database problem. It is an API design problem. And it is the one most teams never think about until they find duplicate records in production and cannot explain why.&lt;/p&gt;
&lt;h4 id=&quot;problem-4-partial-failure-the-half-enrolled-student&quot;&gt;Problem 4: Partial Failure, the Half-Enrolled Student&lt;/h4&gt;
&lt;p&gt;A successful enrollment is not one database write. It is a chain of operations: write the enrollment record, decrement the seat count, send a confirmation notification, update the academic record, generate a fee entry.&lt;/p&gt;
&lt;p&gt;What happens if the system crashes after step 2 but before step 3?&lt;/p&gt;
&lt;p&gt;The seat is taken. The student has no confirmation. The database says enrolled. The academic record disagrees. The student calls support. Support has no idea what state the system is in.&lt;/p&gt;
&lt;p&gt;This is a partial failure and it is one of the hardest problems in distributed systems because it is invisible until a real person reports confusion.&lt;/p&gt;
&lt;p&gt;Every multi-step operation needs either a fully atomic transaction that commits everything or rolls back completely, or a compensation strategy, a predefined plan for how to cleanly undo each completed step if something fails halfway. Designing for the happy path is easy. Designing for the middle of failure is architecture.&lt;/p&gt;
&lt;h4 id=&quot;problem-5-the-stale-cache-trap&quot;&gt;Problem 5: The Stale Cache Trap&lt;/h4&gt;
&lt;p&gt;Earlier we said to add a cache layer to serve seat availability reads. That is still correct. But it introduces a new failure mode that needs to be designed for explicitly.&lt;/p&gt;
&lt;p&gt;The cache says 5 seats available. The database already has 0 after recent enrollments that have not yet invalidated the cache. Students attempt to enroll based on stale data, hit the lock, fail, and receive a confusing error that says the course is full, even though the portal showed availability 3 seconds ago.&lt;/p&gt;
&lt;p&gt;The cache improved performance but silently introduced a trust problem. Students lose confidence in the system. Support tickets spike. And the engineering team is confused because the system is technically working correctly, the cache just has not caught up.&lt;/p&gt;
&lt;p&gt;Cache invalidation strategy needs to be designed upfront: how quickly must the cache reflect reality, what triggers an invalidation, and what happens to users who are mid-flow when availability changes. These are not performance questions. They are product and architecture questions.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-this-means-for-how-you-build&quot;&gt;What This Means for How You Build&lt;/h3&gt;
&lt;p&gt;Five problems. Five different failure modes. Five different solutions. All hiding inside one scenario that happens on a date printed on the academic calendar months in advance.&lt;/p&gt;
&lt;p&gt;This is why scaling is not about upgrading your server. The server was never the problem.&lt;/p&gt;
&lt;p&gt;The engineers who scale systems well ask different questions from the start:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Where are my bottlenecks under 10x load?&lt;/li&gt;
&lt;li&gt;What happens if this one service goes down?&lt;/li&gt;
&lt;li&gt;Am I coupling things that should be independent?&lt;/li&gt;
&lt;li&gt;Is my data model fighting my query patterns?&lt;/li&gt;
&lt;li&gt;What happens in the middle of a failure, not just at the end?&lt;/li&gt;
&lt;li&gt;Can the same request safely arrive twice?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these questions have anything to do with which framework you picked or which cloud provider you use. They are design questions. Thinking questions.&lt;/p&gt;
&lt;p&gt;The engineers who ask them before traffic arrives are the ones who sleep well on enrollment day.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thought-3&quot;&gt;Final Thought&lt;/h3&gt;
&lt;p&gt;Architecture is not about knowing the right tools. It is about asking the right questions early enough that the answers still matter.&lt;/p&gt;
&lt;p&gt;The skill that transfers everywhere, across languages, frameworks, companies, and industries, is the ability to look at a system and ask: what am I not designing for yet?&lt;/p&gt;
&lt;p&gt;Everything else is just syntax.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13904/why-your-system-fails-on-the-most-predictable-day-of-the-year</guid>
<pubDate>Fri, 03 Apr 2026 12:24:22 +0000</pubDate>
</item>
<item>
<title>When Redis Lies: Debugging a Schema That Wasn’t There</title>
<link>https://coderlegion.com/13524/when-redis-lies-debugging-a-schema-that-wasnt-there</link>
<description>&lt;p&gt; Nextcloud ↔ Django (DRF) ↔ Redis-backed distributed system&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;There’s a particular class of bugs that doesn’t live in your code, your database, or your API.&lt;/p&gt;
&lt;p&gt;It lives in the &lt;strong&gt;space between them&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Today’s issue looked simple on the surface:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;“Unable to parse farm schema response.”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But the root cause wasn’t in the UI, nor in the API.&lt;/p&gt;
&lt;p&gt;It was in the cache.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-system-setup&quot;&gt;The System Setup&lt;/h3&gt;
&lt;p&gt;The architecture is fairly typical for a modern distributed backend:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Nextcloud App&lt;/strong&gt; (UI + PHP backend)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Django REST Framework (DRF)&lt;/strong&gt; API (&lt;code&gt;/api/v1/...&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redis&lt;/strong&gt; (used for caching + token storage)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NDVI service&lt;/strong&gt; feeding farm-state data&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Redis had recently replaced APCu as the caching backend to support &lt;strong&gt;shared worker state and token reuse&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-symptom&quot;&gt;The Symptom&lt;/h3&gt;
&lt;p&gt;The Nextcloud admin UI failed to load farm schema with:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;em&gt;“Unable to parse farm schema response”&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This error implied one thing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The frontend expected &lt;strong&gt;valid JSON&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;It received something else&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-misleading-signals&quot;&gt;The Misleading Signals&lt;/h3&gt;
&lt;p&gt;Initial checks showed everything was “correct”:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;✅ Schema endpoint URL was valid&lt;br&gt;
&lt;code&gt;/apps/weather_apis/api/v1/admin/farms/schema&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ Route existed and matched&lt;br&gt;
&lt;code&gt;occ router:match&lt;/code&gt; confirmed it&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ Redis was healthy&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;redis-cli ping&lt;/code&gt; → OK&lt;/li&gt;
&lt;li&gt;PHP connection as &lt;code&gt;www-data&lt;/code&gt; → OK&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ Backend logic was intact&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Yet the UI still failed.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-trap&quot;&gt;The Trap&lt;/h3&gt;
&lt;p&gt;At this point, most debugging paths would focus on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;fixing the controller&lt;/li&gt;
&lt;li&gt;adjusting serializers&lt;/li&gt;
&lt;li&gt;checking permissions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But none of those were the problem.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-reality&quot;&gt;The Reality&lt;/h3&gt;
&lt;p&gt;The issue was &lt;strong&gt;cache-induced inconsistency&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Redis had stored a &lt;strong&gt;stale response&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;The backend schema had evolved&lt;/li&gt;
&lt;li&gt;The cached payload no longer matched the expected structure&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the system behaved like this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Layer   &lt;/th&gt;&lt;th&gt;State&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Route   &lt;/td&gt;&lt;td&gt;Correct&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Backend &lt;/td&gt;&lt;td&gt;Correct&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Cache   &lt;/td&gt;&lt;td&gt;&lt;strong&gt;Outdated&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;UI      &lt;/td&gt;&lt;td&gt;Broken&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The UI wasn’t wrong.&lt;/p&gt;
&lt;p&gt;The backend wasn’t wrong.&lt;/p&gt;
&lt;p&gt;The cache was lying.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-fix&quot;&gt;The Fix&lt;/h3&gt;
&lt;p&gt;The resolution was simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Clear Redis cache keys&lt;/li&gt;
&lt;li&gt;Reload the UI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Immediately:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Schema endpoint returned valid JSON&lt;/li&gt;
&lt;li&gt;UI parsing succeeded&lt;/li&gt;
&lt;li&gt;Farms admin interface recovered&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-deeper-insight&quot;&gt;The Deeper Insight&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Caches don’t just store data — they store assumptions.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;When your system changes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API shapes evolve&lt;/li&gt;
&lt;li&gt;serializers update&lt;/li&gt;
&lt;li&gt;authentication rules shift&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But your cache:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;continues serving &lt;strong&gt;old truths&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-this-matters-2&quot;&gt;Why This Matters&lt;/h3&gt;
&lt;p&gt;This issue highlights a key principle in distributed systems:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;A system can be &lt;strong&gt;locally correct&lt;/strong&gt; but &lt;strong&gt;globally inconsistent&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Every component was working as designed.&lt;/p&gt;
&lt;p&gt;But together, they produced failure.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;extending-the-system-authentication-alignment&quot;&gt;Extending the System: Authentication Alignment&lt;/h3&gt;
&lt;p&gt;Alongside the fix, the farm state endpoint was improved:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Introduced &lt;strong&gt;Redis-backed authentication&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Unified with existing observation pattern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JWT support&lt;/li&gt;
&lt;li&gt;API key authentication&lt;/li&gt;
&lt;li&gt;Integration tokens&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Added:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;scope enforcement&lt;/li&gt;
&lt;li&gt;allowlisting&lt;/li&gt;
&lt;li&gt;OpenAPI documentation updates&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tests were expanded to cover:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API key access&lt;/li&gt;
&lt;li&gt;integration token flows&lt;/li&gt;
&lt;li&gt;proper rejection behavior (&lt;code&gt;403&lt;/code&gt; for unauthenticated)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Result:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;7/7 tests passing&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Consistent and secure API surface&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;practical-takeaways&quot;&gt;Practical Takeaways&lt;/h3&gt;
&lt;h4 id=&quot;1-always-suspect-cache&quot;&gt;1. Always Suspect Cache&lt;/h4&gt;
&lt;p&gt;If you see:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;incorrect response shapes&lt;/li&gt;
&lt;li&gt;parsing errors&lt;/li&gt;
&lt;li&gt;inconsistent behavior across layers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt; Check the cache first.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;2-introduce-cache-versioning&quot;&gt;2. Introduce Cache Versioning&lt;/h4&gt;
&lt;p&gt;Avoid collisions between old and new schemas:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;farms:schema:v1
farms:schema:v2
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;3-use-ttls-for-dynamic-data&quot;&gt;3. Use TTLs for Dynamic Data&lt;/h4&gt;
&lt;p&gt;Schema and metadata endpoints should not live forever in cache.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;4-add-observability&quot;&gt;4. Add Observability&lt;/h4&gt;
&lt;p&gt;Track:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;cache HIT / MISS&lt;/li&gt;
&lt;li&gt;response origin (cache vs source)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h4 id=&quot;5-design-defensive-apis&quot;&gt;5. Design Defensive APIs&lt;/h4&gt;
&lt;p&gt;Ensure endpoints:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;always return valid JSON&lt;/li&gt;
&lt;li&gt;even in failure scenarios&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thought-4&quot;&gt;Final Thought&lt;/h3&gt;
&lt;p&gt;This wasn’t a UI bug.&lt;br&gt;
It wasn’t an API bug.&lt;br&gt;
It wasn’t even a Redis bug.&lt;/p&gt;
&lt;p&gt;It was a &lt;strong&gt;system truth mismatch&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;And those are the bugs that matter most—because solving them means you’re no longer just writing code.&lt;/p&gt;
&lt;p&gt;You’re understanding the system.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;“In distributed systems, the hardest bugs aren’t where things break.&lt;br&gt;
They’re where things &lt;em&gt;appear&lt;/em&gt; to work.”&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13524/when-redis-lies-debugging-a-schema-that-wasnt-there</guid>
<pubDate>Thu, 26 Mar 2026 19:20:01 +0000</pubDate>
</item>
<item>
<title>Escaping Cache Fragmentation: How Misconfigured PHP Workers Flooded My Token System</title>
<link>https://coderlegion.com/13329/escaping-cache-fragmentation-how-misconfigured-php-workers-flooded-my-token-system</link>
<description>&lt;h3 id=&quot;the-symptom-1&quot;&gt;The Symptom&lt;/h3&gt;
&lt;p&gt;I started noticing something strange in my observability stack:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Integration tokens were being minted repeatedly&lt;/li&gt;
&lt;li&gt;My token endpoint showed activity even when no user interaction was happening&lt;/li&gt;
&lt;li&gt;Metrics suggested constant “traffic” to an otherwise idle system&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At first glance, it looked like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A security issue&lt;/li&gt;
&lt;li&gt;A rogue client&lt;/li&gt;
&lt;li&gt;Or a broken API consumer&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It was none of those.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-root-cause&quot;&gt;The Root Cause&lt;/h3&gt;
&lt;p&gt;The issue came down to a subtle but critical architectural mistake:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;I was using a non-shared cache in a multi-worker environment.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&quot;stack-involved-&quot;&gt;Stack involved:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;PHP-FPM (2 workers)&lt;/li&gt;
&lt;li&gt;APCu (in-memory cache)&lt;/li&gt;
&lt;li&gt;Token-based integration between services&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-what-went-wrong&quot;&gt;⚙️ What Went Wrong&lt;/h3&gt;
&lt;p&gt;APCu is &lt;strong&gt;process-local&lt;/strong&gt;, not shared.&lt;/p&gt;
&lt;p&gt;That means:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Worker A cache ≠ Worker B cache
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each PHP-FPM worker had its own isolated memory.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-cascade-effect&quot;&gt;The Cascade Effect&lt;/h3&gt;
&lt;p&gt;My token logic was straightforward:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;if token not in cache:
    mint_new_token()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But in reality, the system behaved like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Request hits Worker A → token exists → OK&lt;/li&gt;
&lt;li&gt;Next request hits Worker B → cache miss → mint new token&lt;/li&gt;
&lt;li&gt;Repeat across workers → continuous token regeneration&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3 id=&quot;why-observability-looked-wrong-&quot;&gt;Why Observability Looked “Wrong”&lt;/h3&gt;
&lt;p&gt;From the outside, it looked like traffic was hitting the token endpoint.&lt;/p&gt;
&lt;p&gt;But in reality:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;The system was generating its own traffic due to cache inconsistency.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is a key lesson:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Not all traffic is external&lt;/li&gt;
&lt;li&gt;Some is &lt;strong&gt;emergent behavior from system design&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-the-fix&quot;&gt;✅ The Fix&lt;/h3&gt;
&lt;p&gt;I switched from APCu to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Redis (shared cache)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;All workers → same cache → consistent token state
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;result-&quot;&gt;Result:&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Tokens minted once&lt;/li&gt;
&lt;li&gt;Reused across all workers&lt;/li&gt;
&lt;li&gt;Metrics stabilized instantly&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;production-hardening-what-i-added-next-&quot;&gt;Production Hardening (What I Added Next)&lt;/h3&gt;
&lt;p&gt;Fixing the cache wasn’t enough — I hardened the system further.&lt;/p&gt;
&lt;h4 id=&quot;1-distributed-locking&quot;&gt;1. Distributed Locking&lt;/h4&gt;
&lt;p&gt;To prevent race conditions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;if token exists:
    return token

acquire lock
    re-check cache
    mint token if still missing
release lock
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;2-ttl-buffering&quot;&gt;2. TTL Buffering&lt;/h4&gt;
&lt;p&gt;Avoid edge expiration issues:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cache_ttl = token_expiry - safety_margin
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;3-observability-metrics&quot;&gt;3. Observability Metrics&lt;/h4&gt;
&lt;p&gt;I added:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;token_cache_hits&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;token_cache_misses&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;token_mint_count&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now anomalies show up immediately.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;key-takeaway&quot;&gt;Key Takeaway&lt;/h3&gt;
&lt;p&gt;This wasn’t just a bug.&lt;/p&gt;
&lt;p&gt;It was a &lt;strong&gt;distributed systems failure mode&lt;/strong&gt;:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Cache locality + multi-worker architecture → inconsistent state → emergent traffic&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;-final-insight&quot;&gt;⚡ Final Insight&lt;/h3&gt;
&lt;p&gt;If your system:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Runs multiple workers&lt;/li&gt;
&lt;li&gt;Uses in-memory caching&lt;/li&gt;
&lt;li&gt;Relies on shared state&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then this rule applies:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;If your cache isn’t shared, your state isn’t real.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;closing&quot;&gt;Closing&lt;/h3&gt;
&lt;p&gt;This issue reinforced something critical in my engineering journey:&lt;/p&gt;
&lt;p&gt;You don’t debug systems by staring at code —&lt;br&gt;
you debug them by understanding &lt;strong&gt;how state flows across boundaries&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;If you're building distributed APIs, token systems, or high-concurrency services —&lt;br&gt;
this is one edge case worth designing for early.&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13329/escaping-cache-fragmentation-how-misconfigured-php-workers-flooded-my-token-system</guid>
<pubDate>Sun, 22 Mar 2026 18:02:00 +0000</pubDate>
</item>
<item>
<title>From 80-Second APIs to Sub-Second: Rebuilding a Geospatial Backend with Async Pipelines</title>
<link>https://coderlegion.com/13317/from-80-second-apis-to-sub-second-rebuilding-a-geospatial-backend-with-async-pipelines</link>
<description>&lt;h3 id=&quot;from-80-second-apis-to-sub-second-fixing-latency-with-async-pipelines-django-celery-&quot;&gt;&lt;strong&gt;From 80-Second APIs to Sub-Second: Fixing Latency with Async Pipelines (Django + Celery)&lt;/strong&gt;&lt;/h3&gt;
&lt;h3 id=&quot;introduction-1&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;At some point, every backend engineer hits this wall:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The API works perfectly… until it doesn’t.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I hit that wall with a farm analytics endpoint computing NDVI (Normalized Difference Vegetation Index) from satellite imagery. The system was correct, the logic was sound, and the results were accurate.&lt;/p&gt;
&lt;p&gt;But the numbers told a different story:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;P95 latency: 1.25 minutes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s not an API. That’s a blocking compute job pretending to be one.&lt;/p&gt;
&lt;p&gt;This is the story of how I redesigned the system—from a synchronous request-driven model to an asynchronous data pipeline—and brought latency down to &lt;strong&gt;sub-second performance (P95 ≈ 725ms)&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-original-architecture-the-hidden-problem-&quot;&gt;The Original Architecture (The Hidden Problem)&lt;/h3&gt;
&lt;p&gt;At first glance, the system looked clean:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;[Client]
   ↓
[Django API]
   ↓
[STAC API → Satellite Data]
   ↓
[Raster Processing (NDVI)]
   ↓
[Response]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;what-happened-on-each-request-&quot;&gt;What happened on each request?&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Query satellite imagery via STAC&lt;/li&gt;
&lt;li&gt;Fetch raster bands (Red &amp;amp; NIR) from remote storage&lt;/li&gt;
&lt;li&gt;Process NDVI using rasterio&lt;/li&gt;
&lt;li&gt;Aggregate coverage&lt;/li&gt;
&lt;li&gt;Return result&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;why-this-seemed-fine&quot;&gt;Why this seemed fine&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;It worked locally&lt;/li&gt;
&lt;li&gt;It returned correct data&lt;/li&gt;
&lt;li&gt;It followed a “pure API” mindset&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But under the hood:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Remote I/O (S3-backed satellite data)&lt;/li&gt;
&lt;li&gt;Heavy raster decoding (JPEG2000)&lt;/li&gt;
&lt;li&gt;Sequential band reads&lt;/li&gt;
&lt;li&gt;Full computation per request&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-breaking-point&quot;&gt;The Breaking Point&lt;/h3&gt;
&lt;p&gt;Logs told the truth.&lt;/p&gt;
&lt;p&gt;Each request looked like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;STAC request → ~5s
Raster read (B04) → ~5–10s
Raster read (B08) → ~5–10s
Processing → ~5s+
Total → ~80+ seconds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the key realization:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;I wasn’t building an API—I was executing a geospatial compute pipeline on every request.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-core-insight&quot;&gt;The Core Insight&lt;/h3&gt;
&lt;p&gt;This is the shift that changes everything:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;APIs should &lt;strong&gt;serve data&lt;/strong&gt;, not &lt;strong&gt;compute it on demand&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The problem wasn’t Python.&lt;br&gt;
The problem wasn’t Django.&lt;br&gt;
The problem was &lt;strong&gt;architecture&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-new-architecture-async-pipeline-&quot;&gt;The New Architecture (Async Pipeline)&lt;/h3&gt;
&lt;p&gt;I redesigned the system around &lt;strong&gt;asynchronous computation + caching&lt;/strong&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;             (Scheduled / Triggered)
                    ↓
             [Celery Worker]
                    ↓
         [NDVI Computation Pipeline]
                    ↓
             [Redis / Database]
                    ↓
[Client] → [Django API] → [Cache Lookup]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;key-changes&quot;&gt;Key changes&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;NDVI computation moved out of the request path&lt;/li&gt;
&lt;li&gt;Results cached in Redis&lt;/li&gt;
&lt;li&gt;Background jobs compute and refresh data&lt;/li&gt;
&lt;li&gt;API returns instantly (no heavy compute)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;diagram-1-before-vs-after&quot;&gt;Diagram 1 — Before vs After&lt;/h3&gt;
&lt;h4 id=&quot;before-request-driven-&quot;&gt;Before (Request-driven)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Request
   ↓
STAC API
   ↓
Raster I/O
   ↓
NDVI Compute
   ↓
Response (80s)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;after-pipeline-driven-&quot;&gt;After (Pipeline-driven)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;Request → Cache → Response (~725ms P95)
              ↓ (miss)
         Async Task
              ↓
       Compute + Store
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;implementation&quot;&gt;Implementation&lt;/h3&gt;
&lt;h4 id=&quot;1-fast-api-path-non-blocking-&quot;&gt;1. Fast API Path (Non-blocking)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from django.core.cache import cache
from ndvi.tasks import compute_farm_state_coverage

def get_farm_state(farm_id: int) -&amp;gt; dict:
    cache_key = f&quot;farm_state:{farm_id}&quot;

    data = cache.get(cache_key)
    if data:
        return data

    compute_farm_state_coverage.delay(farm_id=farm_id)

    return {
        &quot;coverage_pct&quot;: None,
        &quot;status&quot;: &quot;processing&quot;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;2-celery-task-async-compute-&quot;&gt;2. Celery Task (Async Compute)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from celery import shared_task
from django.core.cache import cache

@shared_task(bind=True, autoretry_for=(Exception,), retry_backoff=True)
def compute_farm_state_coverage(self, farm_id: int) -&amp;gt; None:
    coverage = compute_ndvi_coverage(farm_id)

    cache.set(
        f&quot;farm_state:{farm_id}&quot;,
        {
            &quot;coverage_pct&quot;: coverage,
            &quot;status&quot;: &quot;ready&quot;
        },
        timeout=60 * 60 * 6,
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h4 id=&quot;3-daily-backfill-critical-&quot;&gt;3. Daily Backfill (Critical)&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from celery import shared_task

@shared_task
def enqueue_daily_farm_state_coverage():
    farm_ids = get_active_farm_ids()

    for farm_id in farm_ids:
        compute_farm_state_coverage.delay(farm_id=farm_id)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;observability-the-real-upgrade-&quot;&gt;Observability (The Real Upgrade)&lt;/h3&gt;
&lt;p&gt;Metrics added:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Task duration&lt;/li&gt;
&lt;li&gt;Task success/failure&lt;/li&gt;
&lt;li&gt;Queue depth&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;metrics-grafana-observations-&quot;&gt;Metrics (Grafana Observations)&lt;/h3&gt;
&lt;h4 id=&quot;grafana-screenshots&quot;&gt;Grafana Screenshots&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;1. Latency Graph&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sgrbfnr3v7fcvdop951k.png&quot; alt=&quot;725ms on farm get endpoint&quot;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;before&quot;&gt;Before&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;P95 latency: ~1.25 minutes&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;after&quot;&gt;After&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;API latency: ~725ms (P95)&lt;/li&gt;
&lt;li&gt;Background tasks: 60–90s&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;before-vs-after-summary&quot;&gt;Before vs After Summary&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Metric        &lt;/th&gt;&lt;th&gt;Before         &lt;/th&gt;&lt;th&gt;After&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;API latency   &lt;/td&gt;&lt;td&gt;1.25 min       &lt;/td&gt;&lt;td&gt;~725 ms (P95)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;System type   &lt;/td&gt;&lt;td&gt;Request-driven &lt;/td&gt;&lt;td&gt;Pipeline-driven&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Scalability   &lt;/td&gt;&lt;td&gt;Poor           &lt;/td&gt;&lt;td&gt;Strong&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Observability &lt;/td&gt;&lt;td&gt;Minimal        &lt;/td&gt;&lt;td&gt;Improved&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-thought-5&quot;&gt;Final Thought&lt;/h3&gt;
&lt;blockquote&gt;&lt;p&gt;I stopped treating my API like a calculator and started treating my system like a data pipeline.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That’s when everything changed.&lt;/p&gt;
&lt;hr&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13317/from-80-second-apis-to-sub-second-rebuilding-a-geospatial-backend-with-async-pipelines</guid>
<pubDate>Sun, 22 Mar 2026 11:15:19 +0000</pubDate>
</item>
<item>
<title>Decision Stack</title>
<link>https://coderlegion.com/13122/decision-stack</link>
<description>&lt;p&gt;✖ ✖ ✖ ✖ ✖ ✖ ✖&lt;/p&gt;
&lt;p&gt;topic: decision_stack&lt;/p&gt;
&lt;p&gt;Lately I’ve been following Cristiano Messina’s “Decision Stack” series.&lt;/p&gt;
&lt;p&gt;A very interesting and professional perspective&lt;br&gt;
on how modern systems should be designed.&lt;/p&gt;
&lt;p&gt;The model is simple but powerful:&lt;/p&gt;
&lt;p&gt;signals → context → objectives → constraints → execution → feedback&lt;/p&gt;
&lt;p&gt;Where:&lt;/p&gt;
&lt;p&gt;signals detect change&lt;br&gt;
context defines meaning&lt;br&gt;
objectives define what matters&lt;br&gt;
constraints define boundaries&lt;br&gt;
execution performs actions&lt;br&gt;
feedback drives learning  &lt;/p&gt;
&lt;p&gt;✖&lt;/p&gt;
&lt;p&gt;A question to practitioners:&lt;/p&gt;
&lt;p&gt;Which of these layers&lt;br&gt;
are already explicitly implemented&lt;br&gt;
in your systems?&lt;/p&gt;
&lt;p&gt;And in what form?&lt;/p&gt;
&lt;p&gt;Are they:&lt;/p&gt;
&lt;p&gt;centralized&lt;br&gt;
scattered&lt;br&gt;
implicit&lt;br&gt;
formalized  &lt;/p&gt;
&lt;p&gt;✖&lt;/p&gt;
&lt;p&gt;Curious how others structure this today.&lt;/p&gt;
&lt;p&gt;✖ ✖ ✖ ✖ ✖ ✖ ✖&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13122/decision-stack</guid>
<pubDate>Wed, 18 Mar 2026 06:29:23 +0000</pubDate>
</item>
<item>
<title>Modern Enterprise HR Is a Distributed System</title>
<link>https://coderlegion.com/11173/modern-enterprise-hr-is-a-distributed-system</link>
<description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;A Systems Thinking Perspective&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Most people see HR as administration, policies, forms, and approvals.&lt;/p&gt;
&lt;p&gt;But inside large organizations, HR operations behave much more like distributed systems than administrative departments.&lt;/p&gt;
&lt;p&gt;And if you don’t approach them architecturally, inefficiency becomes structural.&lt;/p&gt;
&lt;h3 id=&quot;enterprise-processes-are-systems&quot;&gt;Enterprise Processes Are Systems&lt;/h3&gt;
&lt;p&gt;In software engineering, we think in terms of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Inputs&lt;/li&gt;
&lt;li&gt;Logic&lt;/li&gt;
&lt;li&gt;Dependencies&lt;/li&gt;
&lt;li&gt;Outputs&lt;/li&gt;
&lt;li&gt;Failure points&lt;/li&gt;
&lt;li&gt;Large-scale enterprise operations follow the same pattern.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;An employee lifecycle workflow may appear simple: Onboarding → Payroll → Benefits → Exit&lt;/p&gt;
&lt;p&gt;But under the surface, it involves:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Conditional logic&lt;/li&gt;
&lt;li&gt;Policy variations&lt;/li&gt;
&lt;li&gt;Data validation layers&lt;/li&gt;
&lt;li&gt;Approval chains&lt;/li&gt;
&lt;li&gt;Historical record dependencies&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The complexity isn’t visible — but it’s real.&lt;/p&gt;
&lt;h3 id=&quot;where-enterprise-operations-usually-struggle&quot;&gt;Where Enterprise Operations Usually Struggle&lt;/h3&gt;
&lt;p&gt;Across industries, common patterns emerge:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Hidden Dependencies&lt;br&gt;
Processes rely on assumptions that aren’t formally documented.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Version Drift&lt;br&gt;
Policies evolve, but legacy data doesn’t always align with updated logic.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Manual Overrides&lt;br&gt;
Temporary fixes become permanent workflow fragments.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Data Fragmentation&lt;br&gt;
Different business units may interpret similar rules differently.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These are not HR problems, They’re architecture problems.&lt;/p&gt;
&lt;h3 id=&quot;the-systems-thinking-shift&quot;&gt;The Systems Thinking Shift&lt;/h3&gt;
&lt;p&gt;When you stop treating enterprise operations as tasks and start viewing them as systems, optimization changes.&lt;/p&gt;
&lt;p&gt;Instead of asking:&lt;/p&gt;
&lt;p&gt;“&lt;strong&gt;How do we process this faster?&lt;/strong&gt;”&lt;/p&gt;
&lt;p&gt;You begin asking:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Where is the structural bottleneck?&lt;/li&gt;
&lt;li&gt;What logic is implicit rather than explicit?&lt;/li&gt;
&lt;li&gt;Which step creates cascading impact downstream?&lt;/li&gt;
&lt;li&gt;Is automation accelerating inefficiency?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That shift transforms operational thinking.&lt;/p&gt;
&lt;h3 id=&quot;automation-is-not-optimization&quot;&gt;Automation Is Not Optimization&lt;/h3&gt;
&lt;p&gt;Many organizations rush to digitize workflows; Digitizing a flawed structure doesn’t fix it, but scales it.&lt;/p&gt;
&lt;p&gt;Optimization starts with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;End-to-end process mapping&lt;/li&gt;
&lt;li&gt;Clear ownership definitions&lt;/li&gt;
&lt;li&gt;Policy logic documentation&lt;/li&gt;
&lt;li&gt;Dependency isolation&lt;/li&gt;
&lt;li&gt;Data integrity validation&lt;/li&gt;
&lt;li&gt;Only then does automation create value.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Why This Matters for Engineers&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Software engineers are trained to think structurally.&lt;/p&gt;
&lt;p&gt;Enterprise environments increasingly require that mindset — even outside traditional technical roles.&lt;/p&gt;
&lt;p&gt;Whether the domain is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HR&lt;/li&gt;
&lt;li&gt;Finance&lt;/li&gt;
&lt;li&gt;Supply chain&lt;/li&gt;
&lt;li&gt;Sustainability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The core principle remains:&lt;br&gt;
Systems thinking reduces complexity before technology reduces effort.&lt;/p&gt;
&lt;h3 id=&quot;final-thought-6&quot;&gt;Final Thought&lt;/h3&gt;
&lt;p&gt;Enterprise efficiency is rarely limited by tools, it’s limited by architecture. And architecture exists far beyond code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Connect with me&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Via LinkedIn:&lt;/em&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/fadydesokysaeedabdelaziz&quot;&gt;https://www.linkedin.com/in/fadydesokysaeedabdelaziz&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Via GitHub:&lt;/em&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/fadydesoky&quot;&gt;https://github.com/fadydesoky&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/11173/modern-enterprise-hr-is-a-distributed-system</guid>
<pubDate>Fri, 06 Feb 2026 08:59:50 +0000</pubDate>
</item>
<item>
<title>Building Research Infrastructure at Scale: Inside Globus's Hybrid SaaS Architecture</title>
<link>https://coderlegion.com/10517/building-research-infrastructure-at-scale-inside-globuss-hybrid-saas-architecture</link>
<description>&lt;p&gt;Research computing faces a unique architectural challenge: how do you build a platform that works across institutional boundaries, handles petabyte-scale data movement, supports diverse storage systems, and maintains fine-grained security controls, all without requiring users to be infrastructure experts?&lt;/p&gt;
&lt;p&gt;Globus, a non-profit service from the University of Chicago, has spent nearly 30 years solving this problem. What started as the Globus Toolkit for grid computing has evolved into a comprehensive platform-as-a-service that now handles 2 petabytes of data transfer daily across 2,600+ institutions in 80+ countries.&lt;/p&gt;
&lt;h3 id=&quot;the-hybrid-architecture&quot;&gt;The Hybrid Architecture&lt;/h3&gt;
&lt;p&gt;The key architectural insight behind Globus is its hybrid model. &quot;We can't do that because all the institutions own their own resources,&quot; explains Rachana Ananthakrishnan, Executive Director of Globus. &quot;We can't sort of take over those resources. We have to figure out how to add valuable services as a layer to that resource.&quot;&lt;/p&gt;
&lt;p&gt;This means Globus doesn't centralize data—instead, it orchestrates movement between endpoints while data flows directly over high-speed research networks (often 100-400 Gbps). The architecture consists of three main components:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Local Agents&lt;/strong&gt; run at institutional sites. The Globus Connect agent handles data operations with plugins for different storage types (POSIX, object stores, tape archives). The Globus Compute agent manages remote function execution on various schedulers (Slurm, PBS, Kubernetes). Action Providers allow integration with custom APIs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Global Management Services&lt;/strong&gt; runs as a multi-tenant SaaS. These services handle orchestration, heuristic-based optimization, reliability guarantees, and maintain the security fabric. But crucially, they maintain only control-plane connections—file data moves directly between endpoints.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Security Fabric&lt;/strong&gt; uses OAuth and OIDC to federate authentication across 2,140+ identity providers. This allows researchers to use institutional credentials without creating new accounts, while maintaining fine-grained authorization across distributed resources.&lt;/p&gt;
&lt;h3 id=&quot;platform-capabilities&quot;&gt;Platform Capabilities&lt;/h3&gt;
&lt;p&gt;Globus offers six core capabilities that can be used independently or composed:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Managed Transfer&lt;/strong&gt; provides fire-and-forget data movement with automatic retry, checksum verification, and progress tracking. &quot;The protocol here continuously sends what are called markers,&quot; Ananthakrishnan explains. &quot;So it says I've moved 5 bytes, 10 bytes, 20 bytes, and suddenly there's no marker. Then this keeps retrying with a backoff algorithm.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Data Sharing&lt;/strong&gt; creates permission overlays on existing storage without staging data. Share with any identity, set time restrictions, and maintain audit logs, all without creating local accounts for collaborators.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Unified Access&lt;/strong&gt; abstracts heterogeneous storage through an open connector ecosystem. The Community Connector Program allows vendors and users to build plugins for any storage system, while Globus handles authentication, translation, and rate limiting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Search&lt;/strong&gt; provides schema-agnostic metadata indexing with dynamic schema detection and fine-grained visibility controls. Think Elasticsearch optimized for research with built-in access control.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Compute&lt;/strong&gt; brings function-as-a-service to on-prem HPC. Submit Python functions that run on remote systems with federated authentication. One COVID-era drug screening project used this to distribute ML workloads across diverse resources, completing training cycles in 31 seconds, compared with much longer traditional approaches.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flows&lt;/strong&gt; offers declarative workflow automation using JSON-based definitions. Event-driven execution with AWS Step Functions under the hood, but extended to securely call outside services and remote systems.&lt;/p&gt;
&lt;h3 id=&quot;real-world-performance&quot;&gt;Real-World Performance&lt;/h3&gt;
&lt;p&gt;The Advanced Photon Source at Argonne National Laboratory demonstrates the platform's capabilities at scale. Multiple beamlines use Globus to automate data movement and processing. &quot;These data services have taken the time to solve a structure from weeks to days and now to hours,&quot; notes beamline scientist Darren Sherrell.&lt;/p&gt;
&lt;p&gt;For developers, Globus provides comprehensive REST APIs and SDKs. The platform is extensible—you're not locked into the web interface. The Franklin &amp;amp; Marshall College biology department even built custom flows that allow field volunteers to click a button, fill a questionnaire, and trigger automated processing. &quot;It really increased the community participation,&quot; Ananthakrishnan notes. &quot;In place of teaching all his volunteers how to do these 10 things, he defined a flow.&quot;&lt;/p&gt;
&lt;h3 id=&quot;compliance-and-security&quot;&gt;Compliance and Security&lt;/h3&gt;
&lt;p&gt;For protected data, Globus implements NIST 800-53 and 800-171 controls, signs BAAs for HIPAA, and complies with GDPR and data protection agreements. The architecture supports both personal data (visible only to the user) and shared data (visible across institutions) with appropriate access controls.&lt;/p&gt;
&lt;p&gt;The security model is shared: Globus controls the platform layer, while institutions control their endpoints. &quot;It's a hybrid SaaS, it's a shared security model,&quot; Ananthakrishnan explains. &quot;We do some controls, they have to do some controls.&quot;&lt;/p&gt;
&lt;h3 id=&quot;getting-started&quot;&gt;Getting Started&lt;/h3&gt;
&lt;p&gt;Globus offers a freemium model, basic features are free for non-profit research. Developers can start with the web interface or dive directly into the APIs. Documentation is available at docs.globus.org, and the platform includes a 90-day trial for qualified projects.&lt;/p&gt;
&lt;p&gt;For architects building research platforms, Globus provides a reference architecture for handling distributed data at scale. The hybrid model, federated authentication, and connector ecosystem offer lessons applicable beyond scientific computing to any scenario requiring secure data movement across organizational boundaries.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/10517/building-research-infrastructure-at-scale-inside-globuss-hybrid-saas-architecture</guid>
<pubDate>Mon, 26 Jan 2026 21:26:10 +0000</pubDate>
</item>
<item>
<title>Why AI Startups Need to Focus on Distribution Before Disruption</title>
<link>https://coderlegion.com/8663/why-ai-startups-need-to-focus-on-distribution-before-disruption</link>
<description>&lt;p&gt;There’s a familiar story playing out across the AI startup world.&lt;/p&gt;
&lt;p&gt;A founder builds something technically impressive.&lt;br&gt;
The model works.&lt;br&gt;
The demo looks sharp.&lt;br&gt;
Early users are intrigued.&lt;/p&gt;
&lt;p&gt;And then… nothing scales.&lt;/p&gt;
&lt;p&gt;Not because the product isn’t good. But because disruption without distribution doesn’t go anywhere.&lt;/p&gt;
&lt;p&gt;This is one of the most consistent mistakes I see AI startups make today.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Myth: “If the Tech Is Good Enough, Adoption Will Follow”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This belief has deep roots in startup culture.&lt;/p&gt;
&lt;p&gt;Build something meaningfully better.&lt;br&gt;
Ship it.&lt;br&gt;
Let the market discover it.&lt;/p&gt;
&lt;p&gt;That logic worked in earlier software cycles, when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;competition was limited&lt;/li&gt;
&lt;li&gt;attention was less fragmented&lt;/li&gt;
&lt;li&gt;switching costs were lower&lt;/li&gt;
&lt;li&gt;novelty carried more weight&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In AI, that world no longer exists.&lt;/p&gt;
&lt;p&gt;Today, technical capability is abundant.&lt;br&gt;
Attention is scarce.&lt;/p&gt;
&lt;p&gt;And distribution, not disruption, is the real bottleneck.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why AI Products Are Especially Vulnerable Without Distribution&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;AI products face a unique challenge:&lt;/p&gt;
&lt;p&gt;They often look similar from the outside.&lt;/p&gt;
&lt;p&gt;To a user:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;multiple tools claim “AI-powered”&lt;/li&gt;
&lt;li&gt;outputs feel interchangeable&lt;/li&gt;
&lt;li&gt;differentiation is hard to see quickly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That means adoption is driven less by technical depth and more by:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;where people discover the product&lt;/li&gt;
&lt;li&gt;how often they encounter it&lt;/li&gt;
&lt;li&gt;how easily it fits into existing habits&lt;/li&gt;
&lt;li&gt;how much they trust the source&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without intentional distribution, even strong AI products remain invisible.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Disruption Is a Product Story. Distribution Is a Behaviour Story.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Disruption answers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What’s new?&lt;/li&gt;
&lt;li&gt;What’s better?&lt;/li&gt;
&lt;li&gt;What’s different?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Distribution answers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Where do people already spend time?&lt;/li&gt;
&lt;li&gt;How do they make decisions?&lt;/li&gt;
&lt;li&gt;Who do they trust?&lt;/li&gt;
&lt;li&gt;What triggers action?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most AI startups obsess over the first set of questions and ignore the second.&lt;/p&gt;
&lt;p&gt;That imbalance is fatal.&lt;/p&gt;
&lt;p&gt;Because products don’t spread through features. They spread through behavioural alignment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What Distribution Actually Means in the AI Era&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Many founders hear “distribution” and immediately think:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ads&lt;/li&gt;
&lt;li&gt;growth hacks&lt;/li&gt;
&lt;li&gt;viral loops&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s surface-level thinking.&lt;/p&gt;
&lt;p&gt;Real distribution is about being present at the moment of need.&lt;/p&gt;
&lt;p&gt;For AI startups, that often means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;embedding into existing workflows&lt;/li&gt;
&lt;li&gt;integrating with tools people already use&lt;/li&gt;
&lt;li&gt;educating the market consistently&lt;/li&gt;
&lt;li&gt;becoming a trusted voice, not just a tool&lt;/li&gt;
&lt;li&gt;showing up before the buying decision is made&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In other words, distribution is not a channel. It’s a system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why Waiting to “Figure Out Distribution Later” Rarely Works&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I often hear:&lt;br&gt;
“Let’s build first. We’ll worry about distribution once it’s ready.”&lt;/p&gt;
&lt;p&gt;In AI, that’s backwards.&lt;/p&gt;
&lt;p&gt;Because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;users don’t know what they need yet&lt;/li&gt;
&lt;li&gt;workflows are still forming&lt;/li&gt;
&lt;li&gt;mental models are still unstable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The startups that win are the ones that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;shape the conversation early&lt;/li&gt;
&lt;li&gt;educate their audience&lt;/li&gt;
&lt;li&gt;define the problem before selling the solution&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Distribution is not a post-launch activity. It’s part of product design.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Quiet Advantage of Distribution-First Teams&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Teams that lead with distribution tend to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;build clearer products&lt;/li&gt;
&lt;li&gt;simplify messaging&lt;/li&gt;
&lt;li&gt;focus on real use cases&lt;/li&gt;
&lt;li&gt;receive better feedback&lt;/li&gt;
&lt;li&gt;iterate in the right direction&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Why?&lt;/p&gt;
&lt;p&gt;Because when you’re close to users early, you don’t build in isolation.&lt;/p&gt;
&lt;p&gt;You don’t guess what matters. You observe it.&lt;/p&gt;
&lt;p&gt;This feedback loop often matters more than another model upgrade.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Disruption Without Distribution Creates Fragile Companies&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;AI startups that rely only on technical disruption often end up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;chasing constant feature upgrades&lt;/li&gt;
&lt;li&gt;reacting to competitor releases&lt;/li&gt;
&lt;li&gt;struggling with retention&lt;/li&gt;
&lt;li&gt;burning trust through over-promising&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In contrast, distribution-led companies build:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;familiarity&lt;/li&gt;
&lt;li&gt;trust&lt;/li&gt;
&lt;li&gt;mindshare&lt;/li&gt;
&lt;li&gt;long-term relevance&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;They don’t need to scream “disruption.” They become the default choice quietly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What This Means for AI Founders Right Now&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you’re building in AI today, the strategic order matters.&lt;/p&gt;
&lt;p&gt;Not:&lt;br&gt;
Disrupt → Build → Distribute&lt;/p&gt;
&lt;p&gt;But:&lt;br&gt;
Distribute → Learn → Build → Deepen&lt;/p&gt;
&lt;p&gt;This doesn’t mean lowering technical ambition. It means aligning it with reality.&lt;/p&gt;
&lt;p&gt;The strongest AI companies of the next decade won’t be the ones with the most impressive demos.&lt;/p&gt;
&lt;p&gt;They’ll be the ones that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;understood their audience early&lt;/li&gt;
&lt;li&gt;earned attention before asking for adoption&lt;/li&gt;
&lt;li&gt;built trust before asking for behaviour change&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The Real Takeaway&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Disruption gets headlines.&lt;br&gt;
Distribution builds companies.&lt;/p&gt;
&lt;p&gt;In the AI era, where intelligence is becoming cheaper and faster to access, attention, trust, and habit formation are the real moats.&lt;/p&gt;
&lt;p&gt;If no one knows you exist or understands why you matter, your disruption doesn’t matter either.&lt;/p&gt;
&lt;p&gt;Focus on distribution first.&lt;br&gt;
The disruption will actually land.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Next Article:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;“The Real Reason Most Developers Are Misusing Generative AI.”&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/8663/why-ai-startups-need-to-focus-on-distribution-before-disruption</guid>
<pubDate>Sun, 21 Dec 2025 03:05:35 +0000</pubDate>
</item>
<item>
<title>Oracle migrations reveal hidden technical debt. Here's what developers need to know before moving.</title>
<link>https://coderlegion.com/6394/oracle-migrations-reveal-hidden-technical-debt-heres-what-developers-need-before-moving</link>
<description>&lt;h2 id=&quot;the-real-cost-of-oracle-technical-debt-what-developers-learn-during-migration&quot;&gt;The Real Cost of Oracle Technical Debt: What Developers Learn During Migration&lt;/h2&gt;
&lt;p&gt;Oracle databases have been around for 40 years. That's a lot of time to accumulate technical debt.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/spencerwkimball/&quot;&gt;Spencer Kimball&lt;/a&gt;, CEO of &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.cockroachlabs.com/&quot;&gt;Cockroach Labs&lt;/a&gt;, spends his days helping Fortune 100 companies migrate off Oracle. The technical challenges he sees are consistent: undocumented stored procedures, excessive use of legacy patterns, and architectural decisions made when networking was slow and security wasn't a priority.&lt;/p&gt;
&lt;p&gt;&quot;The people that wrote them are gone from our company,&quot; Kimball says of the legacy applications his customers want to migrate. &quot;We want help solving this.&quot;&lt;/p&gt;
&lt;h3 id=&quot;postgresql-is-eating-the-world&quot;&gt;PostgreSQL is Eating the World&lt;/h3&gt;
&lt;p&gt;The first thing to understand about modern database migrations is that developers don't want Oracle's dialect anymore. They want PostgreSQL.&lt;/p&gt;
&lt;p&gt;&quot;Postgres is actively eating the world,&quot; Kimball says. When Cockroach Labs started 11 years ago, PostgreSQL wasn't the default choice. Now it is.&lt;/p&gt;
&lt;p&gt;This shift shows up everywhere. AWS Aurora moved its center of gravity from MySQL to PostgreSQL. Even TiDB, which started as a MySQL engine, recently added PostgreSQL support. IBM's customers are asking for &quot;a Postgres database for the enterprise.&quot;&lt;/p&gt;
&lt;p&gt;The reason is straightforward: PostgreSQL is the standard for new development. If you're building something new, you're building it on Postgres.&lt;/p&gt;
&lt;h3 id=&quot;distributed-vs-monolithic-where-developers-get-surprised&quot;&gt;Distributed vs Monolithic: Where Developers Get Surprised&lt;/h3&gt;
&lt;p&gt;The migration from Oracle to a distributed database like CockroachDB involves more than syntax changes. It requires a shift in thinking.&lt;/p&gt;
&lt;p&gt;&quot;Databases have grown up over the last 50 years with this perspective that the database is a tightly coupled monolithic system,&quot; Kimball explains. Oracle was built for InfiniBand networks and specialized hardware. Those requirements don't match cloud reality.&lt;/p&gt;
&lt;p&gt;The most common surprise? Auto-incrementing IDs.&lt;/p&gt;
&lt;p&gt;In a monolithic database, generating sequential IDs is fast because everything runs on one machine. In a distributed system with consensus requirements, that pattern creates bottlenecks.&lt;/p&gt;
&lt;p&gt;The solution is UUID generation with node IDs embedded. These IDs are unique across the cluster, sort correctly, and create zero hot spots. It's a small change, but every developer on a migration team needs to understand it.&lt;/p&gt;
&lt;p&gt;&quot;Some of this is solved just by retraining developers,&quot; Kimball says. &quot;Some of it is done by tooling that will look through a schema and say, 'Here's an auto-incrementing ID. We recommend you replace it with UUID generation.'&quot;&lt;/p&gt;
&lt;h3 id=&quot;ai-is-unraveling-oracle-lock-in&quot;&gt;AI is Unraveling Oracle Lock-In&lt;/h3&gt;
&lt;p&gt;Stored procedures represent Oracle's most effective lock-in mechanism. They were a reasonable architectural choice when network latency was high and database security was limited. DBAs pushed application logic into the database where it could be controlled and protected.&lt;/p&gt;
&lt;p&gt;The result is mountains of undocumented code. Sometimes it's PL/SQL. Sometimes it's C routines with no unit tests and no documentation.&lt;/p&gt;
&lt;p&gt;AI tools are changing the migration economics. Kimball says AI can analyze legacy stored procedures, generate documentation, create unit tests, and suggest complete migrations to PL/pgSQL (PostgreSQL's procedural language).&lt;/p&gt;
&lt;p&gt;&quot;The sharpest decrease in costs is going to come from AI,&quot; he says. Where migrations used to require 100+ consultants over three years at a cost of $10 million, AI-assisted approaches can reduce that significantly.&lt;/p&gt;
&lt;p&gt;The tools from Cognition, Devin, and even Claude can handle much of the heavy lifting that previously required specialized Oracle knowledge.&lt;/p&gt;
&lt;h3 id=&quot;architecture-built-for-ai-scale&quot;&gt;Architecture Built for AI Scale&lt;/h3&gt;
&lt;p&gt;The architectural differences between distributed and monolithic databases matter more as AI workloads increase.&lt;/p&gt;
&lt;p&gt;&quot;You can't even get there without a distributed architecture,&quot; Kimball says about handling AI-scale traffic. The issue isn't just throughput. It's cost.&lt;/p&gt;
&lt;p&gt;Cockroach Labs routinely runs 300-node test clusters for weeks at a time. These clusters contain petabytes of data. Three weeks used to cost $500,000.&lt;/p&gt;
&lt;p&gt;Now they run these tests on spot instances and save 80%. &quot;Could you run a traditional database across spot instances?&quot; Kimball asks. &quot;You'd be down most of the time.&quot;&lt;/p&gt;
&lt;p&gt;A distributed database built for resilience can handle spot instances getting preempted. It replicates, rebalances, and keeps running through the chaos.&lt;/p&gt;
&lt;p&gt;The company is targeting a 10x cost improvement through a combination of optimizations: spot instances, multi-tenancy (packing multiple workloads on the same physical database), layered storage (S3, locally attached SSDs, EBS), and continuous performance optimization.&lt;/p&gt;
&lt;p&gt;They've already achieved 2x performance improvement on the same hardware this year.&lt;/p&gt;
&lt;h3 id=&quot;resilience-under-pressure&quot;&gt;Resilience Under Pressure&lt;/h3&gt;
&lt;p&gt;Performance benchmarks under ideal conditions don't tell you much about production reality. Cockroach Labs released a &quot;Performance under Adversity&quot; benchmark that measures how databases behave during chaos: pod failures, network partitions, disk stalls.&lt;/p&gt;
&lt;p&gt;They tested CockroachDB against Oracle GDD 23ai (the first version of Oracle that can support GDD in a Raft configuration). The results are documented in detail, and the benchmark methodology is open so anyone can reproduce it.&lt;/p&gt;
&lt;p&gt;CockroachDB encountered 56 errors resulting in 51 seconds of downtime across all chaos tests. Oracle GDD encountered 300 errors resulting in 369 seconds of downtime (over six minutes).&lt;/p&gt;
&lt;p&gt;That's 5x more resilient.&lt;/p&gt;
&lt;p&gt;The difference matters most during &quot;grey failures&quot;—those problems that don't immediately take the database offline but slowly degrade latency and breach SLAs.&lt;/p&gt;
&lt;h3 id=&quot;the-migration-path-forward&quot;&gt;The Migration Path Forward&lt;/h3&gt;
&lt;p&gt;If you're stuck on Oracle and planning a migration, here's what to focus on:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Audit your stored procedures.&lt;/strong&gt; Use AI tools to document what you have before you start planning the migration. You probably have more technical debt than you think.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Understand distributed architecture patterns.&lt;/strong&gt; Auto-incrementing IDs are just one example. Get your team trained on how distributed consensus works and where the architectural differences matter.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test for resilience, not just performance.&lt;/strong&gt; Peak performance under ideal conditions matters less than consistent performance under realistic failure scenarios.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Factor in the full cost.&lt;/strong&gt; Don't just compare license fees. Include the cost of downtime, the complexity of running the database, and the total cost of ownership over time.&lt;/p&gt;
&lt;p&gt;The cloud forced many companies to reconsider Oracle. AI is accelerating that shift. If you're going to modernize anyway—and you probably are—make sure you understand the technical implications before you start.&lt;/p&gt;
</description>
<category>Data Engineering</category>
<guid isPermaLink="true">https://coderlegion.com/6394/oracle-migrations-reveal-hidden-technical-debt-heres-what-developers-need-before-moving</guid>
<pubDate>Mon, 13 Oct 2025 21:25:28 +0000</pubDate>
</item>
<item>
<title>New York startup replaces three media tools with one platform using FUSE, S3, and custom AI models.</title>
<link>https://coderlegion.com/6364/new-york-startup-replaces-three-media-tools-with-one-platform-using-fuse-and-custom-models</link>
<description>&lt;h2 id=&quot;how-shade-built-a-file-system-that-actually-works-for-remote-creative-teams&quot;&gt;How Shade Built a File System That Actually Works for Remote Creative Teams&lt;/h2&gt;
&lt;p&gt;Media production teams have a storage problem. They use Lucid Link for file streaming, Frame.io for review and approval, and Iconik for asset management. That's three subscriptions, three login screens, and constant file transfers between systems.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://shade.inc/&quot;&gt;Shade&lt;/a&gt; thinks this is ridiculous. The New York-based startup built a single platform that handles ingestion, storage, review, and delivery. After raising $5 million from General Catalyst and Signal Fire, they now serve 94 customers and project $10 million in revenue next year.&lt;/p&gt;
&lt;p&gt;I recently met with Shade during the 64th &lt;a rel=&quot;nofollow&quot; href=&quot;http://www.itpresstour.net&quot;&gt;IT Press Tour&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-technical-stack&quot;&gt;The Technical Stack&lt;/h3&gt;
&lt;p&gt;Shade's architecture sits on top of S3-compatible storage. Customers can bring their own Wasabi, AWS, or Google Cloud Storage buckets, or use Shade's managed storage. Either way, the raw files stay in object storage while Shade's metadata service runs separately on AWS.&lt;/p&gt;
&lt;p&gt;The desktop application uses FUSE (Filesystem in Userspace) on Mac and Linux, with similar technology on Windows. When you mount a Shade workspace, it appears as a local drive. Behind the scenes, the FUSE layer intercepts file system calls and translates them into S3 requests.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/brandonfan1256/&quot;&gt;Brandon Fan&lt;/a&gt;, Shade's co-founder and CEO, explained the approach: &quot;We have a CDN infrastructure in front of S3 that speeds up access to various locations. We also have local upload capability—it uploads to the nearest server, then syncs to the origin server.&quot;&lt;/p&gt;
&lt;p&gt;This architecture supports parallel chunk-based uploads and downloads. The result is fast enough that teams can stream 4K video without downloading entire files first.&lt;/p&gt;
&lt;h3 id=&quot;custom-ai-for-massive-scale&quot;&gt;Custom AI for Massive Scale&lt;/h3&gt;
&lt;p&gt;The AI components required more custom work than expected. Shade uses OpenAI's vision models for metadata generation—things like identifying shot types, lighting conditions, and visible logos. But the search engine is completely custom.&lt;/p&gt;
&lt;p&gt;&quot;We had to build our own semantic search,&quot; Fan said. &quot;Most traditional models are too slow for millions of assets. We took open-source vision models and distilled them to run on CPU infrastructure instead of GPUs. This keeps costs down while letting users search through hundreds of thousands of assets.&quot;&lt;/p&gt;
&lt;p&gt;The search works across images, videos, and documents. Type &quot;people playing cards in a park&quot; and it finds that exact scene. The system also handles transcription (via Assembly AI) and facial recognition.&lt;/p&gt;
&lt;p&gt;For clustering and organizing large asset libraries, Shade built another custom algorithm. Standard clustering falls apart at scale, so they engineered a solution that handles the volume their customers generate.&lt;/p&gt;
&lt;h3 id=&quot;file-locking-and-conflicts&quot;&gt;File Locking and Conflicts&lt;/h3&gt;
&lt;p&gt;Creative applications like Premiere Pro have built-in project locking. For everything else, Shade creates conflict files when two users modify the same asset. If you rename a folder while someone saves a file into it, Shade preserves both actions and flags the conflict.&lt;/p&gt;
&lt;p&gt;The system maintains a multi-tenant database that stores all metadata, permissions, and file relationships. When a client requests a file, it queries this service to get the S3 URL, then streams the data from object storage.&lt;/p&gt;
&lt;h3 id=&quot;integration-points&quot;&gt;Integration Points&lt;/h3&gt;
&lt;p&gt;Shade offers webhook-based automation through a Zapier-style interface. Teams connect Shade to their project management tools, CRMs, or custom applications. The API exposes all metadata and file operations.&lt;/p&gt;
&lt;p&gt;The platform also supports SSO via SAML, SCIM provisioning, and audit logging. Permissions work at three levels: workspace, individual users, and link-based sharing. You can share files with external collaborators through password-protected links that expire on a schedule.&lt;/p&gt;
&lt;h3 id=&quot;developer-background&quot;&gt;Developer Background&lt;/h3&gt;
&lt;p&gt;Fan comes from an information retrieval research background, publishing papers on search technology before the transformer era. His co-founder Emerson, the CTO, is a roboticist who built the distributed file system infrastructure.&lt;/p&gt;
&lt;p&gt;The team includes engineers from Lucid Link and Frame.io—people who know the competitive landscape and customer pain points firsthand. Most of the 15-person team works from the New York office to maintain tight collaboration between engineering and product.&lt;/p&gt;
&lt;h3 id=&quot;market-position&quot;&gt;Market Position&lt;/h3&gt;
&lt;p&gt;Frame.io sold to Adobe for $1.2 billion in 2021. Iconik raised $80 million. Shade raised $5 million and competes directly with both.&lt;/p&gt;
&lt;p&gt;The cost advantage matters. A team using 10 seats and 10TB on Frame.io plus another 10TB on Lucid Link pays significantly more than the equivalent Shade setup. Fan claims Shade typically runs 30% cheaper than the combined stack it replaces.&lt;/p&gt;
&lt;p&gt;Sales cycles run fast—usually a seven-day trial followed by contract signing. Procurement and legal reviews take longer, but the technical evaluation wraps quickly. Most deals close within a month of first contact.&lt;/p&gt;
&lt;p&gt;The current average contract value sits around $10,000 to $15,000 annually for a 10-user, 25TB setup. Growth comes primarily through inbound traffic from Google searches and customer referrals.&lt;/p&gt;
&lt;h3 id=&quot;what-s-next&quot;&gt;What's Next&lt;/h3&gt;
&lt;p&gt;Shade plans to double the team this year, with most new hires in engineering. The roadmap includes sub-clipping (searching within video clips), expanded document workflows, and an app marketplace where third parties can build on the platform.&lt;/p&gt;
&lt;p&gt;The vision echoes what Salesforce did for CRM and Notion did for documents—become the default system of record, but for files. Whether that happens depends on execution. But the technical foundation is solid, and the team has proven they can ship fast.&lt;/p&gt;
&lt;p&gt;For engineering teams tired of duct-taping together file storage solutions, Shade offers a cleaner option. One platform, one API, one place where files live and teams collaborate.&lt;/p&gt;
</description>
<category>Collaboration Tools</category>
<guid isPermaLink="true">https://coderlegion.com/6364/new-york-startup-replaces-three-media-tools-with-one-platform-using-fuse-and-custom-models</guid>
<pubDate>Mon, 13 Oct 2025 13:20:57 +0000</pubDate>
</item>
<item>
<title>AuriStor rebuilt the Andrew File System to fix decades of technical debt and performance issues.</title>
<link>https://coderlegion.com/6218/auristor-rebuilt-the-andrew-file-system-fix-decades-technical-debt-and-performance-issues</link>
<description>&lt;h2 id=&quot;auristor-tackles-distributed-file-system-performance-with-protocol-level-improvements&quot;&gt;AuriStor Tackles Distributed File System Performance With Protocol-Level Improvements&lt;/h2&gt;
&lt;p&gt;Most developers don't think much about distributed file systems until they hit a wall. You're moving files across continents, dealing with spotty connections, or trying to sync data for thousands of compute nodes. That's when you realize the infrastructure matters.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.auristor.com/&quot;&gt;AuriStor&lt;/a&gt; spent the past decade rebuilding the Andrew File System. They forked from OpenAFS in 2012 and focused on what they call &quot;paying off technical debt.&quot; The result is AuriStorFS, a commercial product that prioritizes performance and security over backward compatibility.&lt;/p&gt;
&lt;p&gt;The company's approach is technical, not trendy. They identified bottlenecks in the RX remote procedure call protocol and systematically addressed them. Senior engineer Simon Wilkinson presented results showing a 10x performance improvement—the time to transfer 10GB of data dropped from 437,500 milliseconds to under 50,000 milliseconds using multiple threads.&lt;/p&gt;
&lt;h3 id=&quot;protocol-improvements-that-matter&quot;&gt;Protocol improvements that matter&lt;/h3&gt;
&lt;p&gt;RX is a UDP-based RPC protocol. It's not new or exciting, but it works. AuriStor's team analyzed every aspect of how packets move through the system and found ways to optimize.&lt;/p&gt;
&lt;p&gt;They increased the default window size from 32 packets to 8,192 packets. A larger window means more data in flight at once. On a 1ms round-trip link, this bumps theoretical throughput from 362 Mbit/sec to 92 Gbit/sec.&lt;/p&gt;
&lt;p&gt;Window size alone doesn't solve everything. The team implemented RFC-compliant congestion control algorithms, including New Reno and SACK-based loss recovery. They added RACK-TLP for better tail loss detection. These aren't radical ideas—they're proven TCP techniques adapted for RX.&lt;/p&gt;
&lt;p&gt;Path MTU discovery now works properly. The system starts with a conservative 1,200-byte MTU and probes upward. If your network supports jumbo frames, AuriStor uses them. This cuts packet overhead significantly.&lt;/p&gt;
&lt;h3 id=&quot;real-world-performance-gains&quot;&gt;Real-world performance gains&lt;/h3&gt;
&lt;p&gt;One government research organization moved its AFS deployment to the cloud and measured the results. Copying a 1GB file from local disk to AFS took 3 minutes and 11 seconds with OpenAFS servers. With AuriStor's 2021 release, it dropped to 1 minute. The latest release with path MTU discovery and proportional rate reduction cut it to 30 seconds.&lt;/p&gt;
&lt;p&gt;These improvements scale. Production AuriStor file servers handle over 500,000 simultaneous connections from 40,000 cache managers. One deployment manages 1.7 million volumes, with shutdown times reduced from 30 minutes to 4 seconds.&lt;/p&gt;
&lt;p&gt;A large financial institution (operating under NDA) runs 80 cells with 300 servers serving 175,000 clients. They deploy software updates continuously—new releases push out every few minutes. The infrastructure handles 1.5 million volumes across 180 to 200 regional cells globally, distributed across multiple cloud providers.&lt;/p&gt;
&lt;h3 id=&quot;security-without-the-theater&quot;&gt;Security without the theater&lt;/h3&gt;
&lt;p&gt;The security model is straightforward. AuriStorFS uses GSS-API with Kerberos v5 for authentication and AES-256 for encryption. The YFS-RxGK security class supports combined identity authentication—both user and machine identities are verified together.&lt;/p&gt;
&lt;p&gt;This matters for organizations that need to control where data gets accessed. You're not just &quot;you&quot;—you're &quot;you on your phone,&quot; &quot;you on your company laptop,&quot; &quot;you on your personal laptop.&quot; You can grant permissions to a user only when they're on a specific machine.&lt;/p&gt;
&lt;p&gt;Volume-level security policies let administrators enforce encryption requirements. Volumes can only move to servers with equal or stronger security policies. File servers can require specific authentication levels before serving data. Maximum access control lists prevent users from accidentally exposing data, even if they set overly permissive permissions on individual files.&lt;/p&gt;
&lt;h3 id=&quot;the-perpetual-license-model&quot;&gt;The perpetual license model&lt;/h3&gt;
&lt;p&gt;AuriStor's pricing is unusual. Base cost is $21,000 per year per cell, covering up to 4 servers and 1,000 user or machine IDs. Additional servers cost $1,000 to $2,500 each. There's no charge based on storage capacity or data volume. As &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/jeffreyaltman/&quot;&gt;Jeffrey Altman&lt;/a&gt;, Founder and CEO&lt;br&gt;
put it during the presentation to the 64th &lt;a rel=&quot;nofollow&quot; href=&quot;http://www.itpresstour.net&quot;&gt;IT Press Tour&lt;/a&gt;, &quot;We give you the first 100 exabytes for free.&quot;&lt;/p&gt;
&lt;p&gt;The license is perpetual. Stop paying and you keep using the version you licensed. You just don't get updates or support. Security patches continue for at least two years after each release.&lt;/p&gt;
&lt;p&gt;For developers working on large-scale deployments, this model is predictable. Your costs don't explode when you add storage.&lt;/p&gt;
&lt;h3 id=&quot;container-integration-happening-now&quot;&gt;Container integration happening now&lt;/h3&gt;
&lt;p&gt;AuriStor built a CSI driver for Kubernetes that works with Red Hat OpenShift. Their large financial customer is using it to transition 180,000 systems to containers without losing the benefits of AFS.&lt;/p&gt;
&lt;p&gt;The challenge: container images for machine learning applications start at 40 gigabytes. You don't need all that data to run the application—only a fraction gets executed. Copying 40 gigabytes to expensive compute instances wastes time and money.&lt;/p&gt;
&lt;p&gt;AuriStorFS caches only the regions of files that applications actually access. The CSI driver lets containers mount AFS volumes directly. Applications execute binaries from the AFS namespace instead of packaging them into container images.&lt;/p&gt;
&lt;p&gt;One customer has software distribution trees that are hundreds of gigabytes. With updates every few minutes, pushing new container images to 50,000 clients is impossible. AuriStorFS replicates only the incremental changes.&lt;/p&gt;
&lt;p&gt;The Linux kernel's built-in AFS client (kafs) supports AuriStorFS protocols. It's distributed in Fedora, Ubuntu, Debian, and RHEL 9.2 and later. Last night, the team successfully booted a Linux system from AFS for the first time using mainline kernel tools.&lt;/p&gt;
&lt;h3 id=&quot;technical-trade-offs&quot;&gt;Technical trade-offs&lt;/h3&gt;
&lt;p&gt;AuriStorFS doesn't support server-side byte range locks yet. This means it's not suitable for databases or virtual machine images. That's on the roadmap but won't arrive before 2026.&lt;/p&gt;
&lt;p&gt;The system works best for read-heavy workloads with occasional writes. Think software distribution, research data, and home directories. It handles files up to 16 exabytes, though real-world deployments use files in the 5TB to 50TB range regularly.&lt;/p&gt;
&lt;p&gt;Mixed deployments with OpenAFS work but lose some features. You can run both, but you're limited to the lowest common denominator.&lt;/p&gt;
&lt;h3 id=&quot;small-team-focused-mission&quot;&gt;Small team, focused mission&lt;/h3&gt;
&lt;p&gt;AuriStor has a core team of five developers, the same people who worked on OpenAFS before the fork. They're augmented by about five part-time contractors with specialized skills.&lt;/p&gt;
&lt;p&gt;The company is sub-$2 million in annual revenue but profitable. Altman didn't take a salary from 2007 to 2016. The company received SBIR grants from the Department of Energy, sponsored by SLAC and Fermilab.&lt;/p&gt;
&lt;p&gt;More than 50% of their engineering effort goes into network performance. As Altman said, &quot;If we can't make the network fast, it doesn't matter what anything else does.&quot;&lt;/p&gt;
&lt;h3 id=&quot;source-code-is-walled&quot;&gt;Source code is walled&lt;/h3&gt;
&lt;p&gt;AuriStor keeps source code private. Customers can license access to repositories, test infrastructure, and participate in development. If AuriStor goes out of business, source licensees gain the right to create public forks.&lt;/p&gt;
&lt;p&gt;This is a deliberate choice after years of watching OpenAFS struggle with the free rider problem. Organizations would use the software, but not fund development. The trade-off is commercial sustainability versus open collaboration. AuriStor chose sustainability.&lt;/p&gt;
&lt;p&gt;For shops considering distributed file systems, AuriStorFS offers measurable improvements over OpenAFS. The question is whether the licensing model and closed source approach fit your organization. If you need global data access with predictable costs and don't want storage-based pricing, it's worth evaluating.&lt;/p&gt;
</description>
<category>IDEs &amp; Code Editors</category>
<guid isPermaLink="true">https://coderlegion.com/6218/auristor-rebuilt-the-andrew-file-system-fix-decades-technical-debt-and-performance-issues</guid>
<pubDate>Thu, 09 Oct 2025 23:57:38 +0000</pubDate>
</item>
</channel>
</rss>