<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Distributed Systems | Law Zava</title><link>https://lawzava.com/topics/distributed-systems/</link><description>Consistency, partial failure, coordination, and the tradeoffs that appear the moment one process becomes two.</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Thu, 13 Aug 2026 07:51:57 +0000</lastBuildDate><atom:link href="https://lawzava.com/topics/distributed-systems/index.xml" rel="self" type="application/rss+xml"/><item><title>De-Risking the Black Swan: Red-Teaming Distributed Databases Before Production</title><link>https://lawzava.com/blog/2026-03-16-de-risking-black-swan-distributed-databases/</link><pubDate>Mon, 16 Mar 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-03-16-de-risking-black-swan-distributed-databases/</guid><description>Red-teaming distributed databases before production: most catastrophic failures are compound scenarios nobody practiced, not black swans.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Most catastrophic database incidents aren&rsquo;t novel. They&rsquo;re compounded failures that nobody practiced for. The node-failure test passes, so the team moves on. Then a network partition hits during a  <a href="/blog/2016-08-15-database-migrations-without-downtime/"
   
   >schema migration</a>
 while the on-call engineer is handling an unrelated alert, and suddenly you&rsquo;re in territory no runbook covers. Structured red-teaming exposes these compound paths before they become customer-visible outages. It costs a fraction of what a single bad incident costs.</p>
<h2 id="black-swans-vs-ignored-knowns">Black Swans vs. Ignored Knowns</h2>
<p>The term &ldquo;black swan&rdquo; gets overused in infrastructure. Most catastrophic database failures are not genuinely unpredictable. They are known failure modes that compound in ways nobody tested.</p>
<p>Consider the canonical distributed database incident: a network partition isolates a minority of nodes, those nodes continue accepting writes because the partition detection is slow, the partition heals, and now you have conflicting data that the conflict resolution logic wasn&rsquo;t designed to handle at that volume. Every component in this chain is well-understood. The failure isn&rsquo;t in any single component. It&rsquo;s in the interaction between them under specific timing conditions.</p>
<p>The honest term for most &ldquo;black swan&rdquo; database incidents is &ldquo;ignored known.&rdquo; The team knew partitions could happen. They knew conflict resolution had edge cases. They knew detection wasn&rsquo;t instant. They just never tested all three at once.</p>
<p>Red-teaming is how you turn ignored knowns into practiced scenarios.</p>
<h2 id="mission-style-red-teaming">Mission-Style Red-Teaming</h2>
<p> <a href="/blog/2020-06-08-chaos-engineering-practices/"
   
   >Chaos engineering</a>
 tools that randomly kill processes are useful, but they test a narrow failure class: single-component loss. Distributed database failures rarely look like one node dying cleanly. They look like degraded networks, clock drift, slow disks, operator errors during maintenance windows, and combinations of all of the above.</p>
<p>Mission-style red-teaming borrows from military and security practice. A dedicated team designs multi-step failure scenarios with specific objectives, executes them against production-equivalent infrastructure, and scores the defending team&rsquo;s response. The key difference from chaos engineering is intentionality: the red team isn&rsquo;t injecting random faults. They&rsquo;re pursuing a specific failure hypothesis through a sequence of realistic actions.</p>
<p>A red-team exercise has three roles:</p>
<ul>
<li><strong>Red team</strong>: designs and executes the failure scenario. Their goal is to cause data loss, unavailability, or corruption without triggering detection within a target time window.</li>
<li><strong>Blue team</strong>: the on-call and operations engineers responding as they would in a real incident. They don&rsquo;t know the scenario in advance.</li>
<li><strong>White team</strong>: observers who control the exercise, ensure safety boundaries, and document everything for the post-exercise review.</li>
</ul>
<p>The exercise runs for a fixed window, typically two to four hours. The red team executes their scenario. The blue team detects, diagnoses, and responds. Everyone debriefs afterward.</p>
<h2 id="the-stress-scenarios-that-matter">The Stress Scenarios That Matter</h2>
<p>Not all failure modes are worth practicing. Focus on scenarios that are plausible, high-impact, and poorly covered by existing automation.</p>
<p><strong>Network partitions with asymmetric visibility.</strong> One side of the partition can see the other; the other side cannot. This breaks assumptions in consensus protocols that expect symmetric failure detection. Many teams test clean partitions but never test asymmetric ones.</p>
<p><strong>Clock skew under load.</strong> Distributed databases that use timestamps for ordering (which is most of them) behave unpredictably when clocks drift. NTP usually keeps drift small, but under heavy load, NTP corrections can be delayed. The result is transaction ordering violations that are invisible until a consistency check runs, which might be hours or days later.</p>
<p><strong>Quorum erosion during maintenance.</strong> You take one node offline for a rolling upgrade. While it&rsquo;s down, a second node develops a slow disk. You now have a degraded quorum that&rsquo;s technically functional but one failure away from data unavailability. This is the most common compound failure pattern and the least practiced.</p>
<p><strong>Operator mistakes during incidents.</strong> The most dangerous moment for a distributed database is when a human is manually intervening during an incident. Wrong-node restarts, accidental force-quorum operations, and recovery commands run against the wrong cluster are responsible for a disproportionate share of catastrophic data loss. Red-teaming should include scenarios where the operator is given misleading information and time pressure.</p>
<p><strong>Backup restoration under partial failure.</strong> Most backup tests verify that a restore works on a clean target. Real restores happen during incidents, when the target environment is degraded, the team is stressed, and the backup might be from a point in time that&rsquo;s already inconsistent. Test restoration under these conditions, not just in a clean room.</p>
<h2 id="the-ooda-loop-for-incident-rehearsal">The OODA Loop for Incident Rehearsal</h2>
<p>Effective red-team exercises run on a tight observe-orient-decide-act cadence. This isn&rsquo;t just a framework. It&rsquo;s a scoring mechanism.</p>
<p><strong>Observe</strong>: How quickly does the blue team notice something is wrong? Detection time is the single most important metric. A failure that&rsquo;s detected in two minutes has a fundamentally different blast radius than one detected in twenty. Measure time from fault injection to first alert, and time from first alert to accurate diagnosis.</p>
<p><strong>Orient</strong>: Does the team correctly identify what&rsquo;s happening? Misdiagnosis is common in compound failures because the symptoms don&rsquo;t match any single runbook entry. The blue team might see elevated latency and assume it&rsquo;s a hot key, when the actual cause is a partial partition affecting replication. Measure time from first alert to correct hypothesis.</p>
<p><strong>Decide</strong>: Does the team choose an appropriate response? Under pressure, teams often default to the most familiar action (restart the node) rather than the most appropriate one (isolate the partition). Measure whether the chosen action matches the failure mode.</p>
<p><strong>Act</strong>: Does the team execute the response correctly? Even when the right decision is made, execution errors under stress are common. Typos in commands, wrong node targets, and forgotten steps in manual procedures are all frequent. Measure execution accuracy and time to containment.</p>
<p>Each phase gets a score. Over multiple exercises, these scores reveal systemic gaps: maybe detection is fast but diagnosis is slow, or decisions are sound but execution is error-prone. That tells you exactly where to invest in automation, training, or tooling.</p>
<h2 id="scoring-readiness">Scoring Readiness</h2>
<p>After each exercise, score three dimensions:</p>
<p><strong>Readiness</strong> (1-5): Could the team handle this scenario if it happened tomorrow in production? A 1 means the team didn&rsquo;t detect the failure. A 5 means they detected, diagnosed, and contained it within SLA.</p>
<p><strong>Blast radius</strong> (1-5): If the team had not responded, how bad would it have gotten? A 1 means minor degradation. A 5 means unrecoverable data loss or extended outage.</p>
<p><strong>Time to containment</strong> (minutes): Wall-clock time from fault injection to the point where the failure is contained and no longer spreading. This is the metric that matters most to your customers and your SLA.</p>
<p>Plot these over time. Improving readiness scores and decreasing containment times are the clearest signals that your red-teaming program is working. If scores plateau, your scenarios aren&rsquo;t challenging enough.</p>
<h2 id="from-findings-to-backlog">From Findings to Backlog</h2>
<p>Red-team exercises are useless if findings sit in a  <a href="/blog/2021-11-29-incident-management-practices/"
   
   >postmortem</a>
 document that nobody reads. Every exercise should produce a prioritized list of concrete improvements, each with an owner and a deadline.</p>
<p>The conversion process is simple:</p>
<ol>
<li><strong>List every gap discovered.</strong> Detection gaps, diagnostic confusion, tool limitations, missing runbooks, automation failures.</li>
<li><strong>Score each gap by blast radius times likelihood.</strong> Likelihood is informed by the exercise, not guessed.</li>
<li><strong>Assign an owner for each gap.</strong> Not a team. A person.</li>
<li><strong>Set a deadline before the next exercise.</strong> The next exercise will test whether the gap was closed. This creates accountability.</li>
</ol>
<p>Common improvements that come out of red-team exercises include automated partition detection that currently requires manual observation, runbook updates for compound failure scenarios, guardrails on dangerous operator commands during incidents, and backup restoration procedures tested under realistic conditions.</p>
<p>The backlog items from red-teaming tend to be high-value, low-glamour work. They rarely make it onto a roadmap through normal prioritization because they address risks that haven&rsquo;t materialized yet. The exercise provides the evidence needed to justify the investment.</p>
<h2 id="a-quarterly-operating-cadence">A Quarterly Operating Cadence</h2>
<p>Red-teaming works best as a regular practice, not a one-off event. A quarterly cadence balances rigor with operational overhead.</p>
<p>Run quarterly. Dedicate the first few weeks to scenario design based on recent incidents and architectural changes, a half-day to executing the exercise against a production-equivalent environment, and the remainder of the quarter to remediating the gaps you found.</p>
<p>This cadence means every quarter your team practices a realistic failure scenario, identifies concrete gaps, and fixes the most critical ones before the next exercise. Over four quarters, you&rsquo;ve tested and improved your response to a dozen failure modes. That&rsquo;s a fundamentally different reliability posture than &ldquo;we tested node failover once during setup and it worked.&rdquo;</p>
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Most catastrophic database failures are compound scenarios that nobody practiced, not genuinely unpredictable events.</li>
<li>Chaos engineering tests component failure. Red-teaming tests system failure under realistic operational conditions.</li>
<li>Score every exercise on detection time, diagnostic accuracy, decision quality, and execution correctness. Track trends.</li>
<li>Convert findings into owned backlog items with deadlines tied to the next exercise.</li>
<li>Run quarterly. Consistency matters more than intensity.</li>
</ul>
<p>Red-teaming distributed databases is not theater and it&rsquo;s not a luxury. It&rsquo;s the cheapest way to find out whether your recovery assumptions actually hold before your customers find out for you.</p>
]]></content:encoded></item><item><title>Your AI Infrastructure Is Not Ready for Scale. Neither Is Mine.</title><link>https://lawzava.com/blog/2023-12-18-ai-infrastructure-scale/</link><pubDate>Mon, 18 Dec 2023 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2023-12-18-ai-infrastructure-scale/</guid><description>GPU shortage is real, rate limits are a production constraint, and your AI demo will collapse under real traffic. Annoyed thoughts on infrastructure realism.</description><content:encoded><![CDATA[<p>I&rsquo;m going to be blunt: the state of AI infrastructure heading into 2024 is embarrassing.</p>
<p>We have models that can write poetry, generate code, and analyze images. We don&rsquo;t have enough GPUs to run them reliably. We don&rsquo;t have pricing that makes sense at scale. And we definitely don&rsquo;t have the operational maturity to treat these systems like the production dependencies they have become.</p>
<p>I&rsquo;ve spent December watching AI features I helped build at a fintech company run into every scaling problem distributed systems teams have been solving for twenty years. Rate limits. Cascading failures. Cost explosions. Latency spikes. The problems aren&rsquo;t new. The industry is just re-learning them with a fresh coat of hype.</p>
<h2 id="the-gpu-situation-is-absurd">The GPU Situation Is Absurd</h2>
<p>You can&rsquo;t get H100s. You can&rsquo;t reliably get inference capacity from any major provider unless you sign a months-long commitment or an enterprise contract that costs more than most startups raise in a seed round. The entire industry is building products on top of infrastructure that&rsquo;s supply-constrained, and nobody wants to talk about what happens when demand doubles next year.</p>
<p>I tried to reserve inference capacity for a production workload last month. The response from one provider was &ldquo;we can put you on a waitlist.&rdquo; A waitlist. For compute. In 2023. This isn&rsquo;t a technology problem. It&rsquo;s a supply chain problem wearing a technology costume.</p>
<h2 id="rate-limits-are-a-production-constraint">Rate Limits Are a Production Constraint</h2>
<p>Every AI API has rate limits. At low volume, you don&rsquo;t notice them. At production scale, they become the hardest ceiling in your architecture.</p>
<p>I hit OpenAI&rsquo;s rate limit during a load test and watched requests queue up until the entire feature became unusable. Not degraded &ndash; unusable. The fix wasn&rsquo;t clever engineering. It was a priority queue, backpressure, and load shedding. Distributed systems 101. The fact that most AI teams are learning this for the first time worries me.</p>
<h2 id="your-demo-wont-survive-real-traffic">Your Demo Won&rsquo;t Survive Real Traffic</h2>
<p>Here is what happens when your AI feature goes from 100 requests per day to 10,000:</p>
<p>Latency goes from &ldquo;acceptable&rdquo; to &ldquo;users are closing the tab.&rdquo; Costs go from &ldquo;rounding error&rdquo; to &ldquo;someone just Slacked asking why the API bill tripled.&rdquo; A provider outage that used to affect a handful of test users now takes down a production feature that the sales team just promised to a client.</p>
<p>I&rsquo;ve seen all three of these happen at the same company. In the same month.</p>
<h2 id="what-you-actually-need">What You Actually Need</h2>
<p><strong>Queues and backpressure.</strong> Treat your AI traffic as a managed stream, not an open pipe. Priority queues for critical requests. Backpressure when the system is saturated. Load shedding for low-priority work. This isn&rsquo;t optional once you have real users.</p>
<p><strong>Circuit breakers.</strong> Your model provider will have bad hours. Mine had a bad day last week. Circuit breakers stop a provider outage from cascading through your entire system. They&rsquo;re boring. They&rsquo;re essential. I&rsquo;ve been building systems with circuit breakers since my telecom days. The pattern hasn&rsquo;t changed. The dependency has.</p>
<p><strong>Graceful degradation.</strong> When GPT-4 is down, what happens? If the answer is &ldquo;the feature breaks,&rdquo; you don&rsquo;t have a production system. You have a demo with users. Fall back to cached responses. Fall back to a smaller, faster model. Fall back to a static message that says &ldquo;this feature is temporarily unavailable.&rdquo; Anything is better than a spinning loader.</p>
<p><strong>Cost controls that are actually enforced.</strong> Per-tenant budgets. Per-feature budgets. Daily caps. If you don&rsquo;t enforce them, you&rsquo;ll get a surprise invoice that triggers an emergency meeting. I&rsquo;ve seen a single prompt change &ndash; adding two paragraphs of context &ndash; increase monthly costs by 35%. Token pricing is deceptively simple until you multiply it by production volume.</p>
<p><strong>Caching.</strong> Exact-match caching is trivial to implement and saves real money. Same question, same context, same answer &ndash; serve it from cache. Semantic caching is fancier and worth exploring, but start with the easy wins.</p>
<h2 id="this-is-distributed-systems-work">This Is Distributed Systems Work</h2>
<p>None of this is novel. Queues, circuit breakers, graceful degradation, cost controls, caching &ndash; these are patterns from every distributed systems textbook ever written. The only thing that&rsquo;s new is the dependency type.</p>
<p>What frustrates me is that the AI community is treating infrastructure as a solved problem while building on top of infrastructure that&rsquo;s anything but solved. The models are impressive. The plumbing is held together with optimism and rate limit retries.</p>
<p>Build your AI features like you would build any production system that depends on an unreliable, expensive, supply-constrained external service. Because that&rsquo;s exactly what it is.</p>
]]></content:encoded></item><item><title>Distributed Systems Patterns I Keep Reaching For</title><link>https://lawzava.com/blog/2022-05-30-distributed-systems-patterns/</link><pubDate>Mon, 30 May 2022 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2022-05-30-distributed-systems-patterns/</guid><description>The patterns that actually survive production across failure handling, consistency, messaging, coordination, and scaling.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Most distributed systems advice reads like a textbook. This is the shortlist I actually use. Timeouts, retries, circuit breakers, sagas, outbox/inbox, and backpressure &ndash; applied with discipline, not ceremony.</p>
<p>I&rsquo;ve built and operated distributed systems at Verizon, AT&amp;T, Decloud, and most recently at a large consumer platform. The failure modes are remarkably consistent across all of them. Network partitions, cascading timeouts, retry storms, stale caches. The systems that survive aren&rsquo;t the clever ones. They&rsquo;re the ones with boring, well-applied patterns.</p>
<p>This isn&rsquo;t a catalog. It&rsquo;s the set of patterns I keep reaching for, along with real tradeoffs and code where it helps.</p>
<h2 id="failure-handling">Failure Handling</h2>
<h3 id="timeouts-and-deadlines">Timeouts and Deadlines</h3>
<p>Every remote call without a timeout is a bug waiting to happen. I learned this the hard way at Decloud &ndash; a single downstream service that started responding in 30 seconds instead of 300 milliseconds brought down our entire checkout flow. No timeout, no deadline propagation, no circuit breaker. Just threads piling up until the JVM ran out of memory.</p>
<p>Propagate deadlines end to end. If the user&rsquo;s request has 2 seconds left, the downstream call should know that. In Go, this is <code>context.WithTimeout</code> and it works beautifully:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">fetchUser</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">id</span> <span style="color:#66d9ef">string</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">User</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">cancel</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">WithTimeout</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#ae81ff">500</span><span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Millisecond</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">cancel</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">req</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">NewRequestWithContext</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;GET&#34;</span>, <span style="color:#a6e22e">userServiceURL</span><span style="color:#f92672">+</span><span style="color:#e6db74">&#34;/&#34;</span><span style="color:#f92672">+</span><span style="color:#a6e22e">id</span>, <span style="color:#66d9ef">nil</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">resp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">httpClient</span>.<span style="color:#a6e22e">Do</span>(<span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;fetch user %s: %w&#34;</span>, <span style="color:#a6e22e">id</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Body</span>.<span style="color:#a6e22e">Close</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">u</span> <span style="color:#a6e22e">User</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">u</span>, <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">NewDecoder</span>(<span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Body</span>).<span style="color:#a6e22e">Decode</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">u</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The caller sets the budget. The callee respects it. Simple.</p>
<h3 id="retry-with-backoff-and-jitter">Retry With Backoff and Jitter</h3>
<p>Retries fix transient errors. Immediate retries create storms. I&rsquo;ve seen a single retry loop without jitter generate enough traffic to keep a recovering service down for an extra twenty minutes.</p>
<p>Exponential backoff with full jitter. Cap the attempts. Only retry on errors that are actually transient &ndash; a 400 isn&rsquo;t transient, a 503 probably is.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">retryWithBackoff</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">maxAttempts</span> <span style="color:#66d9ef">int</span>, <span style="color:#a6e22e">fn</span> <span style="color:#66d9ef">func</span>() <span style="color:#66d9ef">error</span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">err</span> <span style="color:#66d9ef">error</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">attempt</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">attempt</span> &lt; <span style="color:#a6e22e">maxAttempts</span>; <span style="color:#a6e22e">attempt</span><span style="color:#f92672">++</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">fn</span>(); <span style="color:#a6e22e">err</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">attempt</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">maxAttempts</span><span style="color:#f92672">-</span><span style="color:#ae81ff">1</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">break</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">backoff</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>(<span style="color:#ae81ff">1</span><span style="color:#f92672">&lt;&lt;</span>uint(<span style="color:#a6e22e">attempt</span>)) <span style="color:#f92672">*</span> <span style="color:#ae81ff">100</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Millisecond</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">jitter</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>(<span style="color:#a6e22e">rand</span>.<span style="color:#a6e22e">Int63n</span>(int64(<span style="color:#a6e22e">backoff</span>)))
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">select</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">case</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Done</span>():
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">ctx</span>.<span style="color:#a6e22e">Err</span>()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">case</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">After</span>(<span style="color:#a6e22e">jitter</span>):
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;after %d attempts: %w&#34;</span>, <span style="color:#a6e22e">maxAttempts</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="circuit-breaker">Circuit Breaker</h3>
<p>After a threshold of errors, stop calling the failing dependency. Just stop. Return a degraded response, a cached value, or an honest error. Let the dependency recover without your traffic making things worse.</p>
<p>The mental model: closed (normal), open (failing, short-circuit), half-open (testing recovery). The implementation doesn&rsquo;t need to be complex. A counter, a timestamp, and a threshold.</p>
<h3 id="bulkheads">Bulkheads</h3>
<p>At a large consumer platform, we had a service that talked to six downstream dependencies through the same HTTP client pool. One slow dependency drained the pool and every other call started timing out too. Classic.</p>
<p>Separate connection pools per dependency. Separate goroutine budgets. The Titanic metaphor is overused but accurate &ndash; bulkheads keep one leak from sinking the whole ship.</p>
<h3 id="rate-limiting">Rate Limiting</h3>
<p>A predictable 429 is better than an unpredictable timeout. Always. Apply rate limits at the edge and between services. I&rsquo;ll talk more about this in a future post.</p>
<h2 id="consistency-and-transactions">Consistency and Transactions</h2>
<h3 id="sagas">Sagas</h3>
<p>Distributed transactions across service boundaries don&rsquo;t work. Full stop. Two-phase commit sounds great in a database textbook and falls apart the moment you have services owned by different teams with different SLAs.</p>
<p>Sagas replace a global transaction with local transactions and compensating actions. Two flavors:</p>
<p><strong>Choreography</strong>: services react to events. Decentralized, but the flow gets hard to trace once you have more than four or five steps. Debugging a choreographed saga during an incident is an exercise in grep and prayer.</p>
<p><strong>Orchestration</strong>: a coordinator drives the flow. Easier to understand, easier to audit, easier to debug. The coordinator becomes a critical path, so it needs to be durable. I default to orchestration unless the flow is truly simple.</p>
<h3 id="outbox-and-inbox">Outbox and Inbox</h3>
<p>You can&rsquo;t publish an event and update a database atomically with two separate calls. The outbox pattern solves this: write the event to a table in the same transaction as your state change. A separate publisher reads the outbox and sends the events.</p>
<p>The flip side is duplicate delivery. The inbox pattern handles that &ndash; record processed event IDs at the consumer and skip repeats. Pair them together. This is the backbone of every reliable event pipeline I&rsquo;ve built.</p>
<h3 id="idempotency">Idempotency</h3>
<p>Make writes idempotent. Use client-provided idempotency keys. Store the result keyed by that token. If the same request shows up twice, return the same result without doing the work again.</p>
<p>This isn&rsquo;t optional in a system with retries. If you have retries (and you should), you need idempotency.</p>
<h3 id="cqrs-and-read-models">CQRS and Read Models</h3>
<p>Strong consistency across services is expensive and usually unnecessary. Separate your write model from your read model. Accept eventual consistency for queries, search, and reporting. The read model can be denormalized, optimized, and rebuilt without touching the write path.</p>
<h2 id="messaging">Messaging</h2>
<h3 id="events">Events</h3>
<p>Events decouple services in time and deployment. Two styles worth knowing:</p>
<ul>
<li><strong>Event notification</strong>: &ldquo;something happened, look it up if you care.&rdquo; Lightweight, but consumers need access to the source.</li>
<li><strong>Event-carried state</strong>: &ldquo;something happened, here are the details.&rdquo; Heavier payload, but consumers are self-sufficient.</li>
</ul>
<p>Keep schemas versioned and backward compatible. Breaking an event schema in production is one of those things you only do once.</p>
<h3 id="work-queues">Work Queues</h3>
<p>Queues buffer work and let you process at a steady rate. Multiple consumers on the same queue give you horizontal scaling for free. Set visibility timeouts, handle retries explicitly, and always have a dead-letter queue for messages that can&rsquo;t be processed.</p>
<h2 id="coordination">Coordination</h2>
<h3 id="leader-election">Leader Election</h3>
<p>Some work must be done by exactly one node. Scheduling, cleanup, deduplication. Use leader election with lease-based expiration. If the leader dies, the lease expires, and someone else takes over.</p>
<p>I&rsquo;ve seen teams try to avoid leader election by distributing coordination across all nodes. It always ends with split-brain bugs that take weeks to reproduce.</p>
<h3 id="distributed-locks">Distributed Locks</h3>
<p>Use sparingly. A distributed lock should protect a small, short-lived critical section. If you find yourself holding a lock for seconds, you probably need a different design &ndash; partition by key or use a single-writer pattern.</p>
<h2 id="scaling">Scaling</h2>
<h3 id="sharding-and-consistent-hashing">Sharding and Consistent Hashing</h3>
<p>Partition by key. When you add or remove nodes, consistent hashing minimizes the data that moves. Keep shard ownership explicit and routing predictable. Implicit sharding is debugging hell.</p>
<h3 id="backpressure">Backpressure</h3>
<p>When overloaded, shed work instead of collapsing. Bounded queues, rate limits, and explicit flow control. The system that says &ldquo;no&rdquo; gracefully is more reliable than the system that says &ldquo;yes&rdquo; and falls over.</p>
<p>This is a cultural thing too. Engineers need to be comfortable returning errors under load instead of trying to serve every request.</p>
<h2 id="observability">Observability</h2>
<p>Correlation IDs through every log and event. Latency percentiles (not averages). Error rates per dependency. Queue depth trends. Alert on symptoms &ndash; elevated latency, increasing error rates &ndash; not on individual errors.</p>
<p>Without observability, you&rsquo;re guessing. During an incident, guessing is expensive.</p>
<h2 id="the-default-kit">The Default Kit</h2>
<p>If I&rsquo;m starting a new service, this is what goes in on day one:</p>
<ul>
<li>Timeouts and deadline propagation on every remote call</li>
<li>Retries with exponential backoff and jitter for transient errors</li>
<li>Circuit breakers and bulkheads around dependencies</li>
<li>Idempotency keys for writes, inbox deduplication for events</li>
<li>Outbox pattern for reliable event publishing</li>
<li>Rate limiting and backpressure at boundaries</li>
<li>Correlation IDs and service-level metrics</li>
</ul>
<p>None of this is novel. That&rsquo;s the point. Distributed systems are messy by nature. Patterns make the mess predictable. Discipline over heroics.</p>
]]></content:encoded></item><item><title>What Actually Works for Distributed Teams (Six Months In)</title><link>https://lawzava.com/blog/2020-09-28-distributed-team-practices/</link><pubDate>Mon, 28 Sep 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-09-28-distributed-team-practices/</guid><description>After running a remote-first company for years and watching everyone else scramble through COVID, here&amp;amp;rsquo;s what I&amp;amp;rsquo;ve learned actually works &amp;amp;ndash; and what doesn&amp;amp;rsquo;t.</description><content:encoded><![CDATA[<p>Last Tuesday one of my engineers pinged me at 11pm his time to ask about a deployment flag. I was asleep. He figured it out from our runbook, shipped it, and left a note in the PR. I found out about the whole thing over morning coffee.</p>
<p>That&rsquo;s a distributed team working. No drama. No blocked work. No meeting about it afterward.</p>
<p>I&rsquo;ve been running Decloud as a remote-first company since before the pandemic made it fashionable. So when March hit and every company on Earth suddenly became &ldquo;distributed,&rdquo; I had a front-row seat to a lot of people reinventing wheels. Badly.</p>
<p>Six months in, the panic phase is over. But most teams are still stuck in this weird middle ground where they&rsquo;re doing office work over Zoom and calling it remote. It&rsquo;s not. And that gap is where the pain lives.</p>
<h2 id="writing-is-the-job-now">Writing is the job now</h2>
<p>This is the single biggest adjustment and most teams still haven&rsquo;t made it. In an office, you can be sloppy with communication because you&rsquo;ll bump into someone at lunch and clear it up. Distributed? That ambiguity sits there rotting until someone makes the wrong assumption.</p>
<p>At Decloud, we write everything down. Not because we love documentation &ndash; honestly, nobody does. Because the alternative is having the same conversation three times across two time zones and a Slack thread that&rsquo;s 200 messages deep.</p>
<p>When someone proposes a change, they write it up. Context, options, recommendation. It takes maybe 20 minutes. It saves hours of meetings that would&rsquo;ve happened otherwise. The people who push back on this the hardest are usually the ones who are used to winning arguments by talking louder in a room. That doesn&rsquo;t work in a Google Doc.</p>
<h2 id="meetings-should-hurt-to-schedule">Meetings should hurt to schedule</h2>
<p>Hot take: if your remote team is in more meetings than your office team was, you&rsquo;ve failed. The whole point of going distributed is that async is the default. Meetings are the exception.</p>
<p>We have a few standing syncs at Decloud. Short ones. Everything else requires an agenda shared beforehand and a written outcome after. If you can&rsquo;t write an agenda, you don&rsquo;t need a meeting. You need to think more about what you actually want.</p>
<p>The time zone thing makes this easier, honestly. When you only have a four-hour overlap with half your team, you get very precious about those hours. Pairing sessions, hard design problems, the stuff where real-time back-and-forth actually matters. Everything else goes async.</p>
<h2 id="onboarding-is-where-you-find-out-if-your-system-works">Onboarding is where you find out if your system works</h2>
<p>New hires are the stress test. If your distributed setup actually works, a new person should be able to get productive without scheduling fifteen &ldquo;intro calls.&rdquo; If they can&rsquo;t, your documentation is bad. Full stop.</p>
<p>We give every new hire a buddy, a setup guide, and a small ticket that ships in the first week. The shipping part matters. Nothing builds confidence like seeing your code in production on day four. Compare that to spending your first week in orientation decks learning about the company values. Please.</p>
<h2 id="the-green-dot-problem">The green dot problem</h2>
<p>I talk to other engineering leads and the thing that makes me genuinely angry is the surveillance stuff. Screen monitoring. Activity tracking. Checking who&rsquo;s online at what time.</p>
<p>Stop it. You&rsquo;re measuring presence, not output. I don&rsquo;t care if someone takes a two-hour break at 2pm to go for a run. I care if the work ships. If you can&rsquo;t evaluate your engineers without watching their screen, that&rsquo;s a management problem, not an employee problem.</p>
<p>At Decloud we&rsquo;re explicit about this: here&rsquo;s what we expect this sprint, here&rsquo;s how we check in, here&rsquo;s what &ldquo;done&rdquo; looks like. That clarity is the actual work of management. It&rsquo;s harder than installing monitoring software, which is exactly why most people don&rsquo;t do it.</p>
<h2 id="what-still-sucks">What still sucks</h2>
<p>I&rsquo;m not going to pretend it&rsquo;s all figured out. Some things about distributed work are genuinely worse.</p>
<p>Catching burnout is harder. In an office you can see someone looking tired or disengaged. Over Slack, people just go quiet, and by the time you notice, they&rsquo;re already job hunting. I still don&rsquo;t have a great answer for this beyond frequent 1:1s and actually paying attention.</p>
<p>Spontaneous collaboration is basically gone. Those hallway conversations where two people accidentally solve a problem? They don&rsquo;t happen on Zoom. We&rsquo;ve tried virtual coffee chats and random pairing. It&rsquo;s fine. It&rsquo;s not the same.</p>
<p>And onboarding senior people into leadership roles remotely is rough. Building the trust and political capital that lets you make big calls &ndash; that takes longer when you can&rsquo;t read a room.</p>
<h2 id="the-actual-secret">The actual secret</h2>
<p>None of this is complicated. Write things down. Meet less. Trust your people. Ship the onboarding. That&rsquo;s it. That&rsquo;s the whole framework.</p>
<p>The reason most teams struggle with remote work isn&rsquo;t because they lack the right tools or the right process doc. It&rsquo;s because distributed work is less forgiving of the dysfunction you were already getting away with in the office. Bad communication, unclear ownership, meetings that should&rsquo;ve been emails &ndash; all of that was survivable when everyone sat in the same building. Remove the building, and it falls apart.</p>
<p>Fix the fundamentals and the rest follows. Or don&rsquo;t, and keep blaming Zoom fatigue. Up to you.</p>
]]></content:encoded></item><item><title>Observability for Small Distributed Teams (What Actually Works)</title><link>https://lawzava.com/blog/2020-09-14-observability-distributed-teams/</link><pubDate>Mon, 14 Sep 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-09-14-observability-distributed-teams/</guid><description>Most observability advice is written for 500-engineer orgs. Here&amp;amp;rsquo;s what actually matters when you&amp;amp;rsquo;re a small distributed team trying not to drown in dashboards.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>You don&rsquo;t need Datadog&rsquo;s enterprise tier. You need structured logs, one good dashboard per service, alerts that don&rsquo;t cry wolf, and a <code>request_id</code> on everything. That&rsquo;s 80% of it.</p>
<hr>
<p>I&rsquo;ve been working with distributed teams for most of this year. Some are five people across three time zones. Some are thirty across eight. The pattern I keep seeing: they either have zero observability or they went full enterprise cargo cult and now nobody can find anything.</p>
<p>There&rsquo;s a middle ground. I want to talk about that.</p>
<h2 id="the-actual-problem-with-distributed-teams">The actual problem with distributed teams</h2>
<p>In an office, someone notices something is slow. They say it out loud. Someone else goes &ldquo;oh yeah, I deployed ten minutes ago.&rdquo; Problem found in under a minute.</p>
<p>Remote? That same issue sits in a Slack thread for 45 minutes while people in different time zones wake up, read context, and try to figure out what changed. I&rsquo;ve watched this happen. Repeatedly.</p>
<p>The fix isn&rsquo;t more tools. It&rsquo;s making your systems capable of answering basic questions without requiring a human to be online at the right moment.</p>
<p>Three questions. That&rsquo;s it:</p>
<ol>
<li>Is this thing broken right now?</li>
<li>What changed recently?</li>
<li>Where do I look next?</li>
</ol>
<p>If your setup can answer those, you&rsquo;re ahead of most teams I&rsquo;ve worked with.</p>
<h2 id="enterprise-observability-isnt-your-observability">Enterprise observability isn&rsquo;t your observability</h2>
<p>Google has 10,000 SREs. They built custom everything. When you read their SRE book and try to implement the same stack with your team of eight, you end up with:</p>
<ul>
<li>A Prometheus instance nobody configured alerts for</li>
<li>Grafana dashboards copied from a blog post that don&rsquo;t match your services</li>
<li>Jaeger running but with 0.1% sampling so traces are useless when you actually need them</li>
<li>An ELK stack eating 40% of your infrastructure budget</li>
</ul>
<p>I&rsquo;ve seen this exact setup at three different companies this year. Not exaggerating.</p>
<h2 id="what-you-actually-need">What you actually need</h2>
<p>Here&rsquo;s my stack recommendation for a small distributed team. Opinionated, yes. But it works.</p>
<p><strong>Logs: Structured JSON to a managed service.</strong> Loki if you&rsquo;re cheap. Papertrail if you want simple. The key is structured, not the tool. Every log line should look roughly like this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;level&#34;</span>: <span style="color:#e6db74">&#34;error&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;msg&#34;</span>: <span style="color:#e6db74">&#34;payment failed&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;service&#34;</span>: <span style="color:#e6db74">&#34;checkout&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;request_id&#34;</span>: <span style="color:#e6db74">&#34;7f3c2c4d&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;user_id&#34;</span>: <span style="color:#e6db74">&#34;u_123&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;error&#34;</span>: <span style="color:#e6db74">&#34;card_declined&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;duration_ms&#34;</span>: <span style="color:#ae81ff">340</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Same field names. Every service. No exceptions. The <code>request_id</code> alone will save you hours of debugging per incident. I can&rsquo;t stress this enough. Propagate it through HTTP headers, queue messages, background jobs. Everywhere.</p>
<p><strong>Metrics: Prometheus + Grafana.</strong> Still the best bang for buck. But here&rsquo;s the thing &ndash; don&rsquo;t build 30 dashboards. Build one per service. Three panels:</p>
<ol>
<li>Request rate and error rate (tells you if something is broken)</li>
<li>Latency percentiles (tells you if it&rsquo;s degrading)</li>
<li>Recent deploys and config changes overlaid on the graphs (tells you what caused it)</li>
</ol>
<p>That&rsquo;s your dashboard. If a new engineer can&rsquo;t look at it and understand the health of the service in 30 seconds, strip it down further.</p>
<p><strong>Traces: Jaeger or Zipkin, but only if you have more than two services.</strong> If you&rsquo;re a monolith with a database and a cache, traces are overhead you don&rsquo;t need yet. Just use request IDs in your logs. Seriously.</p>
<p>When you do need traces, bump sampling to at least 10% on critical paths. 0.1% default sampling means you&rsquo;ll never have a trace for the request that actually broke.</p>
<p><strong>Alerts: Less is more.</strong> Every alert that pages someone at 3am and isn&rsquo;t actionable is erosion of trust. Once your team stops trusting alerts, you&rsquo;ve lost. They&rsquo;ll start muting channels. I&rsquo;ve seen it happen.</p>
<p>My rule: every alert needs three things.</p>
<ul>
<li>A condition that&rsquo;s actually abnormal (not &ldquo;CPU above 60%&rdquo;)</li>
<li>A link to the relevant dashboard</li>
<li>A link to a runbook that says what to do first</li>
</ul>
<p>If you can&rsquo;t write those three things for an alert, the alert shouldn&rsquo;t exist.</p>
<h2 id="the-request_id-sermon">The request_id sermon</h2>
<p>I keep coming back to this because it&rsquo;s the single highest-leverage thing you can do.</p>
<p>At the fintech startup we had services talking to services talking to queues talking to workers. When something went wrong without correlation IDs, the debugging process was: check this log, then that log, then maybe this other log, hope the timestamps line up, piece it together manually. Took forever.</p>
<p>After we standardized on a single <code>request_id</code> header propagated everywhere? Same investigation. One search. Done.</p>
<p>The implementation is trivial. Middleware that reads <code>X-Request-ID</code> from incoming requests. Generates a UUID if missing. Passes it along. Logs it on every line. Takes an afternoon to implement across your whole stack.</p>
<p>An afternoon of work for months of saved debugging time. That&rsquo;s the kind of trade I like.</p>
<h2 id="runbooks-the-unsexy-high-leverage-tool">Runbooks: the unsexy high-leverage tool</h2>
<p>Nobody wants to write runbooks. I get it. But here&rsquo;s the scenario: it&rsquo;s 2am in your time zone. The alert fires. The person on call is in a different country. They&rsquo;ve been on the team for three weeks.</p>
<p>Without a runbook, they&rsquo;re messaging people, waiting for responses, guessing. With a runbook, they open it, follow the steps, and either fix it or know exactly who to escalate to.</p>
<p>Keep them short. Keep them next to the code. Update them after every incident. A runbook that says &ldquo;check the database connection pool, then check Redis, then check the upstream API timeout&rdquo; is worth more than a 50-page incident response process document nobody has read.</p>
<h2 id="mistakes-i-keep-seeing">Mistakes I keep seeing</h2>
<p><strong>Collecting everything.</strong> Storage is cheap. Cardinality explosions aren&rsquo;t. I watched a team&rsquo;s Prometheus instance fall over because they added a <code>user_id</code> label to a counter. Millions of time series. Dead monitoring system. During an outage. Ironic.</p>
<p><strong>Dashboard graveyards.</strong> Thirty dashboards, twenty-eight of which nobody has looked at in months. Two of which are actually useful but you can&rsquo;t remember which ones. Delete aggressively.</p>
<p><strong>Happy path instrumentation only.</strong> Your error paths need more instrumentation than your happy paths. The happy path works. You know this because nobody is complaining. The error paths are where surprises live.</p>
<p><strong>Separate conventions per team.</strong> One team calls it <code>user_id</code>, another calls it <code>userId</code>, a third calls it <code>uid</code>. Now your cross-service queries are a mess. Pick a convention. Enforce it in code review. This is boring work that pays off enormously.</p>
<h2 id="what-to-measure-about-your-observability-itself">What to measure about your observability itself</h2>
<p>One meta-metric I track: time from &ldquo;something seems wrong&rdquo; to &ldquo;I know what changed and where to look.&rdquo; If that number is going down over time, your observability is working. If it&rsquo;s flat or going up, you&rsquo;re adding complexity without adding clarity.</p>
<p>The other one: how many alerts fired this week that didn&rsquo;t need a human response? If it&rsquo;s more than 20%, you have a noise problem.</p>
<h2 id="start-here">Start here</h2>
<p>If you&rsquo;re starting from scratch with a small distributed team, do this in order:</p>
<ol>
<li>Structured JSON logs with a shared <code>request_id</code>. One week of work, max.</li>
<li>One Grafana dashboard per service with the three panels I mentioned. Another week.</li>
<li>Three to five alerts that are actually actionable. A few days.</li>
<li>Short runbooks for those alerts. A day.</li>
</ol>
<p>That&rsquo;s a month of work spread across your team. After that, you have a system that answers the three questions. Everything else &ndash; traces, SLOs, error budgets, custom metrics &ndash; layer it on when you feel the pain, not before.</p>
<p>Don&rsquo;t let perfect be the enemy of &ldquo;I can actually debug production at 2am without waking up three people.&rdquo;</p>
]]></content:encoded></item><item><title>Event-Driven Architecture: What I Got Wrong and What Survived</title><link>https://lawzava.com/blog/2020-07-06-event-driven-architecture/</link><pubDate>Mon, 06 Jul 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-07-06-event-driven-architecture/</guid><description>Lessons from building event-driven systems at the fintech startup and Decloud: what works, what silently corrupts your data, and Go patterns that hold up.</description><content:encoded><![CDATA[<p>It was a Tuesday at the fintech startup. Our financial news pipeline processed market events from about forty data sources &ndash; earnings announcements, price movements, regulatory filings &ndash; and fanned them out to user watchlists. Event-driven. Clean architecture diagrams. The works.</p>
<p>Then Reuters published a correction to an earnings report. The correction event arrived, got processed, updated the record. Except three other consumers had already read the original event, cached it, and built derivative data on top of it. The correction propagated to the main feed within seconds. The analytics service picked it up four minutes later. The notification service? It had already sent push notifications to eight thousand users with the wrong number.</p>
<p>Nobody&rsquo;s data was technically &ldquo;lost.&rdquo; Everything was eventually consistent. But &ldquo;eventually&rdquo; turned out to be forty minutes, and by then a bunch of users had made trading decisions based on stale information. The postmortem wasn&rsquo;t fun.</p>
<p>That incident taught me more about event-driven architecture than any conference talk. Events are easy to publish. The hard part is everything that happens after.</p>
<h2 id="quick-take">Quick take</h2>
<p>Event-driven architecture decouples your services beautifully and then introduces a whole new category of bugs you&rsquo;ve never debugged before. It&rsquo;s worth it &ndash; sometimes. The key is idempotent consumers, an outbox pattern for reliable publishing, and accepting that you&rsquo;re trading request-response simplicity for operational complexity. Go into it with open eyes or don&rsquo;t go at all.</p>
<hr>
<h2 id="events-vs-commands">Events vs. commands</h2>
<p>This distinction matters more than people think. Get it wrong and your &ldquo;loosely coupled&rdquo; system is actually tightly coupled with extra steps.</p>
<p><strong>Events</strong> are facts. Past tense. &ldquo;OrderPlaced.&rdquo; &ldquo;UserRegistered.&rdquo; &ldquo;PriceCorrected.&rdquo; The producer publishes what happened and doesn&rsquo;t care who&rsquo;s listening. Could be zero consumers. Could be fifty.</p>
<p><strong>Commands</strong> are directives. &ldquo;SendEmail.&rdquo; &ldquo;ChargeCard.&rdquo; &ldquo;UpdateCache.&rdquo; Targeted at a specific service. The sender expects something to happen.</p>
<p>At Decloud, I caught us mixing these constantly in the early days. A service would publish &ldquo;ProcessPayment&rdquo; as an &ldquo;event.&rdquo; That&rsquo;s not an event. That&rsquo;s a command wearing an event&rsquo;s clothes. And it created an invisible dependency &ndash; if the payment service went down, the publisher had to care, which defeated the whole point.</p>
<p>Rule of thumb: if removing all consumers would break the producer&rsquo;s logic, it&rsquo;s a command, not an event.</p>
<h2 id="the-event-contract-is-your-public-api">The event contract is your public API</h2>
<p>This is where the fintech startup&rsquo;s correction bug really originated. We treated events like internal implementation details. Field names leaked database columns. Payloads included internal IDs that only made sense inside one service.</p>
<p>Your event schema is a contract. Treat it like a public API. Version it. Document it. Don&rsquo;t shove your ORM struct into JSON and call it a day.</p>
<p>Here&rsquo;s the envelope pattern we settled on at Decloud:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;id&#34;</span>: <span style="color:#e6db74">&#34;7f2b5a1e-6c0a-4c2c-9a7a-9c2c6d49f9a2&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;type&#34;</span>: <span style="color:#e6db74">&#34;pricing.QuoteUpdated&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;pricing-service&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;time&#34;</span>: <span style="color:#e6db74">&#34;2020-07-06T10:30:00Z&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#ae81ff">2</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;correlationId&#34;</span>: <span style="color:#e6db74">&#34;req-8f43&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;data&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;instrumentId&#34;</span>: <span style="color:#e6db74">&#34;AAPL&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;price&#34;</span>: <span style="color:#ae81ff">364.11</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;currency&#34;</span>: <span style="color:#e6db74">&#34;USD&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>correlationId</code> saved us countless debugging hours. When a user reports something wrong, you grep for that ID across every service&rsquo;s logs and see the full journey. Without it, debugging event-driven systems is archaeology.</p>
<h2 id="idempotent-consumers-or-die">Idempotent consumers or die</h2>
<p>Every broker I&rsquo;ve worked with &ndash; Kafka, RabbitMQ, NATS &ndash; delivers at-least-once. Duplicates will happen. Network blip, consumer restart, rebalance. Your consumer sees the same event twice. Or five times.</p>
<p>If your handler isn&rsquo;t idempotent, you charge a credit card twice. Or send the same notification to eight thousand people. Again.</p>
<p>Here&rsquo;s the pattern we use in Go at Decloud:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">EventHandler</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">db</span>    <span style="color:#f92672">*</span><span style="color:#a6e22e">sql</span>.<span style="color:#a6e22e">DB</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">topic</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">h</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">EventHandler</span>) <span style="color:#a6e22e">Handle</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">event</span> <span style="color:#a6e22e">Event</span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">tx</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">BeginTx</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#66d9ef">nil</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;begin tx: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">tx</span>.<span style="color:#a6e22e">Rollback</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Check if we&#39;ve already processed this event</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">exists</span> <span style="color:#66d9ef">bool</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">tx</span>.<span style="color:#a6e22e">QueryRowContext</span>(<span style="color:#a6e22e">ctx</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;SELECT EXISTS(SELECT 1 FROM processed_events WHERE event_id = $1)&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">ID</span>,
</span></span><span style="display:flex;"><span>    ).<span style="color:#a6e22e">Scan</span>(<span style="color:#f92672">&amp;</span><span style="color:#a6e22e">exists</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;check idempotency: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">exists</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span> <span style="color:#75715e">// Already processed. Done.</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Do the actual work</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">processEvent</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">tx</span>, <span style="color:#a6e22e">event</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;process event: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Record that we&#39;ve processed this event -- same transaction</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">err</span> = <span style="color:#a6e22e">tx</span>.<span style="color:#a6e22e">ExecContext</span>(<span style="color:#a6e22e">ctx</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;INSERT INTO processed_events (event_id, topic, processed_at) VALUES ($1, $2, NOW())&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">ID</span>, <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">topic</span>,
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;record processed event: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">tx</span>.<span style="color:#a6e22e">Commit</span>()
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The important bit: the idempotency check and the business logic live in the same database transaction. If the process crashes after doing the work but before recording the event ID, the transaction rolls back and the event gets reprocessed safely. If it crashes after commit, the duplicate delivery gets caught by the <code>EXISTS</code> check.</p>
<p>This pattern handles 99% of our idempotency needs. The remaining 1% involves external side effects (sending emails, calling third-party APIs) which need a different approach.</p>
<h2 id="the-outbox-pattern">The outbox pattern</h2>
<p>This one took me too long to learn. The naive approach: save to database, then publish event.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// DON&#39;T DO THIS</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">OrderService</span>) <span style="color:#a6e22e">PlaceOrder</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">order</span> <span style="color:#a6e22e">Order</span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">SaveOrder</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">order</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">err</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// What if we crash right here?</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">publisher</span>.<span style="color:#a6e22e">Publish</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">&#34;orders.OrderPlaced&#34;</span>, <span style="color:#a6e22e">order</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>If you crash between the database write and the publish, the order exists but the event never fires. Downstream services never learn about it. Data silently drifts.</p>
<p>The fix is the outbox pattern. Write the event to an outbox table in the same transaction as your business data. A separate process polls the outbox and publishes.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">OrderService</span>) <span style="color:#a6e22e">PlaceOrder</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">order</span> <span style="color:#a6e22e">Order</span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">tx</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">BeginTx</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#66d9ef">nil</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;begin tx: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">tx</span>.<span style="color:#a6e22e">Rollback</span>()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">saveOrder</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">tx</span>, <span style="color:#a6e22e">order</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;save order: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">evt</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">OutboxEvent</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">ID</span>:        <span style="color:#a6e22e">uuid</span>.<span style="color:#a6e22e">New</span>().<span style="color:#a6e22e">String</span>(),
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Type</span>:      <span style="color:#e6db74">&#34;orders.OrderPlaced&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">Payload</span>:   <span style="color:#a6e22e">marshal</span>(<span style="color:#a6e22e">order</span>),
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">CreatedAt</span>: <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Now</span>(),
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">saveOutboxEvent</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">tx</span>, <span style="color:#a6e22e">evt</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;save outbox event: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">tx</span>.<span style="color:#a6e22e">Commit</span>()
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>And the publisher, running separately:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">p</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">OutboxPublisher</span>) <span style="color:#a6e22e">Poll</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">events</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">FetchUnpublished</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#ae81ff">100</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;fetch unpublished: %w&#34;</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">evt</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">events</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">broker</span>.<span style="color:#a6e22e">Publish</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">evt</span>.<span style="color:#a6e22e">Type</span>, <span style="color:#a6e22e">evt</span>.<span style="color:#a6e22e">Payload</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Log and continue. We&#39;ll retry on next poll.</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;publish failed for event %s: %v&#34;</span>, <span style="color:#a6e22e">evt</span>.<span style="color:#a6e22e">ID</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">p</span>.<span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">MarkPublished</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">evt</span>.<span style="color:#a6e22e">ID</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Event published but not marked. Will be re-published.</span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Consumer idempotency handles the duplicate.</span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;mark published failed for event %s: %v&#34;</span>, <span style="color:#a6e22e">evt</span>.<span style="color:#a6e22e">ID</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Is it more code? Yes. Does it guarantee that every committed business change produces an event? Also yes. The outbox publisher can crash, restart, and pick up where it left off. Consumers handle the duplicates. The system converges.</p>
<p>At Decloud, we run the outbox poller on a 500ms tick. Good enough for our latency requirements. If you need sub-100ms, look into CDC (change data capture) with something like Debezium reading the WAL directly. Same principle, lower latency, more operational overhead.</p>
<h2 id="picking-a-broker">Picking a broker</h2>
<p>I&rsquo;m not going to do a feature comparison chart. Those are outdated the moment you publish them. Here&rsquo;s how I think about it instead.</p>
<p><strong>Kafka</strong> if you need ordered replay. We use it at Decloud for anything where &ldquo;play back the last 7 days of events&rdquo; is a real requirement. The retention model is its killer feature. The operational cost is its tax. Running Kafka well requires dedicated attention.</p>
<p><strong>RabbitMQ</strong> if you need flexible routing and work queues. Fan-out, topic exchanges, priority queues. If your pattern is &ldquo;distribute work across competing consumers,&rdquo; Rabbit is easier to reason about than Kafka consumer groups.</p>
<p><strong>NATS</strong> if you want something lightweight. We use NATS for internal service communication that doesn&rsquo;t need persistence. Fire-and-forget telemetry, cache invalidation signals, that kind of thing. JetStream adds persistence if you need it, but at that point evaluate whether Kafka serves you better.</p>
<p><strong>Managed services</strong> (SQS, EventBridge, Pub/Sub) if operational simplicity matters more than configurability. At Decloud we run on AWS, and for non-critical event flows, SQS with a dead-letter queue is hard to beat for simplicity.</p>
<h2 id="ordering-is-a-lie-mostly">Ordering is a lie (mostly)</h2>
<p>Kafka guarantees ordering per partition. RabbitMQ doesn&rsquo;t guarantee ordering at all with competing consumers. NATS JetStream gives you ordering per stream but not per subject with multiple consumers.</p>
<p>In practice, design for out-of-order delivery. If you can&rsquo;t, partition by the entity that needs ordering (e.g., all events for order ID &ldquo;abc&rdquo; go to the same partition).</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#75715e">// Partition key ensures all events for the same order</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// land on the same Kafka partition</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">partitionKey</span>(<span style="color:#a6e22e">event</span> <span style="color:#a6e22e">Event</span>) <span style="color:#66d9ef">string</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">switch</span> <span style="color:#a6e22e">e</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">Data</span>.(<span style="color:#66d9ef">type</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">OrderEvent</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">OrderID</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">UserEvent</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">e</span>.<span style="color:#a6e22e">UserID</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">default</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">ID</span> <span style="color:#75715e">// fallback: event-level ordering only</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>At the fintech startup, we partitioned by instrument ID. All events for AAPL went to the same partition. Ordering within an instrument was guaranteed. Ordering across instruments didn&rsquo;t matter.</p>
<h2 id="dead-letters-and-retries">Dead letters and retries</h2>
<p>When a consumer fails, retry with exponential backoff. After N retries, send the event to a dead-letter queue. Don&rsquo;t retry forever. I&rsquo;ve seen a single poison event take down a consumer group for hours because it kept crashing and restarting in a loop.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> (<span style="color:#a6e22e">h</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">RetryHandler</span>) <span style="color:#a6e22e">HandleWithRetry</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">event</span> <span style="color:#a6e22e">Event</span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">lastErr</span> <span style="color:#66d9ef">error</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">attempt</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">attempt</span> <span style="color:#f92672">&lt;=</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">maxRetries</span>; <span style="color:#a6e22e">attempt</span><span style="color:#f92672">++</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">attempt</span> &gt; <span style="color:#ae81ff">0</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">backoff</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>(<span style="color:#a6e22e">attempt</span><span style="color:#f92672">*</span><span style="color:#a6e22e">attempt</span>) <span style="color:#f92672">*</span> <span style="color:#ae81ff">100</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Millisecond</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">backoff</span> &gt; <span style="color:#ae81ff">10</span><span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">backoff</span> = <span style="color:#ae81ff">10</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Sleep</span>(<span style="color:#a6e22e">backoff</span>)
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">lastErr</span> = <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">inner</span>.<span style="color:#a6e22e">Handle</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">event</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">lastErr</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;attempt %d failed for event %s: %v&#34;</span>, <span style="color:#a6e22e">attempt</span><span style="color:#f92672">+</span><span style="color:#ae81ff">1</span>, <span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">ID</span>, <span style="color:#a6e22e">lastErr</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// Max retries exceeded. Dead-letter it.</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">h</span>.<span style="color:#a6e22e">deadLetter</span>.<span style="color:#a6e22e">Send</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">event</span>, <span style="color:#a6e22e">lastErr</span>); <span style="color:#a6e22e">err</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;dead letter failed for event %s: %w&#34;</span>, <span style="color:#a6e22e">event</span>.<span style="color:#a6e22e">ID</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">nil</span> <span style="color:#75715e">// Event is safely in the DLQ. Consumer can move on.</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The dead-letter queue isn&rsquo;t a trash can. Set up alerts. Review it weekly. At Decloud we have a Slack alert for any event hitting the DLQ and a weekly review where we categorize failures: transient (infra blip), poison (bad data), or bug (our code).</p>
<h2 id="when-events-are-the-wrong-answer">When events are the wrong answer</h2>
<p>I&rsquo;ve seen teams go all-in on event-driven architecture for a CRUD app with three services. Don&rsquo;t.</p>
<p>Events make sense when:</p>
<ul>
<li><strong>Multiple consumers</strong> need to react to the same change independently</li>
<li><strong>Temporal decoupling</strong> matters &ndash; the producer shouldn&rsquo;t wait for or even know about consumers</li>
<li><strong>Data pipelines</strong> need a replay-friendly stream of changes</li>
<li><strong>Cross-team boundaries</strong> where synchronous coupling would create deployment dependencies</li>
</ul>
<p>Events are overhead when:</p>
<ul>
<li>You have one producer and one consumer. That&rsquo;s a function call with extra steps.</li>
<li>You need synchronous request-response. An HTTP call is simpler and easier to debug.</li>
<li>The system is small enough that a monolith or simple service-to-service calls work fine.</li>
</ul>
<p>At Decloud, roughly 40% of our inter-service communication is event-driven. The rest is gRPC. That ratio feels right. Event-driven for fan-out and cross-domain integration. Direct calls for everything else.</p>
<h2 id="observability-or-it-didnt-happen">Observability or it didn&rsquo;t happen</h2>
<p>You can&rsquo;t debug an event-driven system without distributed tracing. Full stop.</p>
<p>Every event carries a correlation ID. Every consumer logs that ID. When something goes wrong, you search by correlation ID and see the full chain: which service published, who consumed, what happened next.</p>
<p>We also track:</p>
<ul>
<li><strong>Consumer lag</strong> &ndash; how far behind each consumer group is. If it&rsquo;s growing, you&rsquo;re either underprovisioned or have a bug.</li>
<li><strong>Processing time per event type</strong> &ndash; catches performance regressions early.</li>
<li><strong>Dead-letter rate</strong> &ndash; our canary. A spike means something broke.</li>
<li><strong>End-to-end latency</strong> &ndash; time from event publication to final consumer processing. This is the number users actually feel.</li>
</ul>
<hr>
<p>Event-driven architecture is a trade. You give up the simplicity of &ldquo;service A calls service B and gets a response.&rdquo; In return you get decoupling, independent scaling, and resilience to individual service failures. Whether that trade is worth it depends on your system, your team, and how much operational maturity you have.</p>
<p>If you can&rsquo;t answer &ldquo;how do I debug a failed event end-to-end?&rdquo; before you start building, you&rsquo;re not ready. Get observability in place first. The events can wait.</p>
]]></content:encoded></item><item><title>Database Replication Patterns That Actually Matter</title><link>https://lawzava.com/blog/2020-01-20-database-replication-patterns/</link><pubDate>Mon, 20 Jan 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-01-20-database-replication-patterns/</guid><description>A practical breakdown of replication modes, topologies, and the tradeoffs between consistency, availability, and not losing your users&amp;amp;rsquo; data at 3am.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Every replication pattern is a bet on which failure you can tolerate. Pick wrong and you either lose data or lose availability. There&rsquo;s no option C.</p>
<hr>
<p>At the fintech startup we ingested financial news and market data from dozens of sources into PostgreSQL. Millions of rows per day. The kind of dataset where &ldquo;eventually consistent&rdquo; means a trader sees yesterday&rsquo;s price and you get a very angry phone call.</p>
<p>That experience shaped how I think about replication. Not as a feature you toggle on, but as an architectural decision that determines how your system fails. Because it will fail.</p>
<h2 id="why-you-replicate">Why you replicate</h2>
<p>The reasons are fewer than people think:</p>
<ul>
<li><strong>Availability.</strong> Your primary dies, a replica takes over, users keep working.</li>
<li><strong>Read scaling.</strong> Heavy reporting queries stop competing with writes.</li>
<li><strong>Latency.</strong> Put a copy closer to users so reads don&rsquo;t cross the Atlantic.</li>
<li><strong>Isolation.</strong> Backups, analytics, migrations &ndash; all without touching production writes.</li>
</ul>
<p>That&rsquo;s basically it. If your reason isn&rsquo;t one of these, you probably don&rsquo;t need replication yet. You need a better backup strategy.</p>
<h2 id="the-three-modes">The three modes</h2>
<h3 id="asynchronous">Asynchronous</h3>
<p>The primary commits, returns success to the client, then ships WAL records to replicas whenever it gets around to it.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- postgresql.conf on the primary
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>wal_level <span style="color:#f92672">=</span> replica
</span></span><span style="display:flex;"><span>max_wal_senders <span style="color:#f92672">=</span> <span style="color:#ae81ff">5</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- No synchronous_standby_names. That&#39;s the point.
</span></span></span></code></pre></div><p>The replica connects with a <code>primary_conninfo</code> and streams changes:</p>
<pre tabindex="0"><code># recovery.conf (or standby.signal in PG12+)
primary_conninfo = &#39;host=primary-db port=5432 user=replicator&#39;
</code></pre><p>This is the fastest mode for writes. The primary doesn&rsquo;t wait for anyone. But there&rsquo;s a window &ndash; could be milliseconds, could be seconds under load &ndash; where a replica is behind. If the primary dies in that window, those transactions are gone.</p>
<p>At the fintech startup, async replication was fine for our read replicas serving the news feed. A 200ms lag on a news article? Nobody notices. A 200ms lag on a stock price used for trading signals? Different conversation entirely.</p>
<h3 id="synchronous">Synchronous</h3>
<p>The primary waits for at least one replica to confirm it wrote the WAL to disk before telling the client &ldquo;committed.&rdquo;</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- postgresql.conf
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>synchronous_standby_names <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;replica1&#39;</span>
</span></span><span style="display:flex;"><span>synchronous_commit <span style="color:#f92672">=</span> <span style="color:#66d9ef">on</span>
</span></span></code></pre></div><p>Zero data loss on failover. Sounds great. The cost: every single write now includes a network round trip to the replica. In the same datacenter, maybe 1-2ms added latency. Across regions? 50-150ms. Per write.</p>
<p>Worse: if that replica goes down, your primary blocks all writes until the replica comes back or you reconfigure. I&rsquo;ve seen this take down production systems that were &ldquo;highly available.&rdquo; The replication designed for availability became the single point of failure.</p>
<h3 id="quorum-semi-synchronous">Quorum (semi-synchronous)</h3>
<p>The pragmatic middle ground. Wait for <em>any</em> N out of M replicas to acknowledge, not all of them.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- Wait for any 1 of these 3 replicas
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>synchronous_standby_names <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;ANY 1 (replica1, replica2, replica3)&#39;</span>
</span></span></code></pre></div><p>One replica can die and writes keep flowing. You still get the durability guarantee because at least one standby has the data. This is what I&rsquo;d recommend for most production PostgreSQL setups that need strong durability.</p>
<p>The math is simple: if you have 3 standbys and require <code>ANY 1</code>, you can lose 2 replicas before writes stall. With <code>ANY 2</code>, you can lose 1. Pick based on how many failures you want to survive simultaneously.</p>
<h2 id="topologies">Topologies</h2>
<h3 id="primary-replica-the-default">Primary-replica (the default)</h3>
<p>One writer, N readers. Dead simple. Works for the vast majority of applications.</p>
<pre tabindex="0"><code>  Writes           Reads
    |                |
    v                v
[Primary] ----&gt; [Replica 1]
    |
    +---------&gt; [Replica 2]
</code></pre><p>If you&rsquo;re building a new system and someone suggests multi-primary on day one, push back hard. Start here.</p>
<h3 id="cascading-replicas">Cascading replicas</h3>
<p>A replica can feed other replicas downstream instead of every replica pulling from the primary. This matters when you have many replicas or they span continents.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- On the cascading replica (replica2 feeds from replica1, not primary)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>primary_conninfo <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;host=replica1 port=5432 user=replicator&#39;</span>
</span></span></code></pre></div><p>The tradeoff: more lag for downstream replicas. Each hop adds latency. But the primary&rsquo;s <code>max_wal_senders</code> slots aren&rsquo;t exhausted by a fleet of read replicas. We used this at the fintech startup to feed analytics replicas off a primary standby &ndash; kept the main failover target clean while analytics could thrash their copy however they wanted.</p>
<h3 id="multi-primary">Multi-primary</h3>
<p>Multiple nodes accept writes and sync with each other. Sounds appealing. Is usually painful.</p>
<p>The fundamental problem: two nodes write conflicting data to the same row at the same time. Now what?</p>
<p>PostgreSQL&rsquo;s logical replication can do this with BDR or similar extensions, but you&rsquo;re signing up for conflict resolution logic, operational complexity, and debugging sessions that make you question your career choices. MySQL Group Replication and Galera have the same fundamental tradeoffs.</p>
<p>Use multi-primary when you have offices in multiple continents that all need local write latency and the business has accepted the cost. Not because it sounds cool on a whiteboard.</p>
<h2 id="read-your-writes-consistency">Read-your-writes consistency</h2>
<p>This is where async replication bites you in the application layer.</p>
<p>User writes a comment. POST succeeds (hits the primary). Page refreshes. GET goes to a replica. Comment isn&rsquo;t there. User writes the comment again. Now you have duplicates.</p>
<p>Three approaches, from simple to robust:</p>
<p><strong>1. Sticky routing after writes</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">handleRequest</span>(<span style="color:#a6e22e">w</span> <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ResponseWriter</span>, <span style="color:#a6e22e">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Request</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">cookie</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Cookie</span>(<span style="color:#e6db74">&#34;last_write&#34;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">cookie</span> <span style="color:#f92672">!=</span> <span style="color:#66d9ef">nil</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Since</span>(<span style="color:#a6e22e">parseCookieTime</span>(<span style="color:#a6e22e">cookie</span>)) &lt; <span style="color:#ae81ff">5</span><span style="color:#f92672">*</span><span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Second</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#75715e">// Recent write -- route to primary</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">db</span> = <span style="color:#a6e22e">primaryDB</span>
</span></span><span style="display:flex;"><span>    } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">db</span> = <span style="color:#a6e22e">replicaDB</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// ...</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Crude but effective. After a write, force reads to the primary for a few seconds. The window just needs to exceed your typical replication lag.</p>
<p><strong>2. Session affinity to a specific replica</strong></p>
<p>Pin a user session to one replica. They won&rsquo;t see inconsistency within their own session because they always read from the same copy. Doesn&rsquo;t help with cross-user consistency but handles the most visible problem.</p>
<p><strong>3. LSN tracking</strong></p>
<p>The proper solution. After a write, capture the WAL position (LSN) from the primary. Before reading from a replica, check if the replica has replayed past that LSN.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- After write, on the primary:
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">SELECT</span> pg_current_wal_lsn();
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- Returns something like: 0/16B9188
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- Before read, on the replica:
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">SELECT</span> pg_last_wal_replay_lsn();
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- If this &gt;= the saved LSN, the replica is caught up
</span></span></span></code></pre></div><p>More work to implement. More correct.</p>
<h2 id="conflict-resolution-in-multi-primary">Conflict resolution in multi-primary</h2>
<p>If you went down the multi-primary path despite my warning, you need a conflict strategy.</p>
<p><strong>Last write wins (LWW).</strong> Timestamp comparison. Whichever write has the later timestamp survives. Simple, lossy. A perfectly valid update gets silently dropped because a clock was 1ms behind. If your data can tolerate silent overwrites, fine. Financial data can&rsquo;t.</p>
<p><strong>Application-level merge.</strong> You define the merge logic per table or per field. A counter gets added. A set gets unioned. A text field takes the longer version. This works but every schema change means updating merge rules.</p>
<p><strong>CRDTs.</strong> Conflict-free replicated data types. Mathematically guaranteed to converge. Limited to specific data structures: counters, sets, registers. You can&rsquo;t CRDT your way through an arbitrary relational schema. Good for specific use cases, not a general solution.</p>
<h2 id="failover">Failover</h2>
<p>Promoting a replica sounds easy. <code>pg_promote()</code> or <code>SELECT pg_promote()</code> in PG12+. Done. Except no.</p>
<p>The hard parts:</p>
<p><strong>Fencing the old primary.</strong> If the old primary isn&rsquo;t actually dead &ndash; just slow, or network-partitioned &ndash; you now have two nodes accepting writes. Split brain. The nightmare scenario. You need STONITH (shoot the other node in the head) or at minimum, revoke the old primary&rsquo;s ability to accept connections.</p>
<p><strong>Client reconnection.</strong> Your application&rsquo;s connection string points to the old primary. Now it needs to point to the new one. Options: DNS update (slow propagation), virtual IP (fast but requires infra), connection proxy like PgBouncer or HAProxy that handles routing, or application-level logic.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># HAProxy config for automatic failover routing</span>
</span></span><span style="display:flex;"><span><span style="color:#ae81ff">listen postgres</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">bind *:5432</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">option httpchk GET /primary</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">server pg1 10.0.1.1:5432 check port 8008</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ae81ff">server pg2 10.0.1.2:5432 check port 8008</span>
</span></span></code></pre></div><p><strong>Rebuilding the old primary.</strong> After failover, the old primary has diverged. It accepted some writes the new primary doesn&rsquo;t have (or vice versa). You need to either <code>pg_rewind</code> it or rebuild from scratch with <code>pg_basebackup</code>. Test this process <em>before</em> you need it.</p>
<p>I&rsquo;ve run failover drills at every company I&rsquo;ve worked at. The drill always surfaces something the documentation missed. Always.</p>
<h2 id="monitoring-replication">Monitoring replication</h2>
<p>Replication lag is a ticking bomb with a variable fuse. You need to watch it continuously.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- On the primary: check all connected replicas
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">SELECT</span>
</span></span><span style="display:flex;"><span>    client_addr,
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">state</span>,
</span></span><span style="display:flex;"><span>    sent_lsn,
</span></span><span style="display:flex;"><span>    write_lsn,
</span></span><span style="display:flex;"><span>    flush_lsn,
</span></span><span style="display:flex;"><span>    replay_lsn,
</span></span><span style="display:flex;"><span>    pg_wal_lsn_diff(sent_lsn, replay_lsn) <span style="color:#66d9ef">AS</span> replay_lag_bytes
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> pg_stat_replication;
</span></span></code></pre></div><p>Key things to alert on:</p>
<ul>
<li><strong>Replay lag exceeding your tolerance.</strong> If you promised &ldquo;reads within 1 second of writes,&rdquo; alert before that threshold.</li>
<li><strong>Replica disconnection.</strong> A replica that silently falls off is worse than one that loudly crashes.</li>
<li><strong>WAL accumulation on the primary.</strong> If a replica can&rsquo;t keep up, the primary retains WAL segments. Disk fills. Primary crashes. You&rsquo;ve now lost the thing that was supposed to protect you.</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-sql" data-lang="sql"><span style="display:flex;"><span><span style="color:#75715e">-- Check WAL retention on the primary
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">SELECT</span> pg_wal_lsn_diff(pg_current_wal_lsn(), <span style="color:#e6db74">&#39;0/0&#39;</span>) <span style="color:#66d9ef">AS</span> total_wal_bytes;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- Or more practically, watch the pg_wal directory size
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">-- and alert when it grows beyond expected bounds.
</span></span></span></code></pre></div><h2 id="choosing-your-pattern">Choosing your pattern</h2>
<p>Skip the decision matrix. Ask three questions:</p>
<p><strong>Can you lose any committed transactions?</strong> If no, synchronous or quorum replication. Full stop. This was non-negotiable for the financial data at the fintech startup. A lost trade record isn&rsquo;t a bug, it&rsquo;s a regulatory incident.</p>
<p><strong>Do you need writes in multiple regions?</strong> If no &ndash; and it&rsquo;s almost always no &ndash; use primary-replica. If yes, accept the operational cost of multi-primary and budget engineering time accordingly. At Decloud, we keep writes centralized and replicate reads out. Simpler. Fewer 3am pages.</p>
<p><strong>What&rsquo;s your read-to-write ratio?</strong> If reads dominate (10:1 or more), async replicas for reads with synchronous replication to a single failover standby gives you the best of both: fast reads, safe failover, and write performance that&rsquo;s only slightly penalized.</p>
<hr>
<p>Replication isn&rsquo;t a feature flag. It&rsquo;s a contract between your system and your users about what happens when hardware fails, networks partition, and Murphy&rsquo;s Law does its thing. Pick the pattern that matches the promises you&rsquo;ve actually made, not the ones you wish you could make.</p>
]]></content:encoded></item><item><title>Most Edge Computing Projects Are Premature Optimization</title><link>https://lawzava.com/blog/2019-11-18-edge-computing-architecture/</link><pubDate>Mon, 18 Nov 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-11-18-edge-computing-architecture/</guid><description>Edge computing is real, but most teams adopting it don&amp;amp;rsquo;t have an edge problem. They have an architecture problem they&amp;amp;rsquo;re solving with geography.</description><content:encoded><![CDATA[<p>I keep seeing startups pitch edge computing like it&rsquo;s the next container revolution. Investors nod. Architects draw diagrams with little boxes at &ldquo;the edge.&rdquo; Nobody asks the obvious question: do you actually have a latency problem that geography can fix?</p>
<p>At a mobility startup we processed GPS telemetry from thousands of bikes across the city. Real IoT. Real device data. Real volume. You&rsquo;d think that&rsquo;s a textbook edge case. It wasn&rsquo;t. A well-tuned message queue and a couple of regional cloud instances handled everything we needed. Sub-second was easy without edge infrastructure.</p>
<h2 id="the-edge-is-a-spectrum-not-a-destination">The edge is a spectrum, not a destination</h2>
<p>Device. Network edge. Regional edge. Cloud. Each hop adds latency, sure. But each hop closer to the device also adds operational pain: deployment complexity, consistency headaches, debugging in the dark.</p>
<p>The math only works when you have a genuine constraint:</p>
<ul>
<li><strong>Latency below 50ms</strong> on a critical path. Not aspirational latency. Measured, user-impacting latency.</li>
<li><strong>Bandwidth costs that actually hurt.</strong> If you&rsquo;re shipping raw video or sensor streams, processing locally makes sense. If you&rsquo;re sending JSON, it doesn&rsquo;t.</li>
<li><strong>Unreliable connectivity.</strong> Factory floors, moving vehicles, rural deployments. The network genuinely can&rsquo;t be trusted.</li>
<li><strong>Data residency requirements.</strong> Regulation says the data stays put. Fine. That&rsquo;s a real constraint.</li>
</ul>
<p>If none of those apply, you&rsquo;re adding distributed systems complexity for a problem you don&rsquo;t have.</p>
<h2 id="what-edge-computing-actually-costs-you">What edge computing actually costs you</h2>
<p>Consistency goes out the window. You&rsquo;re now designing for eventual convergence, conflict resolution, and partial failures at every node. Your deployment pipeline needs to handle hundreds of locations instead of a handful of regions. Observability becomes sampling and aggregation because you can&rsquo;t ship raw telemetry from every edge node without defeating the purpose.</p>
<p>Security surface area multiplies. Every edge node is a potential compromise point. You&rsquo;re trusting code running in locations you don&rsquo;t physically control.</p>
<p>Now at Decloud, we&rsquo;re building cloud infrastructure tooling. I see teams adopt edge patterns and then spend months building the operational scaffolding that a centralized architecture gives you for free. The edge didn&rsquo;t solve their problem. It replaced one set of problems with a harder set.</p>
<h2 id="when-its-actually-worth-it">When it&rsquo;s actually worth it</h2>
<p>CDN edge compute is the one place where the tradeoff is almost always favorable. Auth checks, request routing, simple personalization &ndash; these are stateless, short-lived operations running on infrastructure someone else manages. Cloudflare Workers and similar products nailed this by keeping the programming model simple and the blast radius small.</p>
<p>Beyond CDN, edge computing earns its complexity in exactly three scenarios: real-time industrial control, high-volume media processing at the source, and latency-sensitive interactive applications where every millisecond is measurable in revenue.</p>
<p>Everything else? Run it in the cloud. Optimize your queries. Use a CDN. Move on.</p>
<h2 id="bottom-line">Bottom line</h2>
<p>Edge computing is a targeted optimization, not an architecture paradigm. Treating it as a default is resume-driven development dressed up as forward thinking. Start with the simplest thing that works, measure where it doesn&rsquo;t, and push compute to the edge only when the numbers force your hand.</p>
]]></content:encoded></item><item><title>You Probably Don't Need Multi-Region</title><link>https://lawzava.com/blog/2019-06-17-multi-region-architecture/</link><pubDate>Mon, 17 Jun 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-06-17-multi-region-architecture/</guid><description>Multi-region is a commitment most teams make too early. When it actually pays off, the patterns that work, and why data is the part that ruins your week.</description><content:encoded><![CDATA[<p>At the fintech startup we served financial data to users across Europe, Asia, and the US. Latency mattered because stale stock data is worse than no stock data. We went multi-region not because it was trendy but because a user in Singapore waiting 400ms for a London API response was a user who stopped trusting the product.</p>
<p>That experience taught me something most architecture blogs skip over: multi-region isn&rsquo;t a scaling decision. It&rsquo;s a commitment. You&rsquo;re signing up for a permanent increase in operational surface area, and most teams underestimate what that means on a Tuesday night when replication lag spikes and nobody remembers which region owns writes.</p>
<h2 id="when-it-actually-makes-sense">When It Actually Makes Sense</h2>
<p>Three situations. That&rsquo;s it.</p>
<p><strong>Your users are genuinely global and latency affects the product.</strong> Not &ldquo;we have a few customers in Asia.&rdquo; I mean latency is measurably hurting conversion, trust, or functionality. At the fintech startup, financial data delayed by hundreds of milliseconds was functionally wrong. That&rsquo;s a real reason.</p>
<p><strong>Compliance requires data residency.</strong> GDPR was forcing this conversation for European users. If you must keep EU citizen data in EU, you need a region there. No architecture cleverness gets around legal requirements.</p>
<p><strong>A regional outage is an existential threat.</strong> If your product going down for four hours costs more than running a second region for a year, the math works. For most startups I&rsquo;ve seen &ndash; including what we&rsquo;re building now at Decloud in my founder-program cohort &ndash; it doesn&rsquo;t.</p>
<h2 id="when-it-doesnt">When It Doesn&rsquo;t</h2>
<p>Here is a test I use: if your team doesn&rsquo;t have 24/7 on-call coverage today, you aren&rsquo;t ready for multi-region. Full stop.</p>
<p>A second region doesn&rsquo;t help if nobody is awake to failover when it matters. I&rsquo;ve watched startups deploy to three regions and then have a single engineer handle incidents at 3am because &ldquo;the architecture is redundant.&rdquo; The architecture isn&rsquo;t the bottleneck. The people are.</p>
<p>Single-region with multiple availability zones handles most failure scenarios. It&rsquo;s cheaper, the mental model is simpler, and your deploys don&rsquo;t need a coordination protocol. If you&rsquo;re a team under twenty engineers, start here and stay here until the pain is specific and measurable.</p>
<h2 id="the-patterns-briefly">The Patterns, Briefly</h2>
<p><strong>Active-passive</strong> is the safe choice. One region handles writes. The other sits warm, ready for failover. You pay for underutilized infrastructure, but your data model stays sane. This is what I would recommend for most teams dipping a toe in.</p>
<p><strong>Active-active</strong> is the ambitious choice. Both regions serve traffic and accept writes. Latency improves for everyone. Failover is faster because traffic already flows everywhere. But now you need conflict resolution, and conflict resolution is where engineers go to suffer.</p>
<p><strong>Follow-the-sun</strong> is niche. The primary region rotates with time zones. It works for batch workloads and internal tools. I&rsquo;ve never seen it work well for user-facing products.</p>
<h2 id="data-will-ruin-your-week">Data Will Ruin Your Week</h2>
<p>Every multi-region conversation eventually becomes a data conversation. You can handwave traffic routing and load balancing. You can&rsquo;t handwave &ldquo;what happens when two regions write to the same row at the same time.&rdquo;</p>
<p>Classify your data before you do anything else:</p>
<ul>
<li><strong>Global and consistent:</strong> User accounts, billing state, permissions. This data must be correct everywhere. Synchronous replication or a single write region.</li>
<li><strong>Regional and isolated:</strong> User-generated content tied to geography, local caches. This can live in one region without drama.</li>
<li><strong>Derived and disposable:</strong> Caches, search indexes, computed feeds. Rebuild it if it breaks. Don&rsquo;t replicate it.</li>
</ul>
<p>Synchronous replication gives you consistency but adds cross-region latency to every write. Async replication keeps things fast but introduces eventual consistency, which is a polite way of saying &ldquo;your users might see stale data and you need a plan for that.&rdquo;</p>
<p>Conflict resolution deserves its own paragraph because it deserves your fear. Last-write-wins sounds simple until you realize your clock synchronization across regions isn&rsquo;t as tight as you assumed. Application-level merge logic sounds correct until you realize every new feature needs to account for it. CRDTs sound elegant until you realize they only work for specific data structures. Pick your poison. Test it under failure conditions, not just happy paths.</p>
<h2 id="traffic-routing">Traffic Routing</h2>
<p>Three options, increasing in complexity:</p>
<p><strong>DNS-based routing</strong> is cheap and simple. It&rsquo;s also slow to failover because DNS caching means some users will hit the wrong region for minutes after a switch. Fine for read-heavy traffic. Dangerous if you need fast failover.</p>
<p><strong>Global load balancers</strong> give you health checks and faster failover. They cost more and add operational surface. Worth it if your availability targets demand sub-minute recovery.</p>
<p><strong>Application-level routing</strong> lets clients or APIs pick a region based on account or data ownership. Maximum flexibility. Maximum chance of a subtle routing bug sending writes to the wrong region.</p>
<h2 id="the-honest-cost">The Honest Cost</h2>
<p>Multi-region costs aren&rsquo;t just &ldquo;two of everything.&rdquo; The infrastructure doubling is the easy part to budget. The hard costs are:</p>
<ul>
<li>Cross-region data transfer fees. These add up fast and nobody notices until the bill arrives.</li>
<li>Doubled deploy pipelines, doubled monitoring, doubled alerting noise.</li>
<li>On-call engineers who now need to understand two regions and the interactions between them.</li>
<li>Every new feature ships slower because someone has to ask &ldquo;does this work multi-region?&rdquo;</li>
</ul>
<p>At a startup accelerator, I watched teams burn runway on infrastructure sophistication that their user base didn&rsquo;t justify. The startup that nailed single-region reliability shipped faster than the one with a beautiful multi-region setup and a three-person team drowning in operational overhead.</p>
<h2 id="my-actual-advice">My Actual Advice</h2>
<p>If you&rsquo;re reading this in 2019 and wondering whether to go multi-region: probably don&rsquo;t. Deploy to a single region with multiple availability zones. Set up proper backups and a disaster recovery plan. Get your deployment pipeline fast enough that you can ship fixes in minutes, not hours.</p>
<p>When the pain becomes specific &ndash; real latency complaints from real users in a real geography, a compliance requirement with a real deadline, a post-mortem that shows a regional outage cost real money &ndash; then revisit. Start with active-passive. Keep your write path simple. Accept that you&rsquo;re trading velocity for resilience and make that trade deliberately.</p>
<p>Multi-region isn&rsquo;t wrong. But it&rsquo;s almost never urgent, and doing it before you&rsquo;re ready makes everything else slower.</p>
]]></content:encoded></item><item><title>Design for Failure or It Will Design Your Weekend</title><link>https://lawzava.com/blog/2019-05-06-designing-for-failure/</link><pubDate>Mon, 06 May 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-05-06-designing-for-failure/</guid><description>Failure is not an edge case but the default state you hold off with good engineering. Hard-won rules for systems that bend instead of shatter.</description><content:encoded><![CDATA[<p>I&rsquo;m halfway through my founder-program cohort, building Decloud, and I keep having the same conversation with other founders here: &ldquo;We&rsquo;ll handle reliability later.&rdquo; Later. The word that has personally cost me more sleep than any production bug.</p>
<p>At the fintech startup, I watched a single slow Elasticsearch query cascade through our entire API layer. One degraded dependency. Total platform outage. The fix took ten minutes. The recovery took four hours. All because nothing in the request path had a timeout.</p>
<p>At a mobility startup, a forgotten WAL retention setting filled a disk at 3 AM and I discovered our &ldquo;tested&rdquo; failover was eleven hours behind. Forty minutes of locked bikes across the city. The monitoring said everything was fine. The monitoring was wrong.</p>
<p>These weren&rsquo;t exotic failures. They were boring, preventable ones. The kind that happen when you assume dependencies work and never verify what happens when they don&rsquo;t.</p>
<h3 id="three-rules-i-actually-follow">Three rules I actually follow</h3>
<p><strong>Set a deadline on everything.</strong> Every outbound call gets a timeout. Every request gets a budget. If a dependency can&rsquo;t answer in time, you move on without it. Slow failure is worse than fast failure because it holds resources hostage while it dies.</p>
<p><strong>Isolate the blast radius.</strong> A slow search index should never starve your payment flow. Separate connection pools. Separate queues. The goal is simple: one problem stays one problem.</p>
<p><strong>Know your fallback before you need it.</strong> A stale cache hit is better than a 500. A default list of popular items is better than a blank page. But the fallback has to be intentional. Accidental fallbacks are just bugs you haven&rsquo;t noticed yet.</p>
<h3 id="the-pattern-that-keeps-saving-me">The pattern that keeps saving me</h3>
<p>Circuit breakers. Dead simple concept. If a dependency is failing, stop calling it. Serve the fallback. Check back later. It turns a cascading outage into a graceful degradation that most users never notice.</p>
<p>The key insight: a breaker that&rsquo;s open isn&rsquo;t a failure state. It&rsquo;s a success state. It means the system chose fast, predictable behavior over slow, unpredictable death.</p>
<h3 id="what-i-got-wrong-early-on">What I got wrong early on</h3>
<p>I used to think resilience meant more redundancy. Add a replica. Add a region. Add a retry. But redundancy without testing is just a more expensive single point of failure. That mobility startup&rsquo;s replica was a perfect example. It existed. It was running. It was useless.</p>
<p>Now I test the recovery path, not just the happy path. If you haven&rsquo;t promoted your replica under realistic conditions in the last quarter, you don&rsquo;t have a failover. You have a hope.</p>
<h3 id="the-uncomfortable-truth">The uncomfortable truth</h3>
<p>Designing for failure isn&rsquo;t a technical problem. It&rsquo;s a prioritization problem. Every founder and every CTO knows they should do it. Most don&rsquo;t because the next feature feels more urgent. It always feels more urgent.</p>
<p>Until 3 AM on a Thursday, when it doesn&rsquo;t.</p>
]]></content:encoded></item><item><title>What Building Distributed Systems at a Fintech Startup Taught Me About Failure</title><link>https://lawzava.com/blog/2018-09-17-building-reliable-distributed-systems/</link><pubDate>Mon, 17 Sep 2018 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2018-09-17-building-reliable-distributed-systems/</guid><description>Hard-won lessons from designing distributed systems that survive real failures &amp;amp;ndash; timeouts, retries, bulkheads, and the habits that keep things running.</description><content:encoded><![CDATA[<p>A few months into my time as CTO at the fintech startup, our financial news aggregation pipeline went silent. No errors. No alerts. Just&hellip; nothing. Users saw stale headlines. The dashboard looked green. Took us twenty minutes to figure out what happened: a single downstream API had started responding with 200 OK and empty bodies. Every service downstream trusted that response, cached the emptiness, and served it proudly. We had built a system that could tolerate crashes, but not a liar.</p>
<p>That incident rewired how I think about distributed systems. The spectacular failures &ndash; network partitions, node crashes, disk corruption &ndash; those are almost easy. You plan for them because they&rsquo;re dramatic. The killers are the subtle ones. The slow dependency that doesn&rsquo;t quite timeout. The clock drift that makes your event ordering nonsensical. The duplicate message that creates two charges on someone&rsquo;s credit card.</p>
<h2 id="partial-failure-is-the-default-state">Partial Failure Is the Default State</h2>
<p>At the fintech startup we pulled financial data from dozens of sources, ran NLP pipelines, scored relevance, and served personalized feeds. At any given moment, something in that chain was degraded. Not down. Degraded. A source returning stale data. A scoring service running hot. A queue backing up.</p>
<p>Once I accepted that partial failure is the <em>normal</em> operating condition, everything clicked. You don&rsquo;t design for &ldquo;what if something breaks.&rdquo; You design for &ldquo;something is always broken, how do we still deliver value?&rdquo;</p>
<p>That means setting explicit targets. Not vague &ldquo;five nines&rdquo; aspirations, but concrete numbers: P99 latency under 400ms for feed requests, error rate below 0.1% for content delivery. Then you design every component to hold those targets even when a dependency is misbehaving.</p>
<h2 id="the-patterns-that-actually-saved-us">The Patterns That Actually Saved Us</h2>
<h3 id="timeouts-everywhere-no-exceptions">Timeouts. Everywhere. No Exceptions.</h3>
<p>I can&rsquo;t overstate this. Every network call needs a timeout. Not just HTTP requests &ndash; database queries, cache lookups, message publishes. All of them.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span>response <span style="color:#f92672">=</span> http<span style="color:#f92672">.</span>get(url, timeout<span style="color:#f92672">=</span><span style="color:#ae81ff">2</span>)
</span></span></code></pre></div><p>Two seconds. That&rsquo;s it. If your financial data provider can&rsquo;t answer in two seconds, we move on. We had a service early on with no timeout on a third-party API call. That provider had a bad day, responses went from 200ms to 45 seconds, and our entire request pipeline backed up behind it. Thread pools exhausted. Cascading failure across three services. All because of one missing timeout.</p>
<p>Even better: use end-to-end deadlines. If the user&rsquo;s request has a 3-second budget, propagate that deadline downstream. Don&rsquo;t let an inner service burn 2.8 seconds and leave nothing for the rest of the chain.</p>
<h3 id="retries-that-dont-make-things-worse">Retries That Don&rsquo;t Make Things Worse</h3>
<p>Retries are a double-edged sword. Done right, they mask transient blips. Done wrong, they turn a struggling service into a dead one.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">for</span> attempt <span style="color:#f92672">in</span> range(max_retries):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> call()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">except</span> RetryableError:
</span></span><span style="display:flex;"><span>        sleep(backoff(attempt))
</span></span></code></pre></div><p>Exponential backoff with jitter. That jitter part matters &ndash; without it, all your retrying clients slam the recovering service at the exact same intervals. Thundering herd. We learned to cap retry budgets too. Three attempts max, then fall back to cached data or a degraded response. Retrying forever is just a distributed denial-of-service attack against yourself.</p>
<h3 id="circuit-breakers">Circuit Breakers</h3>
<p>After the empty-response incident, we added circuit breakers on every external dependency. If a service fails five times in a row, stop calling it. Try again in 30 seconds with a single probe request. If the probe succeeds, gradually let traffic back through.</p>
<p>This is the pattern that prevented the most cascading failures for us. Simple concept, massive impact.</p>
<h3 id="bulkheads">Bulkheads</h3>
<p>We ran our NLP pipeline and our content delivery API on shared infrastructure early on. The NLP jobs were CPU-hungry beasts. During a big news event &ndash; earnings season, a market crash &ndash; the pipeline would spike and starve the API of resources.</p>
<p>Solution: isolation. Separate thread pools, separate connection pools, separate queues. One misbehaving component can&rsquo;t eat another component&rsquo;s lunch. Same principle as watertight compartments on a ship. The Titanic metaphor is overused but accurate.</p>
<h3 id="fallbacks-over-failures">Fallbacks Over Failures</h3>
<p>When our relevance scoring service was slow, we didn&rsquo;t show users an error page. We showed them a feed sorted by recency instead. Less personalized, still useful. When a data source was down, we served cached content with a &ldquo;last updated&rdquo; timestamp.</p>
<p>Define what a degraded-but-useful response looks like for every endpoint. Caches, defaults, partial results. Users almost always prefer stale data over no data.</p>
<h3 id="idempotency">Idempotency</h3>
<p>In a system with retries and at-least-once message delivery, duplicate operations are inevitable. Every write operation needs to be safe to execute twice.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-http" data-lang="http"><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">POST /orders
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">Idempotency-Key: 6f9c2c70-4f0e-4f36-8a6a-3b5a9f4e3c2d
</span></span></span></code></pre></div><p>We tagged every content ingestion event with a unique key. If the same article arrived twice from different sources or from a retry, the system recognized the duplicate and skipped it. Without this, our feeds would have been full of repeated headlines during any period of instability.</p>
<h2 id="consistency-pick-your-battles">Consistency: Pick Your Battles</h2>
<p>Strong consistency across a distributed system is expensive and fragile. We tried it early on with a two-phase commit pattern for content updates. It was slow, it was brittle, and it failed in weird ways under load.</p>
<p>We switched to eventual consistency with explicit conflict resolution. Version numbers on every content record. Last-writer-wins for most fields, merge logic for aggregated scores. Sagas with compensating actions for multi-step workflows &ndash; if step three fails, step two gets rolled back automatically.</p>
<p>The key insight: design your conflict resolution strategy <em>before</em> you need it. Under pressure at 2am isn&rsquo;t when you want to be inventing reconciliation logic.</p>
<h2 id="operations-are-half-the-battle">Operations Are Half the Battle</h2>
<p>The best-designed system falls apart without operational discipline.</p>
<p><strong>Health checks</strong>: We exposed both liveness (process is running) and readiness (process can serve traffic and reach its dependencies) endpoints. Kubernetes used these to route around unhealthy instances automatically.</p>
<p><strong>Observability</strong>: Metrics for rates and latency. Logs for detail. Distributed tracing for following a request across services. After the empty-response incident, we added content-validity checks to our health signals. Green dashboard means nothing if you&rsquo;re not checking the right things.</p>
<p><strong>Load shedding</strong>: When traffic spikes beyond capacity, reject requests explicitly with a 503 rather than letting everything slow to a crawl. Backpressure propagated through the system keeps critical paths alive.</p>
<p><strong>Safe deployments</strong>: Canary releases. Feature flags. Fast rollback. We never did big-bang deployments after the first time it went wrong (and it went wrong immediately).</p>
<h2 id="test-for-failure-not-just-success">Test for Failure, Not Just Success</h2>
<p>Happy-path tests prove your system works when everything is fine. That&rsquo;s the easy part.</p>
<p>We injected latency into staging environments. Killed instances randomly. Returned garbage from mocked dependencies. Simulated network partitions between services. Every one of these tests caught bugs that would have hit production eventually.</p>
<p>Load testing matters too, but don&rsquo;t just test peak throughput. Test sustained load. Test spiky traffic. Test what happens when load ramps up while a dependency is already degraded. That combination is what actually happens in production.</p>
<h2 id="the-real-lesson">The Real Lesson</h2>
<p>Every distributed system I&rsquo;ve worked on has taught me the same thing: failure isn&rsquo;t an edge case. It&rsquo;s an input. The question is never &ldquo;will something fail?&rdquo; It&rsquo;s &ldquo;when this fails, does the system do something reasonable?&rdquo;</p>
<p>At the fintech startup, the systems that survived best weren&rsquo;t the ones with the cleverest algorithms or the most redundancy. They were the ones where we&rsquo;d thought through every dependency and asked: &ldquo;What do we do when this is gone?&rdquo; And then actually built the answer.</p>
]]></content:encoded></item><item><title>Why Monitoring Wasn't Enough and How We Built Observability at a Fintech Startup</title><link>https://lawzava.com/blog/2018-07-09-observability-beyond-monitoring/</link><pubDate>Mon, 09 Jul 2018 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2018-07-09-observability-beyond-monitoring/</guid><description>After a mystery outage that our dashboards couldn&amp;amp;rsquo;t explain, I rebuilt the fintech startup&amp;amp;rsquo;s telemetry stack around metrics, logs, and traces. Here&amp;amp;rsquo;s what I learned.</description><content:encoded><![CDATA[<p>It was 2 AM on a Wednesday, and our news ingestion pipeline at the fintech startup had gone silent. No errors in the logs. No alerts firing. CPU fine, memory fine, disk fine. Every dashboard said the system was healthy. But users were seeing stale financial news, some of it hours old.</p>
<p>I spent forty minutes SSH-ing into boxes, tailing logs, and grepping for exceptions. Nothing. The pipeline just&hellip; stopped processing. It took me another hour to discover the root cause: a third-party API we depended on had started returning empty 200 responses instead of actual data. Our monitoring checked for errors. It checked for timeouts. It never checked for &ldquo;success that contains nothing useful.&rdquo;</p>
<p>That night changed how I think about production systems.</p>
<h2 id="monitoring-checks-boxes-observability-answers-questions">Monitoring Checks Boxes. Observability Answers Questions.</h2>
<p>Monitoring is built around things you already know can go wrong. Threshold crossed, alert fires, runbook engaged. It works great for predictable problems: disk filling up, CPU pegged, error rate spiking. We had all of that at the fintech startup. Grafana dashboards everywhere. PagerDuty wired up. It felt safe.</p>
<p>But distributed systems don&rsquo;t fail in predictable ways. They fail in weird, combinatorial, never-seen-this-before ways. And that&rsquo;s where monitoring falls apart. You can&rsquo;t write an alert for a failure mode you haven&rsquo;t imagined yet.</p>
<p>Observability flips the model. Instead of predefining what questions the system can answer, you instrument it richly enough that you can ask <em>new</em> questions on the fly. During an incident. At 2 AM. Without deploying anything.</p>
<p>The difference: monitoring tells you something is wrong. Observability helps you figure out <em>why</em>.</p>
<h2 id="three-signals-one-story">Three Signals, One Story</h2>
<p>After the empty-200 incident, I started rebuilding our telemetry around three pillars.</p>
<p><strong>Metrics</strong> give you the bird&rsquo;s-eye view. Aggregated numbers over time. At the fintech startup, we track request rates, error rates, and latency distributions for every service using Prometheus.</p>
<pre tabindex="0"><code>http_requests_total{method=&#34;GET&#34;, endpoint=&#34;/api/stories&#34;, status=&#34;200&#34;} 15234
http_request_duration_seconds_bucket{endpoint=&#34;/api/stories&#34;, le=&#34;0.5&#34;} 421
</code></pre><p>Metrics are cheap to store and great for alerting. But they can&rsquo;t tell you why a specific request failed or what user it affected. They&rsquo;re the smoke detector, not the fire investigator.</p>
<p><strong>Logs</strong> capture the details. Each event, each error, each decision the code made. We moved early to structured JSON logs because free-form strings are nearly useless at scale.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{<span style="color:#f92672">&#34;timestamp&#34;</span>:<span style="color:#e6db74">&#34;2018-07-09T10:30:45Z&#34;</span>,<span style="color:#f92672">&#34;level&#34;</span>:<span style="color:#e6db74">&#34;error&#34;</span>,<span style="color:#f92672">&#34;service&#34;</span>:<span style="color:#e6db74">&#34;ingestion&#34;</span>,<span style="color:#f92672">&#34;request_id&#34;</span>:<span style="color:#e6db74">&#34;abc123&#34;</span>,<span style="color:#f92672">&#34;message&#34;</span>:<span style="color:#e6db74">&#34;Empty response from provider&#34;</span>,<span style="color:#f92672">&#34;provider&#34;</span>:<span style="color:#e6db74">&#34;reuters&#34;</span>,<span style="color:#f92672">&#34;duration_ms&#34;</span>:<span style="color:#ae81ff">340</span>}
</span></span></code></pre></div><p>That structured format meant we could query logs in Kibana instead of grepping through them. Game changer for incident response.</p>
<p><strong>Traces</strong> show you the journey of a single request across services. This was the missing piece for us. The fintech startup&rsquo;s architecture involves an ingestion service talking to NLP processors talking to a ranking engine talking to the API layer. When something is slow, you need to see the whole chain.</p>
<pre tabindex="0"><code>Trace abc123
- POST /ingest/batch (120ms)
  - NLP enrichment (45ms)
  - Relevance scoring (30ms)
  - db.write stories (35ms)
</code></pre><p>We started with Zipkin. Not perfect, but suddenly I could see that our NLP service was adding 200ms of latency on certain content types. That was invisible before.</p>
<h2 id="the-glue-correlation-ids">The Glue: Correlation IDs</h2>
<p>Each signal alone is useful. Together, connected by a shared identifier, they&rsquo;re powerful.</p>
<p>Here&rsquo;s the workflow that actually matters: a latency alert fires from metrics. You pull up the trace for a slow request. The trace shows a specific span taking too long. You jump to the logs for that span using the same request ID and find the exact query and error.</p>
<pre tabindex="0"><code>X-Request-ID: 5f3c9e86c2c84e1b
X-B3-TraceId: 4d1e00a3b9bd1d42
X-B3-SpanId: 6df3a1c2b93f6b1a
</code></pre><p>We made it a rule: every log line includes the request ID, and every trace propagates context headers. No exceptions. It took weeks to retrofit across all our services, but the payoff during the next incident was immediate. Instead of an hour of archaeology, I had a clear thread to pull.</p>
<h2 id="instrument-the-critical-path-first">Instrument the Critical Path First</h2>
<p>When I started adding instrumentation at the fintech startup, the temptation was to instrument everything. Don&rsquo;t do that. You&rsquo;ll drown in data and your storage costs will spike.</p>
<p>Start with what matters: the critical path your users depend on.</p>
<p>For us that meant inbound API handlers, the news ingestion pipeline, database calls, and every external API dependency. We used RED (rate, errors, duration) for our services and USE (utilization, saturation, errors) for infrastructure resources.</p>
<p>Tracing has a sampling problem. Capturing every single request is expensive. We settled on keeping 100% of errors and sampling successful requests at about 5%. During incidents, we crank sampling up. Good enough for debugging, affordable enough to run continuously.</p>
<h2 id="designing-systems-that-can-be-debugged">Designing Systems That Can Be Debugged</h2>
<p>Observability isn&rsquo;t something you bolt on after the fact. It&rsquo;s a design choice.</p>
<p>Structured logs over free-form strings. Always. If you can&rsquo;t query it, it&rsquo;s useless when you&rsquo;re under pressure at 2 AM.</p>
<p>Watch your cardinality. Metrics with unbounded label values (like user IDs) will destroy your Prometheus instance. High-cardinality data belongs in logs and traces, not metrics.</p>
<p>Add context that matters. A trace that only shows timings is half the story. Include the request type, the tenant, the result count. When something goes wrong, that context is the difference between a five-minute fix and a two-hour hunt.</p>
<h2 id="our-stack-in-2018">Our Stack in 2018</h2>
<p>For anyone building this out now, here&rsquo;s what we were running:</p>
<ul>
<li><strong>Metrics</strong>: Prometheus with Alertmanager, Grafana for dashboards</li>
<li><strong>Logs</strong>: Fluentd piping into Elasticsearch, Kibana for exploration</li>
<li><strong>Tracing</strong>: Zipkin with OpenTracing instrumentation</li>
</ul>
<p>OpenTracing is getting solid adoption and keeps us from being locked into one vendor. OpenCensus is emerging as an alternative worth watching. Both are pushing toward a world where trace context propagation just works out of the box.</p>
<p>There are commercial options that bundle everything behind one query layer. We looked at them. For our scale, the open source stack made more sense. That calculus changes depending on team size and how much operational overhead you can absorb.</p>
<h2 id="what-that-2-am-incident-taught-me">What That 2 AM Incident Taught Me</h2>
<p>That silent pipeline failure was a gift. It exposed a blind spot in how I thought about production readiness. Having dashboards isn&rsquo;t the same as having understanding. Monitoring answers the questions you thought to ask. Observability gives you the ability to investigate the questions you didn&rsquo;t.</p>
<p>We still have incidents at the fintech startup. But now when something breaks in a way we&rsquo;ve never seen before, we have the telemetry to figure it out fast. That&rsquo;s the whole point.</p>
]]></content:encoded></item><item><title>Event Sourcing in Practice: What I Got Right and Wrong</title><link>https://lawzava.com/blog/2018-03-19-designing-event-sourced-systems/</link><pubDate>Mon, 19 Mar 2018 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2018-03-19-designing-event-sourced-systems/</guid><description>Lessons from building event-sourced systems at the fintech startup &amp;amp;ndash; the patterns that held up, the modeling mistakes, and the operational realities.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Event sourcing is powerful but unforgiving. Get the aggregate boundaries wrong and you&rsquo;ll spend months cleaning up. Get them right and you have auditability, replay, and decoupled integrations almost for free.</p>
<p>I&rsquo;ve been building event-sourced systems at the fintech startup for a while now. We process financial news and market data from hundreds of sources, score relevance in real time, and deliver personalized feeds to users. That pipeline is a natural fit for event sourcing. Every price tick, every news article ingestion, every user interaction &ndash; they&rsquo;re all events that happened at a specific moment, and we need to know exactly what happened and when.</p>
<p>But &ldquo;natural fit&rdquo; doesn&rsquo;t mean &ldquo;easy.&rdquo; I&rsquo;ve made most of the mistakes on this list. This post is what I wish someone had handed me before I started.</p>
<h2 id="the-shift-that-changes-everything">The Shift That Changes Everything</h2>
<p>In a traditional system you store current state. A row in a table says &ldquo;Order #789 has status SHIPPED.&rdquo; You overwrite the old status. History is gone unless you bolted on an audit log.</p>
<p>Event sourcing flips that. You store the facts: OrderPlaced, PaymentReceived, OrderShipped. Current state is derived by replaying those facts. The event log is the source of truth. Everything else &ndash; your read models, your dashboards, your search indexes &ndash; is a projection you can tear down and rebuild from scratch.</p>
<p>At the fintech startup, this wasn&rsquo;t a philosophical choice. Regulators wanted to know the exact sequence of data that produced a specific score for a specific user at a specific time. With event sourcing we could answer that question by replaying the stream. With CRUD we would have been guessing.</p>
<h2 id="the-anatomy-of-an-event">The Anatomy of an Event</h2>
<p>Events are immutable records of things that already happened. Past tense. Specific. Self-contained.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;event_type&#34;</span>: <span style="color:#e6db74">&#34;ArticleScored&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;event_id&#34;</span>: <span style="color:#e6db74">&#34;evt_cf_98231&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;timestamp&#34;</span>: <span style="color:#e6db74">&#34;2018-03-19T10:15:30Z&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;aggregate_id&#34;</span>: <span style="color:#e6db74">&#34;article_44712&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;data&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;source&#34;</span>: <span style="color:#e6db74">&#34;reuters&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;relevance_score&#34;</span>: <span style="color:#ae81ff">0.87</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;matched_topics&#34;</span>: [<span style="color:#e6db74">&#34;AAPL&#34;</span>, <span style="color:#e6db74">&#34;earnings&#34;</span>],
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;scoring_model_version&#34;</span>: <span style="color:#e6db74">&#34;v3.2&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>A few things I learned the hard way about event design:</p>
<p><strong>Include the model version.</strong> We score articles with ML models that change. Without the model version baked into the event, you can&rsquo;t tell whether a score difference came from new data or a new model. We didn&rsquo;t do this initially. Debugging was miserable.</p>
<p><strong>Don&rsquo;t store derived data as the event.</strong> The event is the fact. &ldquo;ArticleScored&rdquo; with a score of 0.87 is a fact. &ldquo;ArticleIsHighlyRelevant&rdquo; is a conclusion you draw from the fact. Store the fact. Let projections draw conclusions.</p>
<p><strong>Carry enough context.</strong> A projection should never need to reach back into another stream to understand what an event means. If your ArticleScored event requires a lookup to know which source it came from, your event is too thin.</p>
<h2 id="aggregates-the-hardest-part">Aggregates: The Hardest Part</h2>
<p>Aggregates define your consistency boundary. They validate commands, enforce invariants, and emit events. Their internal state is rebuilt by replaying the event stream.</p>
<p>Getting aggregate boundaries right is the single most consequential design decision in an event-sourced system. Too big and you get contention, slow replays, and serialization bottlenecks. Too small and you can&rsquo;t enforce invariants that span related data.</p>
<p>We started with a giant &ldquo;UserFeed&rdquo; aggregate that tracked everything &ndash; subscribed topics, read articles, relevance preferences, notification settings. It grew to thousands of events per active user within weeks. Replaying it on every command was brutal.</p>
<p>We broke it apart. Subscriptions became their own aggregate. Notification preferences became their own aggregate. The UserFeed aggregate shrank to just the core feed interaction logic. Replay times dropped from seconds to milliseconds. Contention disappeared.</p>
<p>The rule I follow now: an aggregate should be the smallest unit that can enforce its invariants independently.</p>
<h2 id="cqrs-separating-the-write-and-read-paths">CQRS: Separating the Write and Read Paths</h2>
<p>Event sourcing doesn&rsquo;t require CQRS but you&rsquo;ll almost certainly end up there. The write model and read model have fundamentally different jobs.</p>
<pre tabindex="0"><code>Command -&gt; Aggregate -&gt; Event Store -&gt; Projection -&gt; Read Model
</code></pre><p>The write side validates and enforces rules. The read side is shaped for queries. At the fintech startup our write side emits events like ArticleScored and TopicSubscribed. Our read projections build denormalized views for the API: a user&rsquo;s personalized feed, trending topics, source reliability dashboards.</p>
<p>The catch is eventual consistency. Your read model will lag behind writes. For us that means a user might subscribe to a topic and not see it reflected in their feed for a few hundred milliseconds. We made that explicit in the product. No pretending the system is synchronous when it&rsquo;s not.</p>
<h2 id="projections-and-snapshots">Projections and Snapshots</h2>
<h3 id="projections">Projections</h3>
<p>Projections consume events and write to read models. The cardinal rule: they must be idempotent.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">handle</span>(event):
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> already_processed(event<span style="color:#f92672">.</span>event_id):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span>
</span></span><span style="display:flex;"><span>    update_read_model(event)
</span></span><span style="display:flex;"><span>    mark_processed(event<span style="color:#f92672">.</span>event_id)
</span></span></code></pre></div><p>We&rsquo;ve been bitten by non-idempotent projections exactly once. A network blip caused a batch of ArticleScored events to be delivered twice. Our read model double-counted relevance scores. Users saw garbage rankings. Took us three hours to figure out what happened and another two to rebuild the projection. Idempotency checks would have made it a non-event.</p>
<h3 id="snapshots">Snapshots</h3>
<p>When an aggregate has a long event history, replaying from event zero on every command gets expensive. Snapshots are a cache &ndash; a serialized version of aggregate state at a known position. You replay from the snapshot forward instead of from the beginning.</p>
<p>Key point: snapshots aren&rsquo;t the source of truth. They&rsquo;re disposable. If a snapshot is corrupt or stale, delete it and rebuild from events. We snapshot our most active aggregates every 500 events and it keeps command processing fast.</p>
<h3 id="process-managers">Process Managers</h3>
<p>Some workflows span multiple aggregates. A user signs up, we create their profile, set default subscriptions, and kick off an initial scoring run. No single aggregate owns all of that. A process manager listens to events from each aggregate, issues commands to others, and tracks progress. Think of it as a long-running coordinator that reacts to facts rather than orchestrating a transaction.</p>
<h2 id="schema-evolution-plan-before-you-ship">Schema Evolution: Plan Before You Ship</h2>
<p>Events are forever. You can&rsquo;t go back and change them. So you need a schema evolution strategy before you write your first event.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">version</span>: <span style="color:#ae81ff">2</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">payload</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">source</span>: <span style="color:#ae81ff">reuters</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">relevance_score</span>: <span style="color:#ae81ff">0.87</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">matched_topics</span>: [<span style="color:#e6db74">&#34;AAPL&#34;</span>, <span style="color:#e6db74">&#34;earnings&#34;</span>]
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">scoring_model_version</span>: <span style="color:#e6db74">&#34;v3.2&#34;</span>
</span></span></code></pre></div><p>What works for us:</p>
<ul>
<li><strong>Additive changes with defaults.</strong> New field? Add it. Old events that lack the field get a sensible default when read.</li>
<li><strong>New event types for semantic shifts.</strong> If the meaning of an event changes fundamentally, introduce a new event type. Don&rsquo;t twist the old one.</li>
<li><strong>Upcasters on read.</strong> When loading events, transform old versions into the current shape. The event store stays untouched. The application code only deals with the latest schema.</li>
</ul>
<p>We&rsquo;ve gone through four versions of our scoring events. The upcaster chain is a bit ugly but it works and we&rsquo;ve never had to touch the event store itself.</p>
<h2 id="the-pitfalls-that-actually-hurt">The Pitfalls That Actually Hurt</h2>
<p>Most event sourcing failures come from modeling mistakes, not tooling problems.</p>
<p><strong>Modeling commands as events.</strong> &ldquo;ScoreArticle&rdquo; is a command. &ldquo;ArticleScored&rdquo; is an event. If you store commands in your event log you will confuse every projection that reads them. I&rsquo;ve seen this mistake more times than I want to admit, including in our own early prototypes.</p>
<p><strong>Giant aggregates.</strong> Already covered this. Keep them small. If replay takes more than a few milliseconds, something is wrong.</p>
<p><strong>Synchronous projections on the write path.</strong> The moment you make a write wait for a projection to finish, you&rsquo;ve coupled your write throughput to your read model&rsquo;s performance. Don&rsquo;t do this.</p>
<p><strong>Skipping idempotency.</strong> Events will be delivered more than once. Network partitions, retries, rebalances. Your projections must handle duplicates gracefully or your read models will drift into nonsense.</p>
<p><strong>Chatty technical events.</strong> Events should represent business facts. &ldquo;DatabaseRowUpdated&rdquo; isn&rsquo;t a business fact. &ldquo;ArticleScored&rdquo; is. If your events read like a database changelog, you&rsquo;ve modeled the wrong thing.</p>
<h2 id="when-not-to-bother">When Not to Bother</h2>
<p>Event sourcing is a deliberate tradeoff, not a default architecture.</p>
<p>Skip it when CRUD is genuinely sufficient and audit history isn&rsquo;t a real requirement. Skip it when the domain is simple and the modeling overhead isn&rsquo;t justified. Skip it when your team isn&rsquo;t ready to think in terms of eventual consistency.</p>
<p>At the fintech startup it was the right call because of the regulatory requirements, the temporal query needs, and the natural event-driven shape of financial data. For our marketing site? We use a database and a CMS. Not everything needs to be an event.</p>
<h2 id="the-short-version">The Short Version</h2>
<p>Model events as business facts in past tense. Keep aggregates small and focused. Build read models asynchronously and accept eventual consistency. Make projections idempotent. Plan schema evolution before you ship. And decide early whether the complexity is worth it for your specific domain &ndash; because once you commit to event sourcing, unwinding it is far harder than adopting it.</p>
]]></content:encoded></item><item><title>Multi-Region Architecture: What I Wish Someone Had Told Me</title><link>https://lawzava.com/blog/2017-10-02-building-multi-region-applications/</link><pubDate>Mon, 02 Oct 2017 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2017-10-02-building-multi-region-applications/</guid><description>What I learned evaluating multi-region at the fintech startup: the patterns that work, the ones that burn you, and when you should even bother.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Multi-region buys you latency and resilience. It costs you sanity. Know which one you&rsquo;re trading before you start.</p>
<hr>
<p>At the fintech startup we serve real-time financial news and data to users spread across the UK, Germany, the Nordics, and increasingly beyond. Our backend runs in a single AWS region right now. It works. But I&rsquo;ve spent the last few months seriously evaluating what a multi-region setup would look like for us, and I want to lay out what I&rsquo;ve found &ndash; both the architecture patterns and the ugly operational reality nobody warns you about.</p>
<h3 id="physics-doesnt-care-about-your-sla">Physics doesn&rsquo;t care about your SLA</h3>
<p>A request from London to <code>eu-west-1</code> is fast. A request from Singapore to <code>eu-west-1</code> isn&rsquo;t. We&rsquo;re talking 200-300ms of raw network latency before your application code even runs. Stack a few API calls on top of that, add a database query, and suddenly the page takes two seconds to load. Users don&rsquo;t file bug reports for slow pages. They just leave.</p>
<p>I measured this myself with a cheap VPS in each continent. London to Dublin: ~10ms. Tokyo to Dublin: ~240ms. That&rsquo;s not a rounding error. That&rsquo;s a completely different product experience.</p>
<h3 id="three-patterns-three-sets-of-problems">Three patterns, three sets of problems</h3>
<p><strong>Active-passive</strong> is the one everyone starts with. Your primary region handles all traffic. A standby region sits there with replicated data, waiting. You get disaster recovery. You don&rsquo;t get latency improvement for remote users. The tricky part is failover &ndash; if you haven&rsquo;t rehearsed it, your &ldquo;30-second failover&rdquo; is actually a 45-minute scramble at 3am with someone SSHing into the wrong box.</p>
<p><strong>Active-active</strong> is the dream. Multiple regions serving traffic simultaneously, users routed to the nearest one. Fast everywhere. But you&rsquo;ve just signed up for distributed consensus problems. Two users updating the same record in two regions at the same time? Now you need conflict resolution. Last-write-wins is easy to implement and will silently eat data. Domain-specific merge logic is correct and will take you months to build. Pick your pain.</p>
<p><strong>Follow-the-sun</strong> (or read-local, write-central) is what I keep coming back to for our use case. Reads hit the nearest region. Writes go to a single primary. For a read-heavy system like ours &ndash; users consuming financial content far more than creating it &ndash; this is the sweet spot. Remote users still eat write latency, but writes are a small fraction of our traffic. The tradeoff is acceptable.</p>
<h3 id="data-is-where-it-gets-ugly">Data is where it gets ugly</h3>
<p>Every multi-region discussion eventually becomes a data discussion. The CAP theorem isn&rsquo;t just academic. It&rsquo;s the thing that makes your on-call engineer cry.</p>
<p>Async replication is the pragmatic choice. Your writes are fast because they don&rsquo;t wait for the replica. But a user who just updated their watchlist and immediately reads it back might see stale data. For financial content, that&rsquo;s bad. For user preferences, it&rsquo;s annoying but survivable. You have to know your data well enough to make that call per table, sometimes per column.</p>
<p>Sync replication gives you consistency. It also means every write waits for a cross-region round trip. One slow region drags the whole system down. I&rsquo;ve seen this kill write throughput in practice. Unless your write volume is very low and your tolerance for tail latency is very high, avoid it.</p>
<p>The approach I like best is <strong>regional data affinity</strong>. A UK user&rsquo;s data lives in the EU region. Period. We only replicate the things that truly need to be everywhere &ndash; shared reference data, global config, that sort of thing. This sidesteps most consistency headaches because you&rsquo;re not actually doing multi-master for user data. You&rsquo;re doing single-master per user, distributed across regions. Much simpler failure modes.</p>
<h3 id="getting-users-to-the-right-place">Getting users to the right place</h3>
<p>DNS geo-routing is the obvious first step. Route53 does it, CloudFlare does it, everyone does it. It works well enough. The catch is DNS caching &ndash; TTLs are suggestions, not commands. ISPs will cache your records for longer than you want, which means failover via DNS is slower than you&rsquo;d hope. Minutes, not seconds.</p>
<p>Anycast is faster for failover but harder to operate. You need BGP-level control. Most startups, us included, don&rsquo;t have the network engineering chops for this. It&rsquo;s the right answer at scale. It&rsquo;s premature complexity at our stage.</p>
<p>CDNs solve the easy part (static assets, edge caching) and punt on the hard part (dynamic requests still need to reach an origin). Worth doing regardless of your multi-region strategy, but don&rsquo;t confuse a CDN with actual multi-region architecture. They&rsquo;re complementary, not interchangeable.</p>
<h3 id="the-operational-tax-nobody-budgets-for">The operational tax nobody budgets for</h3>
<p>Here&rsquo;s where most multi-region proposals die, and honestly they should.</p>
<p><strong>Deployments</strong> get harder. You can&rsquo;t just <code>kubectl apply</code> and walk away. You need to decide: do you deploy to all regions simultaneously? Serially? Use one region as a canary? We&rsquo;d probably do serial deployment with the secondary region first, watch it for 15 minutes, then roll to primary. That means every deploy takes longer. Every rollback is more complex. Your CI/CD pipeline just doubled in scope.</p>
<p><strong>Monitoring</strong> has to be per-region AND global. You need to know that latency is high in <code>ap-southeast-1</code> specifically, not just that &ldquo;p99 latency is elevated somewhere.&rdquo; If your monitoring runs in the same region as your primary, congratulations &ndash; when that region dies, so does your ability to see that it died.</p>
<p><strong>Failover</strong> isn&rsquo;t a feature you ship once. It&rsquo;s a muscle you exercise. We do game days at the fintech startup for our current single-region setup, and even those surface surprises. A multi-region failover you&rsquo;ve never tested is a multi-region failover that doesn&rsquo;t work. Full stop. The first time you practice it, something will break that you didn&rsquo;t expect. Better to find that on a Tuesday afternoon than during an actual incident.</p>
<h3 id="should-you-actually-do-this">Should you actually do this?</h3>
<p>Honest answer: most teams shouldn&rsquo;t. Not yet.</p>
<p>If your users are in one geography and your uptime requirements are met by a single region with good practices (multi-AZ, proper backups, tested restores), multi-region is complexity you&rsquo;re borrowing against future needs. That debt has interest.</p>
<p>For us at the fintech startup, the calculus is shifting. We&rsquo;re getting real users in Asia. GDPR already constrains where we store certain data. And our customers are making trading decisions based on our content &ndash; downtime costs them money, not just patience. We&rsquo;ll probably go follow-the-sun within the next year. But I&rsquo;m going in with my eyes open about what it costs.</p>
<p>Build your system so migration is possible. Abstract your data layer. Don&rsquo;t hardcode region assumptions. Use infrastructure-as-code so spinning up a new region isn&rsquo;t a six-week project. Then wait until the numbers actually justify the move.</p>
<p>The worst multi-region architectures I&rsquo;ve seen were built by teams who wanted the résumé bullet point. The best were built by teams who had no choice.</p>
]]></content:encoded></item><item><title>Monitoring Is Not Enough</title><link>https://lawzava.com/blog/2017-03-20-why-observability-matters-more-than-monitoring/</link><pubDate>Mon, 20 Mar 2017 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2017-03-20-why-observability-matters-more-than-monitoring/</guid><description>Your dashboards look green. Your users say the site is broken. That gap is the whole problem.</description><content:encoded><![CDATA[<p>Most teams think they have monitoring figured out. Dashboards, thresholds, PagerDuty. Something spikes, someone gets paged, someone fixes it. Works great when you have a monolith and three endpoints.</p>
<p>It falls apart the second you split into services.</p>
<p>I learned this the hard way at the fintech startup. We had decent Grafana dashboards. Reasonable alerts. Then we started breaking things into microservices and deploying multiple times a day. A request would fail somewhere in a chain of five services and our dashboards would just&hellip; look fine. Every individual service reported healthy metrics. The problem lived in the gaps between them.</p>
<p>That&rsquo;s the core issue with monitoring. It answers questions you already thought to ask. Latency on this endpoint? Sure. Error rate on that queue? Got it. But the failure you actually hit in production is the one you never predicted. Your dashboards have no panel for it.</p>
<h3 id="observability-is-a-different-mindset">Observability is a different mindset</h3>
<p>Observability means you instrument your system so you can ask <em>new</em> questions after something breaks. Not just &ldquo;is it up&rdquo; but &ldquo;why did this specific user&rsquo;s request take 8 seconds at 3am on Tuesday.&rdquo;</p>
<p>Three signals make this work: metrics for trends and alerts, logs for detail, and traces for stitching a single request across every service it touches. Separately they are useful. Together they are a debugging superpower.</p>
<p>Structured logging is the foundation. Stop writing free-text log lines. Make every log entry a JSON object with a trace ID, service name, version, and whatever fields you actually need to filter on.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;timestamp&#34;</span>: <span style="color:#e6db74">&#34;2017-03-20T10:23:45Z&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;level&#34;</span>: <span style="color:#e6db74">&#34;error&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;message&#34;</span>: <span style="color:#e6db74">&#34;Payment processing failed&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;trace_id&#34;</span>: <span style="color:#e6db74">&#34;abc123&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;service&#34;</span>: <span style="color:#e6db74">&#34;payment-service&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;version&#34;</span>: <span style="color:#e6db74">&#34;2.1.3&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Then propagate context. Generate a trace ID at the edge and carry it through every downstream call. This is the single most important thing you can do. Without it you&rsquo;re just grepping logs and praying.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-text" data-lang="text"><span style="display:flex;"><span>X-Trace-Id: abc123
</span></span><span style="display:flex;"><span>X-Span-Id: def456
</span></span><span style="display:flex;"><span>X-Parent-Span-Id: ghi789
</span></span></code></pre></div><h3 id="how-incidents-actually-change">How incidents actually change</h3>
<p>Alert fires. Pull the trace. Follow the slow span to the source. Read the logs for that trace. Confirm with metrics whether it&rsquo;s one user or everyone. Done. Five minutes instead of forty-five minutes of bouncing between dashboards while your Slack channel fills with &ldquo;any update?&rdquo;</p>
<p>Same thing with performance. User says it&rsquo;s slow. Pull their trace. Compare to a healthy one. See the difference &ndash; a cache miss, an extra database round-trip, a third-party call timing out. You fix the actual cause instead of guessing.</p>
<h3 id="the-tools-dont-matter-that-much">The tools don&rsquo;t matter that much</h3>
<p>Prometheus, InfluxDB, ELK, Jaeger, Zipkin &ndash; pick whatever fits your stack. Commercial platforms that bundle all three signals save time. But the tooling isn&rsquo;t the hard part. The hard part is disciplined instrumentation. Consistent field names. Trace IDs everywhere. Every team following the same conventions.</p>
<h3 id="what-actually-matters">What actually matters</h3>
<p>Observability isn&rsquo;t a product you buy. It&rsquo;s a practice you build. You stop staring at dashboards waiting for red. You start asking questions about behavior you didn&rsquo;t expect. That shift &ndash; from reactive to exploratory &ndash; is the entire point. And in a world where every team is shipping services independently, it&rsquo;s the only way to stay sane.</p>
]]></content:encoded></item></channel></rss>