<?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>Engineering | Law Zava</title><link>https://lawzava.com/topics/engineering/</link><description>Craft-level practice: code review, documentation, debugging, design, and the standards that hold under pressure.</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/engineering/index.xml" rel="self" type="application/rss+xml"/><item><title>AI-Native Architecture Patterns 2026: Production Guide</title><link>https://lawzava.com/blog/2026-01-26-ai-native-architecture-2026/</link><pubDate>Mon, 26 Jan 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-01-26-ai-native-architecture-2026/</guid><description>Production AI architecture patterns for gateways, retrieval, evaluation, fallbacks, cost control, and ownership.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>AI-native architecture is mostly about boring interfaces: route model calls through a gateway, ground outputs with retrieval, validate and log everything, and make evaluation part of the release process. The goal isn&rsquo;t to worship a model. The goal is to ship AI features that survive change: model updates, data drift, new policy requirements, and real production load.</p>
<p>AI-native architecture is no longer a sidecar to the main system. By late January 2026, teams treat it as a first-class capability with concrete design and operational practices. The emphasis has shifted from demos to reliability, cost control, and change management.</p>
<h2 id="what-changed">What Changed</h2>
<p>The biggest shift is structural. AI capabilities are now designed into service boundaries, deployment flows, and runtime controls instead of layered on top. That changes how teams think about interfaces, failure modes, and ownership.</p>
<p> <a href="/blog/2024-02-05-ai-native-architecture/"
   
   >Two years ago</a>
, most teams ran AI as a separate service that the rest of the stack called when it needed something smart. The model sat behind an API, and the integration was a thin adapter. That worked for demos and low-stakes features, but it broke down as AI became central to the product. Latency budgets, error handling, and data flow all suffered from the indirection. The shift to native architecture means AI concerns are represented in the same design conversations as database schemas, API contracts, and deployment topologies.</p>
<h2 id="core-patterns-that-hold-up">Core Patterns That Hold Up</h2>
<h3 id="ai-gateway">AI Gateway</h3>
<p>A dedicated gateway organizes AI access and policy. It centralizes routing, safety controls, and observability so teams don&rsquo;t reimplement the same logic across services. It also provides a stable interface as models and capabilities evolve.</p>
<p>In practice, the gateway sits between your application services and model providers. Requests flow in from your services, the gateway applies rate limiting and authentication, selects the appropriate model based on task type and cost constraints, and forwards the request. Responses flow back through the same path, where the gateway logs latency, token usage, and any safety filter activations before returning the result. This single chokepoint means you can swap providers, add fallback models, or enforce new policies without touching application code.</p>
<p>The tradeoff is operational overhead. A gateway is another service to run, monitor, and scale. Teams that skip it usually rebuild the same logic piecemeal across every service that calls a model, which is worse. But you need to staff it. Someone owns the gateway, and that ownership must be explicit from the start.</p>
<h3 id="retrieval-layer">Retrieval Layer</h3>
<p>A retrieval layer handles knowledge access, context assembly, and freshness. It&rsquo;s treated as an application concern rather than a data science add-on. The goal is to make AI behavior grounded, auditable, and resilient to stale inputs.</p>
<p>The retrieval layer receives a query from the orchestration logic, searches across one or more knowledge stores ( <a href="/blog/2023-04-03-vector-databases-explained/"
   
   >vector databases</a>
, document indices, structured data APIs), ranks and filters the results, assembles them into a context window with appropriate formatting, and passes the assembled context to the model along with the original request. The output is grounded in specific sources, which makes it auditable.</p>
<p>Freshness is the hardest part. Stale context produces confident wrong answers, which are worse than no answer. Teams that do this well treat the retrieval layer like a cache: they track staleness explicitly, set TTLs on indexed content, and build refresh pipelines that run on a schedule or when upstream data changes. The retrieval layer isn&rsquo;t a static index. It&rsquo;s a living system with its own operational requirements.</p>
<h3 id="evaluation-pipeline">Evaluation Pipeline</h3>
<p>An evaluation pipeline is part of the architecture, not a later stage. Automated checks and human review are integrated into delivery so quality doesn&rsquo;t depend on a single model choice or a one-off test run.</p>
<p>The pipeline runs at multiple stages. Before deployment, it executes a suite of test cases against the candidate model or prompt configuration and compares results to established baselines. During deployment, it runs a smaller set of smoke tests against live traffic. After deployment, it continuously samples production responses and scores them against quality criteria.</p>
<p>What gets caught depends on the depth of the suite. At a minimum, evaluation catches regressions in factual accuracy when you update a model version, formatting breakdowns when prompt templates change, and safety filter gaps when new input patterns emerge. More mature pipelines also catch subtle drift: the model still produces valid output, but the tone has shifted, or it has started favoring certain response patterns over others. These slow changes are invisible without measurement and are often the ones that erode user trust.</p>
<h2 id="migrating-from-bolt-on-to-native">Migrating From Bolt-On to Native</h2>
<p>Most teams don&rsquo;t start with native architecture. They start with a model API call inside an existing service and grow from there. The migration path is predictable.</p>
<p>The first step is to extract AI concerns into a shared layer. If three services each call a model API with their own retry logic, prompt templates, and error handling, consolidate that into a gateway or shared library. This is a mechanical refactor, not a redesign.</p>
<p>The second step is to make the data flow explicit. Bolt-on integrations often pass raw user input directly to the model. Native architecture introduces a context assembly step where retrieval, formatting, and policy checks happen before the model sees anything. This is where you gain control over what the model knows and how it behaves.</p>
<p>The third step is to add  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >evaluation</a>
 as a first-class concern. This means defining what good output looks like for each use case, writing test cases, and wiring them into your CI pipeline. Until evaluation is automated, every model change is a gamble.</p>
<p>The migration doesn&rsquo;t need to happen all at once. Teams can move one use case at a time, starting with the highest-risk or highest-traffic path. The key is that each step produces a tangible improvement in reliability or operability, not just architectural purity. The team structure matters here because shared routing, evaluation, and governance need explicit owners.</p>
<h2 id="design-priorities">Design Priorities</h2>
<p>The systems that perform well share a few priorities. They build model-agnostic interfaces with clear contracts so that swapping a provider is a configuration change, not a rewrite. They design graceful degradation with explicit fallback paths, because models will fail and the product needs to keep working when they do. And they invest in continuous measurement of quality, safety, and cost, because you can&rsquo;t manage what you don&rsquo;t measure.</p>
<p>Add one more: <strong>ownership</strong>. A feature without an owner is a liability. Someone must be accountable for keeping quality steady as everything around the model changes.</p>
<h2 id="operating-in-production">Operating In Production</h2>
<p>Operational work matters as much as model selection. Good systems make evaluation visible, track drift, and keep changes reversible. They also avoid tight coupling to any single model or provider so capability upgrades don&rsquo;t require a redesign.</p>
<p>The day-to-day reality of operating these systems is closer to running a data pipeline than running a traditional web service. You&rsquo;re monitoring output quality, not just uptime. You&rsquo;re tracking cost per request alongside latency. And you&rsquo;re maintaining a relationship with your evaluation suite that&rsquo;s as important as your relationship with your test suite for deterministic code.</p>
<h2 id="takeaway">Takeaway</h2>
<p>AI-native architecture is now a discipline with stable patterns. The winning approach is to design for change, make evaluation part of the system, and treat AI as a core runtime capability rather than a bolt-on feature. The teams that get this right aren&rsquo;t the ones with the best models. They are the ones with the best systems around their models.</p>
<h2 id="faq">FAQ</h2>
<h3 id="what-is-ai-native-architecture">What is AI-native architecture?</h3>
<p>AI-native architecture treats model calls, retrieval, evaluation, routing, cost control, and fallback behavior as first-class production concerns instead of bolting an API call onto an existing feature.</p>
<h3 id="what-are-the-core-ai-architecture-patterns-in-2026">What are the core AI architecture patterns in 2026?</h3>
<p>The durable patterns are an AI gateway, retrieval layer, evaluation pipeline, model routing, structured output validation, observability, and graceful degradation.</p>
<h3 id="why-do-enterprise-ai-architectures-fail">Why do enterprise AI architectures fail?</h3>
<p>They usually fail because the prototype has no production boundary: no owner, no eval suite, no fallback path, no data freshness model, and no cost attribution.</p>
]]></content:encoded></item><item><title>Building Reliable AI Agents in Go</title><link>https://lawzava.com/blog/2026-01-19-ai-agent-reliability/</link><pubDate>Mon, 19 Jan 2026 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2026-01-19-ai-agent-reliability/</guid><description>Reliable agents are engineered, not prompted: bounded tools, validation at every step, explicit recovery paths. Here&amp;amp;rsquo;s how I build them in Go.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Reliable agents are built, not prompted. Limit tools and steps. Validate every action at the boundary. Persist state so retries are safe. Design explicit recovery paths. Measure outcomes with  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >evals</a>
, not vibes. If you want autonomy, earn it in increments with evidence and guardrails. This post includes the Go patterns I actually use.</p>
<hr>
<p>I&rsquo;ve been building  <a href="/blog/2023-09-18-agent-architecture-patterns/"
   
   >agent systems</a>
 in Go for the past year &ndash; across startups and enterprise teams. The same lesson keeps repeating: the model is the easy part. The hard part is everything around it. Tool validation. State management. Recovery paths. Observability. The boring infrastructure that turns &ldquo;it works in a demo&rdquo; into &ldquo;it works at 3am when nobody is watching.&rdquo;</p>
<p>Reliable agents are engineered, not prompted. Here&rsquo;s how.</p>
<h2 id="what-reliable-actually-means">What &ldquo;reliable&rdquo; actually means</h2>
<p>If you can&rsquo;t write down the success criteria, you can&rsquo;t make an agent reliable. &ldquo;Handle this ticket&rdquo; isn&rsquo;t a spec. &ldquo;Classify into one of five categories, draft a reply citing the relevant policy section, and escalate to a human if confidence is below 0.7&rdquo; is a spec.</p>
<p>A reliable agent operates within known tools, limited steps, and explicit completion checks. It produces repeatable outcomes. It fails safely. Creativity and autonomy aren&rsquo;t the goal. Predictability is.</p>
<p>Reliability is strongest where the task is structured: multi-step workflows with fixed tools, document extraction, data transformation with deterministic post-processing. It degrades as tasks become open-ended, long-running, or novel. That isn&rsquo;t a temporary limitation. It&rsquo;s a fundamental property of probabilistic systems.</p>
<h2 id="the-architecture-that-holds-up">The architecture that holds up</h2>
<p>The reliable agent systems I build don&rsquo;t look like a single prompt calling tools. They look like a small system with explicit responsibilities:</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">Agent</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">tools</span>      <span style="color:#a6e22e">ToolRegistry</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">policy</span>     <span style="color:#a6e22e">PolicyEnforcer</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">validator</span>  <span style="color:#a6e22e">ActionValidator</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">state</span>      <span style="color:#a6e22e">StateStore</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">supervisor</span> <span style="color:#a6e22e">Supervisor</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">maxSteps</span>   <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">timeout</span>    <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</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">type</span> <span style="color:#a6e22e">ToolRegistry</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">tools</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#a6e22e">Tool</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">type</span> <span style="color:#a6e22e">Tool</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Name</span>        <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Schema</span>      <span style="color:#a6e22e">jsonschema</span>.<span style="color:#a6e22e">Schema</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Execute</span>     <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">args</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">RawMessage</span>) (<span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">RawMessage</span>, <span style="color:#66d9ef">error</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">SideEffects</span> <span style="color:#66d9ef">bool</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Idempotent</span>  <span style="color:#66d9ef">bool</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Every component has a clear job. The tool registry enforces schemas. The policy layer checks permissions before execution. The validator inspects arguments and output shape. The state store persists progress so retries don&rsquo;t repeat side effects. The supervisor can stop, escalate, or hand off to a human.</p>
<p>You can implement this in a lightweight way, but the responsibilities need to exist somewhere. If they don&rsquo;t, reliability will always be &ldquo;mostly okay until it isn&rsquo;t.&rdquo;</p>
<h2 id="validation-at-the-boundary">Validation at the boundary</h2>
<p>Agents fail in boring ways. Wrong parameters. Missing required fields. Calling the right tool at the wrong time. Repeating a write action. Getting stuck in a loop.</p>
<p>The fixes are also boring:</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">v</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ActionValidator</span>) <span style="color:#a6e22e">Validate</span>(<span style="color:#a6e22e">action</span> <span style="color:#a6e22e">Action</span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">tool</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">v</span>.<span style="color:#a6e22e">registry</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">action</span>.<span style="color:#a6e22e">ToolName</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</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;unknown tool: %s&#34;</span>, <span style="color:#a6e22e">action</span>.<span style="color:#a6e22e">ToolName</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">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">tool</span>.<span style="color:#a6e22e">Schema</span>.<span style="color:#a6e22e">Validate</span>(<span style="color:#a6e22e">action</span>.<span style="color:#a6e22e">Args</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;invalid args for %s: %w&#34;</span>, <span style="color:#a6e22e">action</span>.<span style="color:#a6e22e">ToolName</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">if</span> <span style="color:#a6e22e">tool</span>.<span style="color:#a6e22e">SideEffects</span> <span style="color:#f92672">&amp;&amp;</span> !<span style="color:#a6e22e">v</span>.<span style="color:#a6e22e">policy</span>.<span style="color:#a6e22e">Allowed</span>(<span style="color:#a6e22e">action</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;action %s denied by policy&#34;</span>, <span style="color:#a6e22e">action</span>.<span style="color:#a6e22e">ToolName</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>Validate arguments at the boundary. Return structured errors. If a tool has side effects, check policy before execution. If a tool isn&rsquo;t idempotent, check whether this exact action has already been executed in the current run.</p>
<p>This isn&rsquo;t clever. It&rsquo;s the same approach I use for any public API. Treat tools like APIs, enforce contracts, and the model has fewer ways to surprise you.</p>
<h2 id="idempotency-and-state">Idempotency and state</h2>
<p>The nastiest agent bugs come from retries that repeat side effects. Duplicate tickets. Repeated refunds. Double-sends. The fix is the same as in any  <a href="/blog/2018-09-17-building-reliable-distributed-systems/"
   
   >distributed system</a>
: make write operations 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-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">StateStore</span>) <span style="color:#a6e22e">ExecuteOnce</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">stepID</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">fn</span> <span style="color:#66d9ef">func</span>() (<span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">RawMessage</span>, <span style="color:#66d9ef">error</span>)) (<span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">RawMessage</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">result</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">Get</span>(<span style="color:#a6e22e">stepID</span>); <span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">result</span>, <span style="color:#66d9ef">nil</span> <span style="color:#75715e">// already executed, return cached result</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">result</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fn</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">s</span>.<span style="color:#a6e22e">Set</span>(<span style="color:#a6e22e">stepID</span>, <span style="color:#a6e22e">result</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">result</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Every meaningful step gets a unique ID. Before executing, check if the step has already completed. If it has, return the cached result. This makes retries safe and recovery straightforward.</p>
<p>I learned this pattern while building cloud infrastructure at a previous startup, not AI systems. Same principles. Different surface area.</p>
<h2 id="the-supervisor-loop">The supervisor loop</h2>
<p>The supervisor is the most important piece. It enforces hard limits and decides what happens when things go wrong:</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">a</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Agent</span>) <span style="color:#a6e22e">Run</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">task</span> <span style="color:#a6e22e">Task</span>) (<span style="color:#a6e22e">Result</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:#a6e22e">a</span>.<span style="color:#a6e22e">timeout</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:#66d9ef">for</span> <span style="color:#a6e22e">step</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">step</span> &lt; <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">maxSteps</span>; <span style="color:#a6e22e">step</span><span style="color:#f92672">++</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">action</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">planNextAction</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">task</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">Result</span>{}, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;planning failed at step %d: %w&#34;</span>, <span style="color:#a6e22e">step</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">if</span> <span style="color:#a6e22e">action</span>.<span style="color:#a6e22e">Type</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">ActionComplete</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">finalize</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">action</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">action</span>.<span style="color:#a6e22e">Type</span> <span style="color:#f92672">==</span> <span style="color:#a6e22e">ActionEscalate</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">escalateToHuman</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">task</span>, <span style="color:#a6e22e">action</span>.<span style="color:#a6e22e">Reason</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">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">validator</span>.<span style="color:#a6e22e">Validate</span>(<span style="color:#a6e22e">action</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:#a6e22e">a</span>.<span style="color:#a6e22e">logValidationFailure</span>(<span style="color:#a6e22e">step</span>, <span style="color:#a6e22e">action</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">continue</span> <span style="color:#75715e">// let the model try again with the error context</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">result</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">state</span>.<span style="color:#a6e22e">ExecuteOnce</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">action</span>.<span style="color:#a6e22e">StepID</span>, <span style="color:#66d9ef">func</span>() (<span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">RawMessage</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">tools</span>.<span style="color:#a6e22e">Execute</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">action</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:#a6e22e">a</span>.<span style="color:#a6e22e">supervisor</span>.<span style="color:#a6e22e">OnFailure</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">step</span>, <span style="color:#a6e22e">action</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></span><span style="display:flex;"><span>        <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">appendResult</span>(<span style="color:#a6e22e">step</span>, <span style="color:#a6e22e">action</span>, <span style="color:#a6e22e">result</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">Result</span>{}, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;agent exceeded max steps (%d)&#34;</span>, <span style="color:#a6e22e">a</span>.<span style="color:#a6e22e">maxSteps</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Hard maximum on steps. Hard timeout. Explicit escalation path. Validation before every tool call. Idempotent execution. Structured logging at every decision point.</p>
<p>This isn&rsquo;t a framework. It&rsquo;s a pattern. Adapt it to your domain. The important thing is that these responsibilities exist in your system, however you implement them.</p>
<h2 id="observability">Observability</h2>
<p>If you can&rsquo;t see what the agent did, you can&rsquo;t improve it. Log enough to answer practical questions:</p>
<ul>
<li>Tool name, step number, latency</li>
<li>Success/failure codes and validation errors</li>
<li>Argument hashes (not raw values for sensitive data)</li>
<li>Completion status and reason for stopping</li>
<li>Human handoff events</li>
</ul>
<p>This data turns &ldquo;the agent is flaky&rdquo; into &ldquo;the search tool fails 8% of the time when the query exceeds 200 characters.&rdquo; The second statement is fixable. &ldquo;Flaky&rdquo; isn&rsquo;t.</p>
<h2 id="where-this-falls-apart">Where this falls apart</h2>
<p>Open-ended creative work. Long-running autonomous loops with shifting context. Novel situations without prior examples. High-stakes decisions without human review.</p>
<p>These aren&rsquo;t temporary limitations waiting for a better model. They are fundamental properties of probabilistic systems operating in complex environments. If your agent needs to handle these cases, the answer isn&rsquo;t a better prompt. The answer is a human checkpoint.</p>
<h2 id="the-uncomfortable-truth">The uncomfortable truth</h2>
<p>Most agent reliability problems aren&rsquo;t model problems. They are engineering problems. Wrong tool schemas. Missing validation. No idempotency. No timeouts. No escalation path. The model does something unexpected, and instead of being caught at the boundary, it cascades into a production issue.</p>
<p>Fix the engineering first. The model reliability improves as a consequence.</p>
<p>If you want autonomy, earn it in increments. With evidence. With guardrails. Not with optimistic prompts and hope.</p>
]]></content:encoded></item><item><title>AI Technical Debt Is Eating Your Team Alive (And You Can't Even See It)</title><link>https://lawzava.com/blog/2025-10-27-ai-technical-debt/</link><pubDate>Mon, 27 Oct 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-10-27-ai-technical-debt/</guid><description>AI debt hides in prompts nobody owns, evals nobody runs, and data pipelines nobody watches. By the time you notice, every change feels dangerous.</description><content:encoded><![CDATA[<p>I wrote about  <a href="/blog/2016-02-22-the-true-cost-of-technical-debt/"
   
   >the true cost of technical debt</a>
 back in 2016. The core argument was simple: if you can&rsquo;t put a number on your debt, you can&rsquo;t make a rational decision about it. Measure the pain, do the math, and present the tradeoff.</p>
<p>That advice still holds. But AI debt is a different animal, and it&rsquo;s making me angry.</p>
<p>With traditional tech debt, at least you can see it. Messy code. Missing tests. A module everyone dreads touching. The debt is in the codebase. You can grep for it. You can point to it in a PR review.</p>
<p>AI debt hides. It hides in prompts copy-pasted from a demo and never documented. In evaluations that were &ldquo;planned for next sprint&rdquo; six months ago. In embeddings that went stale when source docs changed and nobody re-indexed. In  <a href="/blog/2024-09-30-retrieval-strategies-rag/"
   
   >retrieval pipelines</a>
 where data drifted so gradually that answers went from &ldquo;good&rdquo; to &ldquo;plausible&rdquo; to &ldquo;confidently wrong,&rdquo; and nobody noticed until a customer complained. The architectural version of this is why AI-native architecture needs explicit evaluation and retrieval ownership.</p>
<p>The system is still up. It still returns 200 OK. And it&rsquo;s slowly poisoning your product.</p>
<h2 id="the-four-kinds-of-ai-debt-that-keep-showing-up">The four kinds of AI debt that keep showing up</h2>
<p><strong>Prompt debt.</strong> Someone wrote a prompt that worked. They shipped it. Three model versions later, it still &ldquo;works,&rdquo; but the behavior has shifted in ways nobody documented because nobody was measuring. The prompt has magic strings nobody can explain. Changing a single sentence now requires a full regression test nobody has time for, so nobody changes anything, and the prompt becomes legacy code that happens to be written in English.</p>
<p><strong>Eval debt.</strong> This one drives me up the wall. Teams ship AI features with no  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >evaluation suite</a>
. Then they argue about quality using anecdotes. &ldquo;It seemed fine when I tried it.&rdquo; That&rsquo;s not engineering; that&rsquo;s vibes. Without evals, you can&rsquo;t tell if your last change made things better or worse. You&rsquo;re flying blind and calling it agile.</p>
<p><strong>Data and pipeline debt.</strong> Stale embeddings. Missing documents. Labeling standards that drifted. The retrieval layer quietly degrades, and because LLMs are so good at sounding confident, nobody notices that answers are getting worse. This is the most insidious form because it&rsquo;s silent. The system doesn&rsquo;t crash. It just gets less trustworthy.</p>
<p><strong>Architecture debt.</strong> The model interface is hard-coded three layers deep. Tool calls are embedded in application logic. Swapping a provider or upgrading a model feels like open-heart surgery. So teams avoid improvements entirely. The system calcifies.</p>
<h2 id="how-to-actually-fix-this">How to actually fix this</h2>
<p>The same way you fix  <a href="/blog/2021-09-20-technical-debt-management/"
   
   >any tech debt</a>
. Not with a heroic rewrite. With discipline.</p>
<p><strong>Version your prompts like code.</strong> Put them in the repo. Give them owners. Document the intent, not just the text. When someone changes a prompt, they should write down why, and what eval signals should remain stable. This isn&rsquo;t bureaucracy. It&rsquo;s how you stop mystery regressions.</p>
<p><strong>Build evals before you ship.</strong> Start with a small set of real examples and documented expected outcomes. Run them on every meaningful change. It doesn&rsquo;t need to be elaborate. It needs to be consistent. Teams that do this &ndash; even just 20-30 test cases &ndash; move faster because they know what is safe to change.</p>
<p><strong>Decouple the model interface.</strong> Abstract it. Separate retrieval from response logic. That lets you  <a href="/blog/2024-03-18-multi-model-strategies/"
   
   >swap providers</a>
, test with mocks, and upgrade models without touching core flows. It also makes your system testable, which is the whole point.</p>
<p><strong>Monitor freshness alongside quality.</strong> Track when your embeddings were last updated. Track retrieval relevance scores. If your data pipeline is stale, your outputs are stale, no matter how good the model is.</p>
<h2 id="the-uncomfortable-part">The uncomfortable part</h2>
<p>Most teams accumulate AI debt because they shipped under pressure and told themselves they&rsquo;d clean it up later. I&rsquo;ve been guilty of this. Early on at a startup I ran, we had prompts that worked &ldquo;well enough&rdquo; and no eval suite for weeks. The reckoning came when we swapped model versions and spent three days figuring out what broke because we had no baseline to compare against.</p>
<p>The fix isn&rsquo;t a cleanup sprint. It&rsquo;s a steady cadence. Fifteen percent of capacity toward debt work, same as I recommended in 2016. Review prompt changes with rationale. Run evals on every release.  <a href="/blog/2025-03-31-ai-observability-deep/"
   
   >Monitor quality signals</a>
 and data freshness together.</p>
<p>AI debt is manageable. But it requires intention. If every small change to your AI system feels risky, you already have a debt problem. The path forward isn&rsquo;t heroic rewrites. It&rsquo;s a steady sequence of small, documented improvements.</p>
<p>Steady beats dramatic. Every time.</p>
]]></content:encoded></item><item><title>AI Doesn't Make Your Team Faster. Shared Infrastructure Does.</title><link>https://lawzava.com/blog/2025-10-13-ai-team-productivity/</link><pubDate>Mon, 13 Oct 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-10-13-ai-team-productivity/</guid><description>Individual AI speedups are a distraction. The real gains come from treating AI as team infrastructure &amp;amp;ndash; embedded in docs, decisions, and onboarding.</description><content:encoded><![CDATA[<p>Every few weeks someone asks me how AI is changing team productivity. The honest answer: less than most people think, and in different ways than expected.</p>
<p>Individual engineers using  <a href="/blog/2021-06-28-github-copilot-first-look/"
   
   >Copilot</a>
 or ChatGPT to write code faster is fine. It&rsquo;s also not the point. One person moving 20% faster doesn&rsquo;t help if the team is still bottlenecked on the same things it was bottlenecked on six months ago: stale docs, unclear decisions, and onboarding that requires cornering a senior engineer for two hours.</p>
<p>The teams I see getting real gains are the ones that treat AI as shared infrastructure. Not a personal productivity hack. Infrastructure.</p>
<h2 id="what-that-looks-like-in-practice">What that looks like in practice</h2>
<p>A shared assistant for team documentation and search. Not a chatbot that guesses &ndash; something that points to actual internal sources and tells you who owns what. Automated meeting summaries that feed into the same system where the team already tracks decisions.  <a href="/blog/2022-03-21-engineering-onboarding-excellence/"
   
   >Onboarding workflows</a>
 where a new hire can get a credible first answer and a pointer to the right human, instead of posting in Slack and hoping someone responds.</p>
<p>None of these need perfect accuracy. They need consistent routing and clear expectations about when AI is advisory versus authoritative.</p>
<h2 id="the-measurement-trap">The measurement trap</h2>
<p>Here&rsquo;s where most teams go wrong. They  <a href="/blog/2020-08-31-developer-productivity-metrics/"
   
   >measure AI tool adoption</a>
. Number of prompts. Lines of code generated. That&rsquo;s like measuring how many emails your team sends and calling it productivity.</p>
<p>The only question that matters: is the team less stuck?</p>
<p>Fewer repeated questions about the same topic. A shorter gap between a decision being made and that decision being documented. Less rework because someone missed context from a meeting they weren&rsquo;t in.</p>
<p>If AI usage goes up but those numbers stay flat, you have added a toy, not infrastructure.</p>
<h2 id="docs-specifically">Docs, specifically</h2>
<p> <a href="/blog/2025-07-21-ai-documentation-systems/"
   
   >Documentation</a>
 is where AI has the most underrated impact. Not generating docs from scratch &ndash; that&rsquo;s garbage. But proposing small updates when code changes, flagging content that no longer matches reality, and making the update feel like a five-second approval instead of a batch project.</p>
<p>At a startup I ran, we struggled with  <a href="/blog/2022-06-13-engineering-documentation-practices/"
   
   >doc decay</a>
 like everyone else. The trick was making updates feel like routine housekeeping, not a chore you schedule for &ldquo;next sprint&rdquo; and never do.</p>
<h2 id="start-small-stay-boring">Start small, stay boring</h2>
<p>Pick one shared workflow. Make it reliable. Expand based on evidence, not enthusiasm. A small, visible win &ndash; like meeting notes that are actually useful the next day &ndash; changes team behavior more than any broad AI rollout plan.</p>
<p>The teams getting durable gains are the ones keeping AI practical, scoped, and accountable. Boring wins. As usual.</p>
]]></content:encoded></item><item><title>AI Pair Programming: It's a Junior Dev, Not a Wizard</title><link>https://lawzava.com/blog/2025-09-01-ai-pair-programming/</link><pubDate>Mon, 01 Sep 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-09-01-ai-pair-programming/</guid><description>Treat AI coding assistants like a fast, literal junior dev: tight constraints, critical review, and no expectations of architectural insight.</description><content:encoded><![CDATA[<p>I pair with AI every day: building production systems, contributing to Go, and prototyping new ideas. It&rsquo;s part of my workflow the same way version control and testing are &ndash; not because it&rsquo;s magical, but because it&rsquo;s useful when you know its limits.</p>
<p>The teams I&rsquo;ve seen get the most value from  <a href="/blog/2022-11-28-ai-code-assistants-evolution/"
   
   >AI coding assistants</a>
 treat them the same way: like a fast, literal junior developer. Emphasis on literal. The model does exactly what you ask, fills in gaps with plausible guesses, and never tells you when your approach is wrong. That&rsquo;s the mental model that keeps you productive without getting burned.</p>
<h2 id="where-it-shines">Where It Shines</h2>
<p>AI assistants are excellent at work that&rsquo;s well-scoped and pattern-driven. The kind of tasks where you know exactly what the output should look like but don&rsquo;t want to type it all out.</p>
<p>Boilerplate generation, test scaffolding from existing patterns, translating a clear spec into working code, exploring how an unfamiliar API works, and refactoring repetitive code paths into a cleaner abstraction when you already know what that abstraction should be.</p>
<p>I use it heavily for these cases and it genuinely saves hours per week. When I&rsquo;m writing Go and I need a new handler that follows the same pattern as the last ten handlers, the AI drafts it in seconds. I review, adjust, and move on.</p>
<h2 id="where-it-falls-apart">Where It Falls Apart</h2>
<p>The moment you need architectural judgment, project history, or business context, the AI becomes dangerous. Not useless &ndash; dangerous. Because it will confidently produce something that looks right, passes a quick glance, and introduces a subtle bug or design flaw that you don&rsquo;t catch until it&rsquo;s in production.</p>
<p>Watch for these warning signs:</p>
<ul>
<li>It repeats the same mistake after you correct it. The model doesn&rsquo;t learn within a session the way a human colleague does. If it keeps ignoring a constraint, it probably can&rsquo;t reliably hold that constraint in its current context.</li>
<li>It invents things. Functions that don&rsquo;t exist. Config options that aren&rsquo;t real. API endpoints it hallucinated from training data. Always verify against actual docs.</li>
<li>It optimizes for elegance over correctness. The model loves clean, compact code. Sometimes that means it refactors away an important edge case because the edge case made the code ugly.</li>
</ul>
<p>I&rsquo;ve caught all three of these in my own work. More than once.</p>
<h2 id="the-loop-that-works">The Loop That Works</h2>
<p>Long, open-ended chat sessions with AI produce garbage. The  <a href="/blog/2024-07-22-context-window-strategies/"
   
   >context window</a>
 fills up, the model loses track of constraints, and you end up in a back-and-forth that takes longer than writing the code yourself.</p>
<p>Short, focused loops work. Here&rsquo;s the pattern I use:</p>
<ol>
<li><strong>Define the task tightly.</strong> Inputs, outputs, constraints, existing style to match. Be specific. &ldquo;Add a function that does X given Y, handling Z edge case, matching the pattern in the rest of this file.&rdquo;</li>
<li><strong>Get a first pass.</strong> Let the AI draft it.</li>
<li><strong>Review critically.</strong> Not &ldquo;does this look right&rdquo; &ndash; trace through the logic. Check edge cases. Check error handling. Check that it respects the codebase conventions.</li>
<li><strong>Iterate on specific gaps.</strong> Don&rsquo;t ask for a full rewrite. Point at the specific line or logic branch that&rsquo;s wrong and ask for a fix.</li>
<li><strong>Integrate manually.</strong> Copy the code into your editor, run the tests, review the diff. The AI&rsquo;s output is a draft, not a commit.</li>
</ol>
<h2 id="give-it-real-context">Give It Real Context</h2>
<p>Vague prompts produce vague code. The single biggest improvement I&rsquo;ve seen is upgrading from &ldquo;write me a function that processes users&rdquo; to something with actual constraints:</p>
<p>&ldquo;Add a method <code>getActiveUsers(since time.Time)</code> to UserStore. Users are active if their LastSeen is after the given time. Return a slice sorted by LastSeen descending. If the store is empty, return nil, not an empty slice. Match the existing receiver pattern in this file.&rdquo;</p>
<p>That level of specificity is the difference between useful output and time wasted reviewing hallucinated code.</p>
<h2 id="the-trust-boundary">The Trust Boundary</h2>
<p>Here&rsquo;s the line I draw:  <a href="/blog/2024-11-11-ai-safety-production/"
   
   >AI output is untrusted input</a>
. Same as user input. Same as data from an external API. It goes through the same gates.</p>
<ul>
<li>Tests must pass.</li>
<li>Linter must pass.</li>
<li> <a href="/blog/2018-10-01-effective-code-reviews/"
   
   >Code review</a>
 still applies. A human reads the diff.</li>
<li>Security-sensitive code gets extra scrutiny regardless of who or what wrote it.</li>
</ul>
<p>Some teams have started rubber-stamping AI-generated code because &ldquo;the AI wrote it and it looks fine.&rdquo; That&rsquo;s how you get vulnerabilities in production. I&rsquo;ve seen it happen.</p>
<h2 id="the-honest-assessment">The Honest Assessment</h2>
<p>AI pair programming makes me faster at the boring parts of writing software. It doesn&rsquo;t make me better at the hard parts. Architecture decisions, security considerations, performance tradeoffs, understanding what the user actually needs &ndash; those are still entirely on me.</p>
<p>The  <a href="/blog/2023-11-13-ai-developer-productivity/"
   
   >developers who get the most value</a>
 are the ones who already know what good code looks like. The AI accelerates their output. The developers who rely on AI to compensate for gaps in their understanding ship bugs faster.</p>
<p>Use it as a tool. Review its work. Keep the sessions short. And never, ever merge without reading the diff.</p>
]]></content:encoded></item><item><title>AI Docs That Don't Lie to Your Users</title><link>https://lawzava.com/blog/2025-07-21-ai-documentation-systems/</link><pubDate>Mon, 21 Jul 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-07-21-ai-documentation-systems/</guid><description>Most AI documentation systems retrieve the wrong version, hallucinate details, and never admit uncertainty. Here&amp;amp;rsquo;s how to build one that actually helps.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Your AI docs system is only as good as its retrieval and its willingness to say &ldquo;I don&rsquo;t know.&rdquo; Use hybrid search, chunk by document structure with version metadata, cite sources in every answer, and treat freshness as a scheduled operational job &ndash; not a wish on the backlog.</p>
<hr>
<p>I contribute to Go regularly. I also use documentation from dozens of projects every day. And I can tell you the most common failure in  <a href="/blog/2024-09-16-technical-documentation-ai/"
   
   >developer documentation</a>
 isn&rsquo;t bad writing. It&rsquo;s bad retrieval.</p>
<p>A developer hits a cryptic error at midnight. They search. They get a result that looks right. It&rsquo;s from v2. They&rsquo;re on v4. The answer doesn&rsquo;t apply, but they don&rsquo;t realize it until they&rsquo;ve wasted forty minutes. Now multiply that across everyone using your docs.</p>
<p>That&rsquo;s the problem AI documentation systems need to solve. Not &ldquo;make the docs chatty.&rdquo; Make docs findable, version-accurate, and honest about gaps.</p>
<h2 id="the-three-problems-worth-solving">The Three Problems Worth Solving</h2>
<p><strong>Discovery.</strong> Users don&rsquo;t know your terminology. They describe symptoms, not concepts. A developer searching for &ldquo;connection refused after deploy&rdquo; might need the page about TLS configuration, but your keyword search returns the networking overview. Semantic search bridges this gap, but only if your chunks are meaningful units &ndash; not random 500-token slices.</p>
<p><strong>Version accuracy.</strong> Your API changed between v3 and v4. The auth flow is different. The error codes are different. If your retrieval doesn&rsquo;t filter by version, it will surface whatever is most popular in the index. Popular doesn&rsquo;t mean current.</p>
<p><strong>Freshness.</strong> Your product shipped a breaking change last Tuesday. The docs still describe the old behavior. Your AI docs system confidently explains how the old version works. This is worse than having no AI at all because it adds a layer of false authority.</p>
<h2 id="the-system-shape">The System Shape</h2>
<p>An AI docs system is a pipeline, not a chatbot with a  <a href="/blog/2023-04-03-vector-databases-explained/"
   
   >vector store</a>
 bolted on. The pieces that matter:</p>
<p><strong>Content store with metadata.</strong> Every chunk needs a stable ID, a version tag, a last-updated timestamp, and a source URL. Without these, you can&rsquo;t filter, you can&rsquo;t cite, and you can&rsquo;t detect staleness.</p>
<p><strong> <a href="/blog/2024-09-30-retrieval-strategies-rag/"
   
   >Hybrid retrieval</a>
.</strong>  <a href="/blog/2023-06-26-semantic-search-implementation/"
   
   >Semantic search</a>
 for conceptual questions. Keyword search for exact error codes, flag names, and parameter values. Neither alone is sufficient. The combination covers most queries. Add a reranking step that considers version relevance and recency &ndash; not just semantic similarity.</p>
<p><strong>Answer synthesis with citations.</strong> The model generates an answer, but every claim must trace to a specific chunk. If the retrieved chunks don&rsquo;t contain the answer, the system says so explicitly: &ldquo;This doesn&rsquo;t appear to be covered in the current docs. Here&rsquo;s the closest related section.&rdquo; A short answer with a source link beats a fluent paragraph that invents details.</p>
<p><strong>Feedback collection.</strong> Log every question that gets a low-confidence response or explicit negative feedback. Route those to doc owners weekly. This is the actual improvement loop. Without it, you&rsquo;re flying blind.</p>
<h2 id="chunking-matters-more-than-model-choice">Chunking Matters More Than Model Choice</h2>
<p>I&rsquo;ve seen teams agonize over which LLM to use for synthesis while completely ignoring their  <a href="/blog/2025-05-26-ai-data-pipelines/"
   
   >chunking strategy</a>
. The chunking is where the battle is won or lost.</p>
<p>Split by document structure. Headings, sections, and code blocks are natural semantic boundaries. A chunk should be a coherent unit that can answer a question on its own, or clearly can&rsquo;t. Token-count splitting produces fragments that retrieve well by similarity score but fail at actually answering questions.</p>
<p>Attach version metadata to every chunk. If someone asks about v4 auth, filter to v4 chunks before retrieval. This isn&rsquo;t a nice-to-have. It&rsquo;s the difference between helpful and harmful.</p>
<h2 id="freshness-is-ops-work">Freshness Is Ops Work</h2>
<p>Docs go stale. This isn&rsquo;t a failure of discipline &ndash; it&rsquo;s a consequence of shipping software. The solution isn&rsquo;t &ldquo;write better docs.&rdquo; The solution is automated freshness checks.</p>
<p>Schedule weekly jobs that validate links, compare API schema hashes against the documented version, and flag code samples that reference deprecated methods. When a check fails, create a ticket with clear ownership and a deadline. Not a backlog item. A real deadline.</p>
<p>At the fintech startup, we learned this the hard way with financial data: stale information in a financial context isn&rsquo;t just unhelpful, it&rsquo;s dangerous. The same principle applies to docs. Stale docs users trust are worse than no docs at all.</p>
<h2 id="measure-success-by-questions-answered">Measure Success by Questions Answered</h2>
<p>Pageviews are meaningless for docs. The metric that matters is: did the user get the right answer?</p>
<p>Track question success rate through explicit thumbs-up/down on AI answers. Track the count of unanswered or low-confidence questions &ndash; these are your improvement backlog. Track time-to-update for pages flagged as stale.</p>
<p>The feedback loop is the product. The AI layer is just the delivery mechanism. If unanswered questions aren&rsquo;t flowing back into your  <a href="/blog/2022-06-13-engineering-documentation-practices/"
   
   >documentation process</a>
, your AI docs system is a search box with extra steps.</p>
<p>Build retrieval that respects versions. Require citations. Admit uncertainty. Treat freshness as an operational discipline. Everything else is decoration.</p>
]]></content:encoded></item><item><title>Your AI System Looks Healthy. It Is Not.</title><link>https://lawzava.com/blog/2025-03-31-ai-observability-deep/</link><pubDate>Mon, 31 Mar 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-03-31-ai-observability-deep/</guid><description>Traditional monitoring will tell you your AI service is up. It won&amp;amp;rsquo;t tell you it&amp;amp;rsquo;s returning confident garbage. Here&amp;amp;rsquo;s what observability actually looks like for AI.</description><content:encoded><![CDATA[<p>Here&rsquo;s a scenario I&rsquo;ve seen three times this year.</p>
<p>An AI-powered feature is in production. Uptime: 99.9%. Latency: nominal. Error rate: near zero. Dashboards are green. Everyone is happy.</p>
<p>Except the answers are wrong 15% of the time, and nobody knows because nothing is measuring answer quality. The system is healthy. The outputs are not.</p>
<p>This is the fundamental gap in  <a href="/blog/2023-08-21-llm-observability/"
   
   >AI observability</a>
.  <a href="/blog/2017-03-20-why-observability-matters-more-than-monitoring/"
   
   >Traditional monitoring</a>
 tells you whether the service is running. It does not tell you whether the service is useful.</p>
<h2 id="why-ai-systems-fail-silently">Why AI systems fail silently</h2>
<p>A classic API returns structured data. If the response is malformed, you get a parse error. If the logic is wrong, a test catches it. The failure modes are usually loud and obvious.</p>
<p>AI systems fail quietly. The model returns a perfectly formatted response with a confident tone and completely wrong content. The HTTP status is 200. The latency is fine. The JSON is valid. And the user just got told that their refund was processed when it wasn&rsquo;t.</p>
<p>At a fintech startup, we had a similar problem with our financial news summarization pipeline, long before the current AI wave. The summaries looked plausible but occasionally attributed quotes to the wrong CEO or mixed up fiscal quarters. The system was &ldquo;working&rdquo; by every operational metric. The outputs were unreliable. We caught it only because a user complained, not because monitoring flagged it.</p>
<p>The lesson stuck with me. You can&rsquo;t monitor AI like you monitor a REST API. You need different signals.</p>
<h2 id="the-signals-that-actually-matter">The signals that actually matter</h2>
<p>I use a simple framework with five categories. If you are not tracking all five, you have blind spots.</p>
<p><strong>Traceability.</strong> For every response, you need to know: which model, which prompt version, which retrieved context, which tool calls. If you can&rsquo;t reconstruct why the model said what it said, you can&rsquo;t debug a bad answer. You&rsquo;re just guessing. I store a trace object alongside every response that includes model ID, prompt hash, retrieval IDs, and tool call logs. When something goes wrong, the trace is the first thing I pull.</p>
<p><strong>Quality signals.</strong> This is the hard one. You need some measure of whether the output was good. Heuristic checks catch obvious failures: empty responses, responses that are too long or too short, and responses that contain known-bad patterns. Sampled evaluation catches the subtle failures: a human or a second model scores a random slice of outputs against a rubric. Neither is perfect. Together they cover enough ground.</p>
<p><strong>Cost per outcome.</strong> Not cost per request, cost per successful outcome. A system that gets it right on the first try costs less than one that needs three retries and a human escalation. Track the full cost of getting to a good answer, including retries, fallbacks, and human review. This number will surprise you.</p>
<p><strong>Safety and policy.</strong> Refusal rates, blocked content, policy trigger counts. If your refusal rate spikes, something changed &ndash; either the inputs or the model behavior. If it drops to zero, something might be wrong too. These are canary signals.</p>
<p><strong>Operational basics.</strong> Latency percentiles by workflow (not globally &ndash; global averages hide everything), error rates with reason codes, token usage trends. The same stuff you track for any API, but broken down by the AI-specific dimensions that matter.</p>
<h2 id="the-prompt-versioning-problem">The prompt versioning problem</h2>
<p>Here is something that bites almost every team. Someone changes a prompt. Quality drops. Nobody connects the two events because the prompt change was not tracked alongside the quality metrics.</p>
<p>Treat prompts as production code. Version them. Deploy them through your normal release process. Tag every response with the prompt version that produced it. When quality dips, the first question should be: what changed since the last known-good state?</p>
<p>I version prompts in the same repo as the service code. A prompt change gets a PR, a review, and a run against  <a href="/blog/2024-02-19-evaluating-llm-applications/"
   
   >the eval suite</a>
 before it hits production. It sounds like overkill until the first time it prevents a regression. Then it sounds obvious.</p>
<h2 id="keep-it-lean">Keep it lean</h2>
<p>The temptation is to build a dashboard for everything. Do not. Start with the minimum set of signals that lets you answer one question: &ldquo;A user reported a bad answer. Can I explain why it happened and prevent it from happening again?&rdquo;</p>
<p>If you can answer that question end-to-end, your observability is good enough. If you can&rsquo;t, no amount of dashboards will save you.</p>
<p>Log the trace. Track quality. Version your prompts. Measure cost per outcome, not cost per request. That&rsquo;s the baseline. Everything else is optimization.</p>
]]></content:encoded></item><item><title>AI Code Review Is Mostly Noise</title><link>https://lawzava.com/blog/2025-02-03-ai-code-review/</link><pubDate>Mon, 03 Feb 2025 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2025-02-03-ai-code-review/</guid><description>I&amp;amp;rsquo;ve been running AI code review on real PRs for months. It catches some real bugs. It also generates a staggering amount of useless commentary.</description><content:encoded><![CDATA[<p>I&rsquo;m going to say something that will annoy AI tooling vendors: most AI code review output is garbage.</p>
<p>Not all of it. Maybe 15-20% is genuinely useful. But the other 80% is vague, style-obsessed, context-free commentary that would get a human reviewer told to try harder. &ldquo;Consider adding error handling here.&rdquo; Thanks. I hadn&rsquo;t considered that. In Go. Where every third line is error handling.</p>
<p>I&rsquo;ve been running AI review on PRs across production codebases for months. I wanted it to work. I really did. A tireless reviewer that catches logic bugs and security issues while humans  <a href="/blog/2018-10-01-effective-code-reviews/"
   
   >focus on architecture and design</a>
? Sign me up. The reality is more complicated.</p>
<h2 id="what-it-actually-catches">What it actually catches</h2>
<p>When AI code review works, it works well. The wins are real:</p>
<p><strong>Logic errors on changed paths.</strong> The model is good at spotting off-by-one errors, nil pointer risks, and missing edge cases in the specific lines that changed. It caught a race condition in a  <a href="/blog/2022-08-22-golang-concurrency-patterns/"
   
   >Go channel handler</a>
 that three human reviewers missed. That alone justified the experiment.</p>
<p><strong>Security surface area.</strong> SQL injection in a new endpoint. Hardcoded credentials in a test file that was about to be committed. An overly permissive CORS config. These are pattern-matching tasks, and models are decent at pattern matching.</p>
<p><strong>Copy-paste bugs.</strong> Someone copies a function, changes three of four parameters, and forgets the fourth. The model catches this reliably. Humans miss it because we read what we expect to see.</p>
<h2 id="where-it-falls-apart">Where it falls apart</h2>
<p><strong>Business context.</strong> The model doesn&rsquo;t know why your checkout flow has that weird retry logic. It doesn&rsquo;t know that the &ldquo;redundant&rdquo; nil check exists because a specific vendor API lies about its response types. It doesn&rsquo;t know your system&rsquo;s history. So it flags things that aren&rsquo;t problems and misses things that are.</p>
<p><strong>Large diffs.</strong> Anything over a few hundred lines and the model loses the thread. It starts making generic observations instead of specific findings. &ldquo;This function is complex and could benefit from refactoring.&rdquo; Really helpful on a 2,000-line migration PR.</p>
<p><strong>Style opinions nobody asked for.</strong> &ldquo;Consider using a more descriptive variable name.&rdquo; &ldquo;This comment could be more detailed.&rdquo; &ldquo;Consider extracting this into a separate function.&rdquo; If I wanted a style cop, I&rsquo;d configure a linter. AI review should find bugs, not police style.</p>
<h2 id="how-i-actually-use-it">How I actually use it</h2>
<p>After months of tuning, here&rsquo;s what works.</p>
<p><strong>Scope it to the diff.</strong> Don&rsquo;t let the model browse the entire repo. Give it the changed lines and maybe the immediate surrounding context. The more you feed it, the more generic the output gets.</p>
<p><strong>Demand specifics.</strong> My review prompt is aggressive about 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-text" data-lang="text"><span style="display:flex;"><span>Review this diff. For each finding:
</span></span><span style="display:flex;"><span>- Exact line number
</span></span><span style="display:flex;"><span>- Severity: critical / warning / info
</span></span><span style="display:flex;"><span>- What could fail at runtime
</span></span><span style="display:flex;"><span>- A concrete fix
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>Skip style suggestions. Skip anything a linter would catch.
</span></span><span style="display:flex;"><span>If nothing is wrong, say nothing.
</span></span></code></pre></div><p>That last line matters. Without it, the model will always find something to say because it&rsquo;s trained to be helpful. Sometimes the most helpful thing is silence.</p>
<p><strong>Track the hit rate.</strong> I log every AI review comment and whether the human reviewer accepted, dismissed, or ignored it. Our current acceptance rate is about 22%. That means 78% of AI review output is noise. Not great. But the 22% that lands includes some of the highest-severity findings in our review history.</p>
<p><strong>Never gate merges on it.</strong> AI review is advisory. A comment. A suggestion. The human reviewer decides. The moment you make AI review a merge blocker, you&rsquo;ve handed authority to a system that&rsquo;s wrong four times out of five. Don&rsquo;t do this.</p>
<h2 id="the-uncomfortable-math">The uncomfortable math</h2>
<p>AI code review costs money. Token costs, API calls, latency in your CI pipeline. At our current volume, it adds about 15-30 seconds per PR and a few dollars per day. That&rsquo;s cheap for the bugs it catches. But if you aren&rsquo;t measuring hit rate, you have no idea whether it&rsquo;s worth it.</p>
<p>Most teams set up AI review, get excited about the first few catches, and then never look at the numbers again. Six months later, developers have learned to ignore the comments entirely because most of them are noise. The tool becomes furniture.</p>
<h2 id="what-i-actually-want">What I actually want</h2>
<p>I want AI code review that knows when to shut up. That understands the system well enough to distinguish a real bug from an intentional design choice. That can read a PR description and connect the changes to the stated intent.</p>
<p>We aren&rsquo;t there yet. But the foundation is real. Scope it tight, demand specifics, measure ruthlessly, and never trust it to make decisions. It&rsquo;s a second pair of eyes, not a senior engineer.</p>
]]></content:encoded></item><item><title>Let AI Write Your First Draft, Not Your Docs</title><link>https://lawzava.com/blog/2024-09-16-technical-documentation-ai/</link><pubDate>Mon, 16 Sep 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-09-16-technical-documentation-ai/</guid><description>AI is a decent drafting assistant for technical docs. It&amp;amp;rsquo;s a terrible replacement for ownership.</description><content:encoded><![CDATA[<p>Technical documentation is one of the most undervalued forms of engineering communication. Everyone agrees it matters. Almost nobody prioritizes it. I&rsquo;ve watched this pattern repeat at every company I&rsquo;ve worked with, and the failure mode is always the same: docs rot because nobody owns them.</p>
<p>AI won&rsquo;t fix that problem. But it can remove the excuse.</p>
<h2 id="the-drafting-problem">The Drafting Problem</h2>
<p>The hardest part of writing docs is getting started. A blank page plus a busy engineer usually means no documentation. AI is genuinely good at solving this specific problem. Feed it the code structure, recent PRs, and changelogs, and you can get a usable first draft in minutes instead of hours.</p>
<p>That draft will be wrong in places. It will miss context. It will occasionally hallucinate an API parameter that doesn&rsquo;t exist. That&rsquo;s fine. A wrong draft you can edit is still faster than a correct document nobody writes.</p>
<h2 id="where-it-falls-apart">Where It Falls Apart</h2>
<p>The moment you treat AI output as finished documentation, you&rsquo;ve created something worse than no documentation at all. Wrong docs train people to distrust all docs. I&rsquo;ve seen this happen: a team auto-generates reference pages, skips review, and six months later nobody believes anything in the docs. They go straight to the source code. The docs become decoration.</p>
<p>The fix is dead simple: AI drafts, humans review, same PR as the code change. No separate workflow. No &ldquo;we&rsquo;ll update the docs later.&rdquo; If the docs don&rsquo;t land in the same review cycle as the code, they&rsquo;ll drift. This isn&rsquo;t a tooling problem. It&rsquo;s a discipline problem.</p>
<h2 id="the-search-use-case">The Search Use Case</h2>
<p>The other place AI helps is doc search. A retrieval-backed answer system that points users to the right section &ndash; with citations &ndash; is genuinely useful. The key constraint: it should refuse to answer when it can&rsquo;t find supporting material. &ldquo;I don&rsquo;t know, but here&rsquo;s the closest section&rdquo; is a better answer than a confident fabrication.</p>
<p>I&rsquo;ve been setting this up across a few projects and the pattern holds. Grounded search with citations works. Generative answers without grounding don&rsquo;t.</p>
<h2 id="what-i-would-actually-do">What I Would Actually Do</h2>
<p>If I were starting a docs workflow today:</p>
<ul>
<li>Generate first drafts from code context. Edit for accuracy and tone before merging.</li>
<li>Block releases when critical docs are stale. Make it a CI check if you have to.</li>
<li>Keep docs in the repo. Same review, same merge, same ownership.</li>
<li>Add retrieval-backed search with citation links. Refuse when unsupported.</li>
</ul>
<p>None of this is complicated. The tooling exists. The gap is always ownership and review discipline, not technology. AI makes the drafting faster. It doesn&rsquo;t make the caring automatic.</p>
]]></content:encoded></item><item><title>AI-Assisted Code Migration: What Actually Works</title><link>https://lawzava.com/blog/2024-09-02-ai-code-migration/</link><pubDate>Mon, 02 Sep 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-09-02-ai-code-migration/</guid><description>I used LLMs to help migrate a 200K-line Go codebase. The mechanical parts went fast. Everything else was still hard.</description><content:encoded><![CDATA[<p>Last quarter I helped a team migrate a large Go codebase from an internal HTTP framework to standard library patterns: around 200K lines across 40+ services. It was the kind of project where you know the end state, you know the transformation rules, and the work is 90% mechanical and 10% judgment calls that keep you up at night.</p>
<p>We used LLMs to handle the mechanical 90%. It worked. But &ldquo;it worked&rdquo; comes with enough caveats that it&rsquo;s worth being honest about what actually happened.</p>
<h2 id="what-the-ai-was-good-at">What the AI was good at</h2>
<p>Pattern matching and consistent transformation are the sweet spot. We had about 15 distinct patterns that needed to change: custom route handlers to standard ones, middleware signatures, and error response formats. For each pattern, we wrote a clear transformation rule with before/after examples.</p>
<p>The LLM could take a file, identify which patterns were present, and produce a transformed version. For straightforward cases, it was faster than any human and more consistent. It didn&rsquo;t get bored on file 200. It didn&rsquo;t introduce typos. It applied the same transformation rule the same way every time.</p>
<p>We processed about 300 files in two days that would have taken two engineers a couple of weeks. The mechanical savings were real.</p>
<h2 id="what-the-ai-was-bad-at">What the AI was bad at</h2>
<p>Judgment. The 10% of cases that didn&rsquo;t fit neatly into the transformation rules required understanding intent, not just pattern matching: a handler that looked standard but had a subtle side effect; a middleware chained in an unusual order for a specific reason; error handling intentionally different from the standard pattern because of a business rule documented nowhere except a Slack thread from 2021.</p>
<p>The LLM would happily transform these cases using the standard rules. The output would compile. The tests would pass. And the behavior would be subtly wrong in ways that only surfaced under specific conditions.</p>
<p>This is the dangerous part. AI-generated code that&rsquo;s almost right is harder to catch than code that&rsquo;s obviously wrong. It passes automated checks and casual review. Then you find the bug three weeks later when a customer reports something weird.</p>
<h2 id="the-workflow-that-worked">The workflow that worked</h2>
<p>Here&rsquo;s what we settled on after the first batch of surprises:</p>
<p><strong>Step 1: Scope with samples.</strong> Don&rsquo;t start with &ldquo;migrate everything.&rdquo; Pick 10 representative files that cover the range of patterns. Run them through the LLM. Review the output manually. This reveals the transformation rules you need and the edge cases you&rsquo;ll need to handle differently.</p>
<p><strong>Step 2: One rule per pattern.</strong> Write each transformation rule explicitly. Not &ldquo;update the HTTP handlers,&rdquo; but &ldquo;replace <code>framework.Handler(func(ctx *Ctx) error {...})</code> with <code>http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {...})</code> and move error handling to&hellip;&rdquo; The more specific the rule, the better the LLM follows it.</p>
<p><strong>Step 3: Small batches, continuous validation.</strong> We processed 10-20 files at a time. After each batch: run the build, run the tests, run the linter, and do a quick diff review. If something broke, fix it and update the transformation rule before continuing. Don&rsquo;t accumulate 200 files of changes and then try to debug a test failure.</p>
<p><strong>Step 4: Flag the hard ones.</strong> When the LLM produced a transformation that looked different from the standard pattern, we flagged it for human review instead of forcing it through. About 15% of files got flagged. Those were the ones where the AI saved us no time at all &ndash; but catching them early saved us from a lot of pain later.</p>
<h2 id="treat-ai-output-as-draft-code">Treat AI output as draft code</h2>
<p>This is the principle that made the whole process work. Every AI-generated change went through the same review process as a human-written change. Same CI checks. Same code review. Same approval workflow.</p>
<p>The temptation is to trust the AI more because it&rsquo;s consistent and fast. Resist that temptation. The AI is a junior engineer who types incredibly fast and never pushes back on your instructions. That&rsquo;s useful. It isn&rsquo;t the same as reliable.</p>
<h2 id="what-id-do-differently">What I&rsquo;d do differently</h2>
<p>I&rsquo;d build the evaluation harness first. We started the migration, then realized we didn&rsquo;t have a good way to verify that migrated services behaved identically to the originals. We retrofitted integration tests, but it would have been faster to invest that time upfront.</p>
<p>I&rsquo;d also version the transformation rules alongside the code. We iterated on the rules as we discovered edge cases, but we didn&rsquo;t track which version of the rules produced which batch of changes. When we found a bug, tracing it back to the specific rule version that caused it was harder than it should have been.</p>
<h2 id="the-honest-summary">The honest summary</h2>
<p>AI made a two-month migration take three weeks. That&rsquo;s a genuine win. But it didn&rsquo;t change the nature of the hard parts. Scoping, validation, edge case handling, and human judgment on ambiguous cases &ndash; those are still the bottleneck. The AI accelerated the parts that were already straightforward.</p>
<p>Use AI for migrations. Just don&rsquo;t pretend it replaces the discipline that makes migrations safe.</p>
]]></content:encoded></item><item><title>Architecting AI-Native Applications (Without the Delusion)</title><link>https://lawzava.com/blog/2024-02-05-ai-native-architecture/</link><pubDate>Mon, 05 Feb 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-02-05-ai-native-architecture/</guid><description>AI-native apps are fundamentally different from a model bolted onto a CRUD app. How I structure them &amp;amp;ndash; with code, layers, and hard-won opinions.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>AI-native means the model is in the critical path, not a sidebar. That requires confidence-aware routing, structured feedback loops, explicit fallback chains, and a UX that doesn&rsquo;t pretend the system is deterministic. This is the architecture I use.</p>
<hr>
<p>There&rsquo;s a particular kind of architectural diagram I keep seeing in pitch decks. A clean box labeled &ldquo;AI&rdquo; sits neatly between the frontend and the database, connected by two arrows. Everything looks tidy. Everything is a lie.</p>
<p>AI-native applications are messy. The model is non-deterministic. Responses vary in quality. Latency is unpredictable. Costs scale with usage in ways that don&rsquo;t match traditional compute. And yet &ndash; the product&rsquo;s core value depends on this unreliable component working well enough, often enough, that users trust it.</p>
<p>I&rsquo;ve been building these systems for the past year across telcos and fintech companies. The architecture that actually works looks nothing like that clean diagram.</p>
<h2 id="what-ai-native-actually-means">What &ldquo;AI-native&rdquo; actually means</h2>
<p>Let me be precise. An AI-native application is one where removing the AI component wouldn&rsquo;t leave you with a simpler app &ndash; it would leave you with no app. The AI isn&rsquo;t a feature. It&rsquo;s the product.</p>
<p>This creates three architectural consequences you can&rsquo;t ignore:</p>
<ol>
<li><strong>Non-determinism is in the critical path.</strong> The same input can produce different outputs. Your architecture must absorb this instead of pretending it away.</li>
<li><strong>Quality is a spectrum, not a boolean.</strong> You evaluate on ranges and intent, not exact matches.</li>
<li><strong>The system must learn from usage.</strong> Feedback isn&rsquo;t a nice-to-have &ndash; it&rsquo;s what keeps the product from degrading.</li>
</ol>
<h2 id="the-layered-architecture-i-actually-use">The layered architecture I actually use</h2>
<p>After building several of these systems, I&rsquo;ve settled on a layered approach. Not because layers are fashionable, but because each layer has a distinct failure mode and a distinct owner.</p>
<pre tabindex="0"><code>┌─────────────────────────────────────┐
│         Experience Layer            │  &lt;- Uncertainty communication, UI
├─────────────────────────────────────┤
│       Orchestration Layer           │  &lt;- Routing, fallbacks, workflows
├─────────────────────────────────────┤
│         AI Services Layer           │  &lt;- Model calls, retrieval, tools
├─────────────────────────────────────┤
│      Quality &amp; Safety Layer         │  &lt;- Validation, filtering, policy
├─────────────────────────────────────┤
│       Data &amp; Context Layer          │  &lt;- Knowledge, memory, embeddings
├─────────────────────────────────────┤
│     Feedback &amp; Analytics Layer      │  &lt;- Learning, monitoring, eval
└─────────────────────────────────────┘
</code></pre><p>These don&rsquo;t need to be separate services. In most systems I build, they start as packages within a single Go binary. The point is that each responsibility exists, is testable, and has clear ownership.</p>
<h2 id="designing-for-uncertainty">Designing for uncertainty</h2>
<p>This is the part most teams get wrong. They treat the model like a function: input goes in, correct output comes out. Then they&rsquo;re shocked when production users get hallucinated garbage.</p>
<p>The architecture needs to absorb uncertainty at every level. Here is how I handle it in the orchestration layer:</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">Confidence</span> <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> (
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ConfidenceHigh</span>   <span style="color:#a6e22e">Confidence</span> = <span style="color:#66d9ef">iota</span> <span style="color:#75715e">// Route directly to user</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ConfidenceMedium</span>                    <span style="color:#75715e">// Add verification step</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ConfidenceLow</span>                       <span style="color:#75715e">// Escalate or fallback</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">type</span> <span style="color:#a6e22e">AIResponse</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Content</span>    <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Confidence</span> <span style="color:#a6e22e">Confidence</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ModelID</span>    <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Latency</span>    <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">TokensUsed</span> <span style="color:#66d9ef">int</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">s</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">Service</span>) <span style="color:#a6e22e">HandleRequest</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">req</span> <span style="color:#a6e22e">Request</span>) (<span style="color:#f92672">*</span><span style="color:#a6e22e">Response</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">aiResp</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">aiClient</span>.<span style="color:#a6e22e">Generate</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">ToPrompt</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">s</span>.<span style="color:#a6e22e">fallbackResponse</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</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">switch</span> <span style="color:#a6e22e">aiResp</span>.<span style="color:#a6e22e">Confidence</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">case</span> <span style="color:#a6e22e">ConfidenceHigh</span>:
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">directResponse</span>(<span style="color:#a6e22e">aiResp</span>), <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">case</span> <span style="color:#a6e22e">ConfidenceMedium</span>:
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">verified</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">verify</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">aiResp</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:#a6e22e">s</span>.<span style="color:#a6e22e">directResponse</span>(<span style="color:#a6e22e">aiResp</span>), <span style="color:#66d9ef">nil</span> <span style="color:#75715e">// Degrade gracefully</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">verified</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">case</span> <span style="color:#a6e22e">ConfidenceLow</span>:
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">s</span>.<span style="color:#a6e22e">escalate</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>, <span style="color:#a6e22e">aiResp</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">s</span>.<span style="color:#a6e22e">fallbackResponse</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">req</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Confidence doesn&rsquo;t need to be a number shown to the user. It&rsquo;s an internal signal that controls what happens next. High confidence goes straight through. Medium confidence gets a verification step &ndash; maybe a retrieval check, maybe a second model call with a stricter prompt. Low confidence hits the fallback path.</p>
<p>The fallback path is critical. Every AI-native app needs one, and it should be designed before the happy path. What does the product do when the model is down? When it returns garbage? When it takes 30 seconds to respond? If the answer is &ldquo;crash&rdquo; or &ldquo;show a spinner forever,&rdquo; the architecture isn&rsquo;t ready for production.</p>
<h2 id="feedback-loops-as-architecture-not-afterthought">Feedback loops as architecture, not afterthought</h2>
<p>Every request through the system should produce a feedback record. Not because you have time to look at them all, but because without them you&rsquo;re blind to degradation.</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">FeedbackRecord</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">RequestID</span>   <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Prompt</span>      <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Response</span>    <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ModelID</span>     <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Confidence</span>  <span style="color:#a6e22e">Confidence</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Latency</span>     <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">UserSignal</span>  <span style="color:#a6e22e">UserSignal</span>  <span style="color:#75715e">// Accepted, rejected, edited, ignored</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Outcome</span>     <span style="color:#a6e22e">Outcome</span>     <span style="color:#75715e">// Success, partial, failure</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Timestamp</span>   <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Time</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">type</span> <span style="color:#a6e22e">UserSignal</span> <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> (
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">SignalNone</span>     <span style="color:#a6e22e">UserSignal</span> = <span style="color:#66d9ef">iota</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">SignalAccepted</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">SignalRejected</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">SignalEdited</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">SignalIgnored</span>
</span></span><span style="display:flex;"><span>)
</span></span></code></pre></div><p>The user signal is the most valuable field. Did the user accept the output? Edit it? Ignore it entirely? That data drives everything: prompt improvements, model selection changes, confidence calibration.</p>
<p>I learned this the hard way on a project where we shipped an AI feature without feedback instrumentation. Two months later, we had no idea whether the model&rsquo;s quality had drifted or whether users had simply stopped trusting it. We were debugging with anecdotes. Never again.</p>
<h2 id="routing-without-the-phd">Routing without the PhD</h2>
<p>You don&rsquo;t need a machine learning model to route requests to the right model. A few rules go a long way.</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">RouterConfig</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Rules</span> []<span style="color:#a6e22e">RoutingRule</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">type</span> <span style="color:#a6e22e">RoutingRule</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Condition</span> <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">req</span> <span style="color:#a6e22e">Request</span>) <span style="color:#66d9ef">bool</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">ModelID</span>   <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">Timeout</span>   <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</span>
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">MaxTokens</span> <span style="color:#66d9ef">int</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">DefaultRouter</span>() <span style="color:#f92672">*</span><span style="color:#a6e22e">RouterConfig</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">return</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">RouterConfig</span>{
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">Rules</span>: []<span style="color:#a6e22e">RoutingRule</span>{
</span></span><span style="display:flex;"><span>			{
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">Condition</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">r</span> <span style="color:#a6e22e">Request</span>) <span style="color:#66d9ef">bool</span> { <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">TokenEstimate</span>() &lt; <span style="color:#ae81ff">200</span> },
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">ModelID</span>:   <span style="color:#e6db74">&#34;fast-small&#34;</span>,
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">Timeout</span>:   <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:#a6e22e">MaxTokens</span>: <span style="color:#ae81ff">512</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">Condition</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">r</span> <span style="color:#a6e22e">Request</span>) <span style="color:#66d9ef">bool</span> { <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">RequiresReasoning</span>() },
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">ModelID</span>:   <span style="color:#e6db74">&#34;capable-large&#34;</span>,
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">Timeout</span>:   <span style="color:#ae81ff">30</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">MaxTokens</span>: <span style="color:#ae81ff">4096</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">Condition</span>: <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">r</span> <span style="color:#a6e22e">Request</span>) <span style="color:#66d9ef">bool</span> { <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">true</span> }, <span style="color:#75715e">// Default</span>
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">ModelID</span>:   <span style="color:#e6db74">&#34;balanced-medium&#34;</span>,
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">Timeout</span>:   <span style="color:#ae81ff">15</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">MaxTokens</span>: <span style="color:#ae81ff">2048</span>,
</span></span><span style="display:flex;"><span>			},
</span></span><span style="display:flex;"><span>		},
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Small requests get the fast model. Reasoning-heavy requests get the capable one. Everything else gets the balanced option. This isn&rsquo;t clever. It doesn&rsquo;t need to be. It just needs to keep costs predictable and latency acceptable.</p>
<p>The rules are configuration, not code. When you want to change routing &ndash; because a new model dropped, or costs shifted, or you learned that certain request types need more capability &ndash; you change the config. You don&rsquo;t redeploy.</p>
<h2 id="ux-that-respects-the-users-intelligence">UX that respects the user&rsquo;s intelligence</h2>
<p>The biggest UX mistake in AI-native apps is pretending the system is certain when it isn&rsquo;t. Users can handle uncertainty. They can&rsquo;t handle being lied to.</p>
<p>A few principles I follow:</p>
<ul>
<li><strong>Show your work when confidence is low.</strong> If the model retrieved documents to answer a question, show which ones. Let the user verify.</li>
<li><strong>Offer refinement, not just results.</strong> A &ldquo;try again&rdquo; button is lazy. A &ldquo;here is what I found, want me to focus on X?&rdquo; is useful.</li>
<li><strong>Keep the UI stable on failure.</strong> When the model times out, the product should still work. Maybe with reduced functionality, but it shouldn&rsquo;t break.</li>
</ul>
<p>The best AI-native UIs I&rsquo;ve seen treat the model like a very fast but occasionally wrong colleague. You check their work on important things. You trust them on routine things. The UI should support that mental model.</p>
<h2 id="the-data-layer-determines-everything">The data layer determines everything</h2>
<p>I have a saying I repeat in these situations: your AI feature is only as good as the data you feed it.</p>
<p>The context layer needs to support structured facts (database records, configuration), unstructured knowledge (documents, guides, prior conversations), and session memory (what happened earlier in this interaction).</p>
<p>Retrieval quality matters more than model quality for most applications. I&rsquo;ve seen teams spend weeks prompt-engineering their way around a bad retrieval pipeline. Fix the retrieval. The prompts will get simpler.</p>
<h2 id="operational-discipline">Operational discipline</h2>
<p>Production AI-native apps need monitoring that goes beyond uptime checks:</p>
<ul>
<li><strong>Quality monitoring.</strong> Track your confidence distribution over time. If low-confidence responses are increasing, something changed.</li>
<li><strong>Cost tracking per request type.</strong> Not aggregate cost &ndash; per-type. You need to know which workflows are expensive.</li>
<li><strong>Latency budgets.</strong> Set them per workflow, not globally. A search feature and a document analysis feature have different acceptable latencies.</li>
<li><strong>Drift detection.</strong> Model behavior changes. Provider behavior changes. Your data changes. Monitor for all of it.</li>
</ul>
<h2 id="the-honest-version">The honest version</h2>
<p>AI-native architecture isn&rsquo;t a clean diagram. It&rsquo;s a set of hard choices about where to trust the model, where to verify, where to fall back, and how to learn from every interaction. The teams that accept this build reliable products. The teams that draw clean boxes build impressive demos that break in production.</p>
<p>Build the fallback first. Instrument everything. Let the feedback loop make the system smarter over time. That&rsquo;s the architecture that actually ships.</p>
]]></content:encoded></item><item><title>Stop Paying OpenAI to Test Your Prompts</title><link>https://lawzava.com/blog/2024-01-22-local-llms-development/</link><pubDate>Mon, 22 Jan 2024 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2024-01-22-local-llms-development/</guid><description>Local LLMs are finally good enough for development. Use them for iteration, keep the API bills for production.</description><content:encoded><![CDATA[<p>I keep watching developers iterate on prompts by hitting GPT-4 hundreds of times a day. Every keystroke, another API call. Every experiment, another line on the invoice. Then they act surprised when the monthly bill shows up.</p>
<p>This is dumb. Not because the hosted models are bad &ndash; they are great. But because you don&rsquo;t need frontier-model quality to test whether your prompt template works, your parsing logic handles edge cases, or your UI renders a streamed response correctly.</p>
<p>Run a local model. Iterate fast. Save the API calls for when you actually need them.</p>
<h2 id="the-actual-reasons-to-go-local">The actual reasons to go local</h2>
<p>Forget the hand-wavy &ldquo;sovereignty&rdquo; arguments for a moment. The practical reasons are simple:</p>
<p><strong>Speed.</strong> No network round-trip. No rate limits. No waiting in a queue behind someone else&rsquo;s batch job. I can test a prompt change in under a second on a MacBook with Ollama running a 7B model. That feedback loop matters when you&rsquo;re doing fifty iterations in an afternoon.</p>
<p><strong>Cost.</strong> Zero marginal cost per request. I ran through over a thousand prompt variations last month while building an extraction pipeline. On GPT-4, that would have been a few hundred dollars. Locally, it was electricity.</p>
<p><strong>Privacy.</strong> Some of my work involves data I can&rsquo;t send to a third-party API. Full stop. Local inference solves that problem without paperwork.</p>
<h2 id="the-trade-offs-are-real-so-stop-pretending-otherwise">The trade-offs are real, so stop pretending otherwise</h2>
<p>Local models aren&rsquo;t frontier models. A 7B parameter model running on your laptop isn&rsquo;t going to match GPT-4 on complex reasoning tasks. That&rsquo;s fine. You aren&rsquo;t using it for production quality &ndash; you&rsquo;re using it for development velocity.</p>
<p>Where local models genuinely fall short:</p>
<ul>
<li>Multi-step reasoning. They lose the thread.</li>
<li>Long context windows. Most local models tap out well before 128k tokens.</li>
<li>Consistent formatting. They drift more on structured output tasks.</li>
<li>Nuanced instruction following. Subtle prompt changes sometimes get ignored.</li>
</ul>
<p>If your development workflow requires frontier-quality responses at every step, local models aren&rsquo;t for you. But honestly, most development workflows don&rsquo;t. You need a model that&rsquo;s good enough to validate your integration logic, and local models clear that bar easily.</p>
<h2 id="my-actual-setup">My actual setup</h2>
<p>I keep it simple. Ollama for the runtime, a 7B model as default, and an environment variable to swap between local and remote.</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">getLLMConfig</span>() <span style="color:#a6e22e">LLMConfig</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">if</span> <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Getenv</span>(<span style="color:#e6db74">&#34;USE_LOCAL_LLM&#34;</span>) <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;true&#34;</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">return</span> <span style="color:#a6e22e">LLMConfig</span>{
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">BaseURL</span>: <span style="color:#e6db74">&#34;http://localhost:11434&#34;</span>,
</span></span><span style="display:flex;"><span>			<span style="color:#a6e22e">Model</span>:   <span style="color:#e6db74">&#34;mistral&#34;</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">LLMConfig</span>{
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">BaseURL</span>: <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Getenv</span>(<span style="color:#e6db74">&#34;LLM_API_URL&#34;</span>),
</span></span><span style="display:flex;"><span>		<span style="color:#a6e22e">Model</span>:   <span style="color:#a6e22e">os</span>.<span style="color:#a6e22e">Getenv</span>(<span style="color:#e6db74">&#34;LLM_MODEL&#34;</span>),
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That&rsquo;s it. The rest of the application doesn&rsquo;t care which model it&rsquo;s talking to. The interface is the same, the error handling is the same, the retry logic is the same. When I want to validate quality against the real model, I flip the variable and run my eval suite.</p>
<h2 id="the-workflow-that-actually-works">The workflow that actually works</h2>
<ol>
<li><strong>Develop locally.</strong> Prompt changes, parsing logic, UI work, error handling. All against the local model.</li>
<li><strong>Eval against remote.</strong> Before merging, run the same test cases against the production model. Compare outputs.</li>
<li><strong>Ship with confidence.</strong> The integration is tested. The quality is validated. The bill is reasonable.</li>
</ol>
<p>The key insight: your development model and your production model don&rsquo;t need to be the same. They need to share the same interface.</p>
<h2 id="when-to-skip-local-entirely">When to skip local entirely</h2>
<p>Be honest about the cases where local doesn&rsquo;t help:</p>
<ul>
<li>You&rsquo;re doing few-shot prompt engineering where response quality <em>is</em> the variable you&rsquo;re testing.</li>
<li>Your feature depends on capabilities only frontier models have (vision, very long context, tool use with complex chains).</li>
<li>You&rsquo;re evaluating model-specific behavior like safety responses or refusal patterns.</li>
</ul>
<p>In those cases, just use the API. The point isn&rsquo;t religious purity about local inference. The point isn&rsquo;t burning money on API calls when a local model would have told you the same thing.</p>
<h2 id="stop-overthinking-it">Stop overthinking it</h2>
<p>Install Ollama. Pull a model. Point your dev config at localhost. You will iterate faster, spend less, and keep sensitive data on your own machine. When you need the real thing, it&rsquo;s one environment variable away.</p>
<p>This isn&rsquo;t complicated. It&rsquo;s just discipline.</p>
]]></content:encoded></item><item><title>Responsible AI Is Just Risk Management. Treat It That Way.</title><link>https://lawzava.com/blog/2023-10-16-responsible-ai-development/</link><pubDate>Mon, 16 Oct 2023 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2023-10-16-responsible-ai-development/</guid><description>Responsible AI is not an ethics committee. It is operational risk management, and teams that treat it otherwise are building liabilities.</description><content:encoded><![CDATA[<p>I keep seeing &ldquo;responsible AI&rdquo; treated like a corporate checkbox. A slide deck. A committee that meets quarterly and produces guidelines nobody reads. This is wrong, and it&rsquo;s going to hurt people.</p>
<p>My background is in cyber defense. National cyber-defense exercises taught me something simple: safety isn&rsquo;t a layer you bolt on. It&rsquo;s a property of how the system is designed, operated, and monitored. Responsible AI is no different. It&rsquo;s operational risk management. The moment you separate it from engineering and hand it to a policy team, you have lost.</p>
<h2 id="the-problem-with-principles">The Problem With Principles</h2>
<p>Every company publishing AI principles has the same list. Transparency. Fairness. Safety. Privacy. Accountability. These are fine as goals. They&rsquo;re useless as engineering requirements.</p>
<p>&ldquo;Be fair&rdquo; doesn&rsquo;t tell an engineer what to test. &ldquo;Be transparent&rdquo; doesn&rsquo;t tell a product manager what to disclose. The teams shipping reliable AI features are the ones translating these words into concrete, testable constraints. Everyone else is writing poetry.</p>
<h2 id="what-actually-matters">What Actually Matters</h2>
<p><strong>Know your blast radius.</strong> Before you ship, ask: who gets hurt when this is wrong? Not &ldquo;who benefits when it works&rdquo; &ndash; who gets hurt when it fails? If you can&rsquo;t answer that question, you aren&rsquo;t ready to ship.</p>
<p><strong>Test for the failures you fear.</strong> Adversarial inputs. Edge cases. Subgroup performance. I don&rsquo;t care if your average accuracy is 95% if it drops to 60% for a specific population. Test for it. Measure it. Fix it or document why you can&rsquo;t.</p>
<p><strong>Make AI involvement visible.</strong> Users deserve to know when they&rsquo;re interacting with a model. Not buried in terms of service. In the UI. Clearly. This isn&rsquo;t a philosophical position &ndash; it&rsquo;s a practical one. Users who know they&rsquo;re talking to AI calibrate their trust appropriately. Users who don&rsquo;t are one confident hallucination away from a support nightmare.</p>
<p><strong>Own the system end-to-end.</strong> Someone &ndash; a name, not a team &ndash; is responsible for the AI system&rsquo;s behavior in production. That person has the authority to kill the feature if it misbehaves. If nobody has that authority, you don&rsquo;t have accountability. You have diffusion of responsibility.</p>
<h2 id="the-defense-mindset">The Defense Mindset</h2>
<p>In cyber defense, we operate on the assumption that the system will be attacked and will sometimes fail. We design for containment, not prevention. The same mindset applies to AI.</p>
<p>Your model will hallucinate. Your prompts will be injected. Your data will drift. The question isn&rsquo;t whether these things happen. The question is whether you detect them quickly and respond appropriately.</p>
<p>Build monitoring that catches behavioral drift. Ship with a kill switch. Have a rollback plan that doesn&rsquo;t require an incident call with twelve people.</p>
<p>Responsible AI isn&rsquo;t about being good. It&rsquo;s about being prepared. The teams that understand this distinction are the ones I trust to ship AI features that last.</p>
]]></content:encoded></item><item><title>AI Technical Debt Is Eating Your Codebase (You Just Cannot See It Yet)</title><link>https://lawzava.com/blog/2023-10-02-ai-technical-debt/</link><pubDate>Mon, 02 Oct 2023 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2023-10-02-ai-technical-debt/</guid><description>AI features create a new species of technical debt that hides in prompts, data pipelines, and model versions. By the time you notice it, the cleanup bill is brutal.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Your AI features are accumulating debt in places your existing tooling can&rsquo;t see: prompts nobody versions, data nobody validates, models nobody benchmarks after deploy. Treat it like any other dependency: track it, test it, or pay for it later at 10x the cost.</p>
<p>I spend a lot of my time helping teams integrate AI into financial infrastructure: open-source ledger systems, strict correctness requirements, and environments where &ldquo;it usually works&rdquo; is not an acceptable quality bar. What I&rsquo;ve learned is that AI technical debt is sneakier than the regular kind.</p>
<p>Traditional tech debt is familiar. We all know what it looks like: rushed code, missing tests, dependencies you should have updated six months ago. AI debt is different. It accumulates silently because the system keeps producing outputs that look plausible. By the time you notice something is wrong, you&rsquo;re already deep in the hole.</p>
<h2 id="the-five-flavors-of-ai-debt">The Five Flavors of AI Debt</h2>
<p>At a fintech company, I started categorizing the debt I kept seeing across teams. It clusters into five buckets, and they overlap in annoying ways.</p>
<p><strong>Model debt.</strong> Nobody knows which model version is running in production. Nobody benchmarked the current version against the previous one. The model provider shipped an update, behavior shifted, and three weeks later someone noticed the outputs were slightly worse. By then, good luck figuring out what changed.</p>
<p><strong>Prompt debt.</strong> Prompts scattered across files, notebooks, Slack messages, and someone&rsquo;s local branch. Duplicated logic. No review process. One engineer tweaks a system prompt on Tuesday, another tweaks the same prompt on Thursday, and by Friday they&rsquo;re debugging each other&rsquo;s changes without knowing it.</p>
<p><strong>Data debt.</strong> Unknown provenance. &ldquo;Where did this training data come from?&rdquo; &ldquo;I think Jake downloaded it from somewhere.&rdquo; Weak validation, unmeasured drift. The inputs your model sees in production look nothing like what it was tested on, and nobody is tracking the gap.</p>
<p><strong>Evaluation debt.</strong> This is the most dangerous one. No baseline. No regression suite. The team ships a change, eyeballs a few outputs, and declares it good. Then three weeks later users start complaining and there&rsquo;s nothing to compare against.</p>
<p><strong>Infrastructure debt.</strong> Brittle integrations, no fallbacks, and cost attribution that amounts to &ldquo;the AI line item went up, who knows why.&rdquo; In fintech, where we deal with financial transactions, this kind of opacity is unacceptable. But I see it everywhere.</p>
<h2 id="the-warning-signs">The Warning Signs</h2>
<p>You&rsquo;re already in debt if any of these sound familiar:</p>
<ul>
<li>Outputs differ between staging and production and nobody can explain why</li>
<li>You ship prompt changes without running any automated evaluation</li>
<li>You can&rsquo;t answer &ldquo;which model version and prompt version are in production right now?&rdquo; in under thirty seconds</li>
<li>Your data sources are described as &ldquo;the usual ones&rdquo; in documentation that doesn&rsquo;t exist</li>
<li>Your AI costs went up 40% last month and the best explanation is &ldquo;more usage, probably&rdquo;</li>
</ul>
<p>If you nodded at three or more, you have a problem. If you nodded at all five, you have a fire.</p>
<h2 id="what-actually-works">What Actually Works</h2>
<h3 id="version-everything">Version Everything</h3>
<p>Prompts are code. Full stop. At one fintech company, we moved all prompts into version-controlled templates with required code review for changes. It felt like overhead for about a week. Then someone caught a regression in review that would have taken days to debug in production.</p>
<p>Models are dependencies. Pin them. Track deployment dates. Record benchmark results at deploy time so you have a comparison point when behavior drifts.</p>
<h3 id="build-your-eval-suite-before-you-need-it">Build Your Eval Suite Before You Need It</h3>
<p>A lightweight evaluation set &ndash; even 30 representative inputs with expected outputs &ndash; will save you more debugging time than almost any other investment. Run it before every deploy. Run it on a schedule against production. When it catches something, you&rsquo;ll be glad you spent the half-day building it.</p>
<h3 id="make-cost-attribution-explicit">Make Cost Attribution Explicit</h3>
<p>If you can&rsquo;t attribute AI costs to specific features and workflows, you&rsquo;re flying blind. At one fintech company, we tag every API call with the feature path that triggered it. When costs spike, we know exactly which workflow is responsible within minutes, not days.</p>
<h3 id="monitor-drift-not-just-uptime">Monitor Drift, Not Just Uptime</h3>
<p>Traditional monitoring asks &ldquo;is it up?&rdquo; AI monitoring also needs to ask &ldquo;is it still correct?&rdquo; Track output distributions. Flag anomalies. Set up alerts when the model&rsquo;s behavior shifts beyond your tolerance band. This isn&rsquo;t optional &ndash; it&rsquo;s the equivalent of testing in production, which you&rsquo;re already doing whether you admit it or not.</p>
<h2 id="paying-it-down">Paying It Down</h2>
<p>The approach I recommend is the same one I use for regular tech debt: risk-driven, regular, and documented.</p>
<p>Pick the highest-risk debt category. For most teams, that&rsquo;s evaluation debt because it blocks your ability to safely address everything else. Stabilize it. Then move to the next.</p>
<p>Write down every decision. Not a novel &ndash; a paragraph. &ldquo;We pinned model version X because benchmark Y showed regression on task Z.&rdquo; When future-you is debugging at 2 AM, these notes are the difference between a thirty-minute fix and an all-nighter.</p>
<p>AI systems can be reliable. But only if you treat invisible debt with the same seriousness as the kind your linter can catch.</p>
]]></content:encoded></item><item><title>Your LLM Bill Is Your Own Fault</title><link>https://lawzava.com/blog/2023-07-24-ai-cost-optimization/</link><pubDate>Mon, 24 Jul 2023 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2023-07-24-ai-cost-optimization/</guid><description>Everyone&amp;amp;rsquo;s complaining about LLM costs. Almost nobody has done the basics: caching, model routing, or even measuring what they&amp;amp;rsquo;re spending per feature.</description><content:encoded><![CDATA[<p>I got a call last week from a team that was &ldquo;shocked&rdquo; their OpenAI bill hit $14,000 in June. They&rsquo;re a 12-person startup. I asked three questions:</p>
<ol>
<li>Do you cache any responses? No.</li>
<li>Do you use GPT-4 for everything or route to 3.5 for simpler tasks? Everything goes to GPT-4.</li>
<li>Do you set max_tokens on your completions? No, they &ldquo;didn&rsquo;t want to cut off the output.&rdquo;</li>
</ol>
<p>This isn&rsquo;t a cost optimization problem. This is a &ldquo;nobody thought about it for five minutes&rdquo; problem.</p>
<h2 id="the-finops-for-ai-grift">The FinOps-for-AI grift</h2>
<p>I&rsquo;ve already seen three startups pitching &ldquo;FinOps for AI&rdquo; &ndash; dashboards, alerts, recommendations, the whole cloud cost management playbook repackaged for LLM spend. I&rsquo;ve strong feelings about this.</p>
<p>Your LLM costs aren&rsquo;t a monitoring problem. They&rsquo;re an architecture problem. You don&rsquo;t need a dashboard to tell you that sending 4,000-token prompts to GPT-4 for a classification task that GPT-3.5-turbo handles fine is wasteful. You need an engineer to spend an afternoon thinking about it.</p>
<p>I spent years in the telecom space. The cloud cost management industry exists because organizations got too big and too decoupled for anyone to own the bill. A 12-person startup doesn&rsquo;t have that problem. You have one API key. Look at the usage page. It&rsquo;s right there.</p>
<h2 id="the-stuff-that-actually-moves-the-needle">The stuff that actually moves the needle</h2>
<p>Here&rsquo;s what I tell every team that asks me about LLM costs. It takes about two days to implement all of it, and it usually cuts the bill by 40-70%.</p>
<p><strong>Route by task complexity.</strong> This is the single biggest lever. Most LLM workloads are a mix of simple tasks (classification, extraction, formatting) and hard tasks (reasoning, creative generation, complex analysis). GPT-3.5-turbo handles the simple stuff at 1/20th the cost. Build a router. It can be as dumb as a switch statement on the task type. Doesn&rsquo;t need to be fancy.</p>
<p><strong>Cache deterministic requests.</strong> If the same input produces the same acceptable output, cache it. At a fintech company I worked with previously, we had an endpoint that was calling the LLM to classify transaction types. The same transaction descriptions kept coming through. A Redis cache with a 24-hour TTL cut that endpoint&rsquo;s LLM calls by 60% in the first week.</p>
<p><strong>Trim your prompts.</strong> Prompts grow like code comments &ndash; they accrete. Someone adds &ldquo;also make sure to&hellip;&rdquo; and nobody removes the instruction that became redundant three iterations ago. I&rsquo;ve seen production prompts that were 2,000 tokens of instructions for a task that needed 200. Audit them quarterly. Shorter prompts are cheaper and usually produce better output.</p>
<p><strong>Set max_tokens. Always.</strong> If you expect a 50-word response, don&rsquo;t let the model ramble for 500 tokens. This seems obvious. It&rsquo;s apparently not obvious, because I keep seeing it.</p>
<p><strong>Batch where possible.</strong> If you&rsquo;re processing 100 items, don&rsquo;t make 100 API calls. Combine them: &ldquo;Classify each of the following items&hellip;&rdquo; One call, one response. Works great for classification, extraction, and summarization. Doesn&rsquo;t work for complex reasoning tasks where items are independent.</p>
<h2 id="the-cost-model-is-simple">The cost model is simple</h2>
<p>Your monthly LLM bill is roughly:</p>
<p><code>requests * avg_tokens_per_request * price_per_token</code></p>
<p>That&rsquo;s three levers. Reduce any of them and the bill drops. The reason people&rsquo;s bills surprise them is they don&rsquo;t track any of these per feature. They see one big number at the end of the month and panic.</p>
<p>Track cost per feature. Not per API key, not per team. Per feature. &ldquo;The document summarizer costs $X per day. The classification endpoint costs $Y per day.&rdquo; Now you can have an actual conversation about whether the value justifies the cost.</p>
<h2 id="what-actually-annoys-me">What actually annoys me</h2>
<p>The thing that gets under my skin is people treating LLM costs as this novel, complex problem that requires new tools and new thinking. It doesn&rsquo;t. It&rsquo;s the same cost engineering we&rsquo;ve done forever. You measure, you identify waste, you optimize the hot paths, and you set budgets.</p>
<p>The only new wrinkle is that LLM costs scale with input/output volume in a way that&rsquo;s more linear and more visible than traditional compute costs. That actually makes it <em>easier</em> to optimize, not harder. Every request has a clear cost. You don&rsquo;t need to guess about amortized instance hours or reserved capacity pricing.</p>
<p>If your LLM bill is out of control, you don&rsquo;t need a &ldquo;FinOps for AI&rdquo; platform. You need to spend a day implementing caching and model routing. Then spend 30 minutes a month reviewing the usage dashboard that OpenAI already gives you for free.</p>
<p>This isn&rsquo;t rocket science. It&rsquo;s engineering discipline. The same discipline that keeps your AWS bill sane keeps your LLM bill sane. We just collectively forgot it because the technology is new and shiny.</p>
]]></content:encoded></item><item><title>Building Semantic Search in Go: From Embeddings to Production</title><link>https://lawzava.com/blog/2023-06-26-semantic-search-implementation/</link><pubDate>Mon, 26 Jun 2023 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2023-06-26-semantic-search-implementation/</guid><description>A hands-on walkthrough of building semantic search with Go, OpenAI embeddings, and pgvector &amp;amp;ndash; chunking, hybrid retrieval, and the gotchas I hit.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Semantic search isn&rsquo;t hard to build. It&rsquo;s hard to build <em>well</em>. The difference is in chunking, hybrid retrieval, and having an eval set before you ship &ndash; not in which vector database you picked.</p>
<p>I built a semantic search system last month for a documentation corpus. About 15,000 pages of technical docs, financial API references, and internal knowledge base articles. The old keyword search was painful &ndash; users searching for &ldquo;how to reverse a payment&rdquo; would get zero results because the docs called it &ldquo;transaction reversal&rdquo; or &ldquo;credit adjustment.&rdquo; Classic vocabulary mismatch.</p>
<p>Semantic search fixes this. Here&rsquo;s how I built it, what worked, and what I&rsquo;d do differently.</p>
<h2 id="the-architecture">The architecture</h2>
<p>Nothing exotic. The pipeline looks like this:</p>
<pre tabindex="0"><code>Documents -&gt; chunk -&gt; embed (OpenAI) -&gt; store (pgvector)
Query -&gt; embed -&gt; vector search + keyword search -&gt; merge &amp; rank -&gt; return
</code></pre><p>I chose pgvector over a  <a href="/blog/2023-04-03-vector-databases-explained/"
   
   >dedicated vector database</a>
 because the project already used PostgreSQL. One fewer service to operate. The performance has been fine for our scale &ndash; sub-50ms queries at 500K vectors. If you&rsquo;re at millions of vectors with hard latency requirements, Pinecone or Weaviate might make more sense. But don&rsquo;t start there.</p>
<h2 id="chunking-where-most-people-get-it-wrong">Chunking: where most people get it wrong</h2>
<p>Chunking strategy has more impact on search quality than model choice. I learned this the hard way at the fintech startup years ago when we were building news search &ndash; the same principle applies to embeddings.</p>
<p>The naive approach is fixed-size chunks (500 tokens, overlap 50). It&rsquo;s easy to implement and mediocre at everything. The problem: a chunk that starts mid-paragraph and ends mid-sentence creates an embedding that represents&hellip; nothing coherent.</p>
<p>What worked better:</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">chunkDocument</span>(<span style="color:#a6e22e">doc</span> <span style="color:#a6e22e">Document</span>) []<span style="color:#a6e22e">Chunk</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">sections</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">splitByHeadings</span>(<span style="color:#a6e22e">doc</span>.<span style="color:#a6e22e">Content</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">chunks</span> []<span style="color:#a6e22e">Chunk</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">section</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">sections</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">tokenCount</span>(<span style="color:#a6e22e">section</span>.<span style="color:#a6e22e">Text</span>) <span style="color:#f92672">&lt;=</span> <span style="color:#a6e22e">maxChunkTokens</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">chunks</span> = append(<span style="color:#a6e22e">chunks</span>, <span style="color:#a6e22e">Chunk</span>{
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">Text</span>:     <span style="color:#a6e22e">section</span>.<span style="color:#a6e22e">Text</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">Title</span>:    <span style="color:#a6e22e">section</span>.<span style="color:#a6e22e">Heading</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">Source</span>:   <span style="color:#a6e22e">doc</span>.<span style="color:#a6e22e">URL</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">Section</span>:  <span style="color:#a6e22e">section</span>.<span style="color:#a6e22e">Heading</span>,
</span></span><span style="display:flex;"><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></span><span style="display:flex;"><span>        <span style="color:#75715e">// Split long sections by paragraph, keeping heading as context</span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">paragraphs</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">splitByParagraph</span>(<span style="color:#a6e22e">section</span>.<span style="color:#a6e22e">Text</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">para</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">paragraphs</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">tokenCount</span>(<span style="color:#a6e22e">para</span>) &lt; <span style="color:#a6e22e">minChunkTokens</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">continue</span> <span style="color:#75715e">// Skip tiny fragments</span>
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">chunks</span> = append(<span style="color:#a6e22e">chunks</span>, <span style="color:#a6e22e">Chunk</span>{
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">Text</span>:     <span style="color:#a6e22e">section</span>.<span style="color:#a6e22e">Heading</span> <span style="color:#f92672">+</span> <span style="color:#e6db74">&#34;\n\n&#34;</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">para</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">Title</span>:    <span style="color:#a6e22e">section</span>.<span style="color:#a6e22e">Heading</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">Source</span>:   <span style="color:#a6e22e">doc</span>.<span style="color:#a6e22e">URL</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">Section</span>:  <span style="color:#a6e22e">section</span>.<span style="color:#a6e22e">Heading</span>,
</span></span><span style="display:flex;"><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">chunks</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Key decisions here:</p>
<ul>
<li><strong>Split on document structure first</strong> (headings), then on paragraphs. Never mid-sentence.</li>
<li><strong>Prepend the section heading</strong> to each chunk. This gives the embedding model context about what the paragraph is about. A paragraph saying &ldquo;To do this, call the <code>/refund</code> endpoint&rdquo; makes much more sense when preceded by &ldquo;Processing Refunds.&rdquo;</li>
<li><strong>Skip tiny chunks.</strong> Fragments under ~50 tokens produce noisy embeddings. Either merge them with adjacent content or drop them.</li>
<li><strong>Store metadata.</strong> Source URL, section heading, document title. You&rsquo;ll need all of these for display, filtering, and debugging.</li>
</ul>
<p>I tested three chunk sizes: 200, 400, and 800 tokens. 400 hit the sweet spot for our content. Smaller chunks had better precision but worse context. Larger chunks had more context but diluted the signal. Test this with your own content &ndash; the right size depends on document structure.</p>
<h2 id="embedding-and-indexing">Embedding and indexing</h2>
<p>OpenAI&rsquo;s <code>text-embedding-ada-002</code> for now. It&rsquo;s cheap ($0.0001/1K tokens), 1536 dimensions, and good enough for English technical content. I batch embed during ingestion:</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">embedChunks</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">client</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">openai</span>.<span style="color:#a6e22e">Client</span>, <span style="color:#a6e22e">chunks</span> []<span style="color:#a6e22e">Chunk</span>) <span style="color:#66d9ef">error</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">batchSize</span> = <span style="color:#ae81ff">100</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">i</span> &lt; len(<span style="color:#a6e22e">chunks</span>); <span style="color:#a6e22e">i</span> <span style="color:#f92672">+=</span> <span style="color:#a6e22e">batchSize</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">end</span> <span style="color:#f92672">:=</span> min(<span style="color:#a6e22e">i</span><span style="color:#f92672">+</span><span style="color:#a6e22e">batchSize</span>, len(<span style="color:#a6e22e">chunks</span>))
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">batch</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">chunks</span>[<span style="color:#a6e22e">i</span>:<span style="color:#a6e22e">end</span>]
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">texts</span> <span style="color:#f92672">:=</span> make([]<span style="color:#66d9ef">string</span>, len(<span style="color:#a6e22e">batch</span>))
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">j</span>, <span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">batch</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">texts</span>[<span style="color:#a6e22e">j</span>] = <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Text</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">client</span>.<span style="color:#a6e22e">CreateEmbeddings</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">openai</span>.<span style="color:#a6e22e">EmbeddingRequest</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Model</span>: <span style="color:#a6e22e">openai</span>.<span style="color:#a6e22e">AdaEmbeddingV2</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Input</span>: <span style="color:#a6e22e">texts</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;embedding batch %d: %w&#34;</span>, <span style="color:#a6e22e">i</span><span style="color:#f92672">/</span><span style="color:#a6e22e">batchSize</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">j</span>, <span style="color:#a6e22e">embedding</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">resp</span>.<span style="color:#a6e22e">Data</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">batch</span>[<span style="color:#a6e22e">j</span>].<span style="color:#a6e22e">Vector</span> = <span style="color:#a6e22e">embedding</span>.<span style="color:#a6e22e">Embedding</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">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">storeChunks</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">batch</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;storing batch %d: %w&#34;</span>, <span style="color:#a6e22e">i</span><span style="color:#f92672">/</span><span style="color:#a6e22e">batchSize</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>The pgvector schema:</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:#66d9ef">CREATE</span> EXTENSION <span style="color:#66d9ef">IF</span> <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">EXISTS</span> vector;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> chunks (
</span></span><span style="display:flex;"><span>    id          BIGSERIAL <span style="color:#66d9ef">PRIMARY</span> <span style="color:#66d9ef">KEY</span>,
</span></span><span style="display:flex;"><span>    doc_url     TEXT <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
</span></span><span style="display:flex;"><span>    section     TEXT <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
</span></span><span style="display:flex;"><span>    content     TEXT <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
</span></span><span style="display:flex;"><span>    embedding   vector(<span style="color:#ae81ff">1536</span>) <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
</span></span><span style="display:flex;"><span>    created_at  TIMESTAMPTZ <span style="color:#66d9ef">DEFAULT</span> now()
</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">CREATE</span> <span style="color:#66d9ef">INDEX</span> <span style="color:#66d9ef">ON</span> chunks <span style="color:#66d9ef">USING</span> ivfflat (embedding vector_cosine_ops) <span style="color:#66d9ef">WITH</span> (lists <span style="color:#f92672">=</span> <span style="color:#ae81ff">100</span>);
</span></span></code></pre></div><p>IVFFlat index with 100 lists works well for our dataset size. For larger collections, HNSW gives better recall at the cost of more memory. The pgvector docs have good guidance on when to switch.</p>
<h2 id="hybrid-retrieval-the-part-most-tutorials-skip">Hybrid retrieval: the part most tutorials skip</h2>
<p>Pure vector search has a problem. It&rsquo;s great at &ldquo;how do I reverse a payment&rdquo; -&gt; &ldquo;transaction reversal.&rdquo; It&rsquo;s terrible at &ldquo;error code FIN-4032&rdquo; -&gt; the page about that specific error code. Exact matches get lost in the embedding space because semantically similar concepts crowd them out.</p>
<p>The fix is  <a href="/blog/2023-04-17-rag-architecture-patterns/"
   
   >hybrid retrieval</a>
: combine vector similarity with keyword matching.</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">hybridSearch</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">db</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">pgxpool</span>.<span style="color:#a6e22e">Pool</span>, <span style="color:#a6e22e">query</span> <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">limit</span> <span style="color:#66d9ef">int</span>) ([]<span style="color:#a6e22e">Result</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">queryVec</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">embed</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">query</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">rows</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">Query</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#e6db74">`
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        WITH vector_results AS (
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            SELECT id, content, doc_url, section,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                   1 - (embedding &lt;=&gt; $1::vector) AS vector_score
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            FROM chunks
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            ORDER BY embedding &lt;=&gt; $1::vector
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            LIMIT $2 * 3
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        ),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        keyword_results AS (
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            SELECT id, content, doc_url, section,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                   ts_rank(to_tsvector(&#39;english&#39;, content),
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                           plainto_tsquery(&#39;english&#39;, $3)) AS keyword_score
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            FROM chunks
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            WHERE to_tsvector(&#39;english&#39;, content) @@ plainto_tsquery(&#39;english&#39;, $3)
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">            LIMIT $2 * 3
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        )
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        SELECT COALESCE(v.id, k.id) AS id,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">               COALESCE(v.content, k.content) AS content,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">               COALESCE(v.doc_url, k.doc_url) AS doc_url,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">               COALESCE(v.section, k.section) AS section,
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">               COALESCE(v.vector_score, 0) * 0.7
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">                 + COALESCE(k.keyword_score, 0) * 0.3 AS combined_score
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        FROM vector_results v
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        FULL OUTER JOIN keyword_results k ON v.id = k.id
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        ORDER BY combined_score DESC
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">        LIMIT $2
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">    `</span>, <span style="color:#a6e22e">queryVec</span>, <span style="color:#a6e22e">limit</span>, <span style="color:#a6e22e">query</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 style="color:#66d9ef">defer</span> <span style="color:#a6e22e">rows</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">return</span> <span style="color:#a6e22e">scanResults</span>(<span style="color:#a6e22e">rows</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The 0.7/0.3 weighting between vector and keyword scores is a starting point. I tuned it against our eval set and landed on 0.65/0.35 for our specific content. The important thing is having both signals. Pure vector search gave us 72% precision@5. Hybrid pushed it to 89%.</p>
<h2 id="evaluation-do-this-first-not-last">Evaluation: do this first, not last</h2>
<p>I know I&rsquo;m putting this section after the implementation code. Don&rsquo;t build it in this order. Build the eval set first.</p>
<p>Our eval set is 150 query-document pairs. I pulled 100 from actual search logs (what people searched for and which doc they ended up reading) and wrote 50 more to cover edge cases: typos, abbreviations, multi-language queries, and questions that should return nothing.</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">EvalCase</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Query</span>       <span style="color:#66d9ef">string</span>   <span style="color:#e6db74">`json:&#34;query&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">RelevantIDs</span> []<span style="color:#66d9ef">string</span> <span style="color:#e6db74">`json:&#34;relevant_ids&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Notes</span>       <span style="color:#66d9ef">string</span>   <span style="color:#e6db74">`json:&#34;notes&#34;`</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">runEval</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">cases</span> []<span style="color:#a6e22e">EvalCase</span>) <span style="color:#a6e22e">EvalReport</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">report</span> <span style="color:#a6e22e">EvalReport</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">c</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">cases</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">results</span>, <span style="color:#a6e22e">_</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">hybridSearch</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">db</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Query</span>, <span style="color:#ae81ff">5</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">resultIDs</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">extractIDs</span>(<span style="color:#a6e22e">results</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">report</span>.<span style="color:#a6e22e">Cases</span> = append(<span style="color:#a6e22e">report</span>.<span style="color:#a6e22e">Cases</span>, <span style="color:#a6e22e">CaseResult</span>{
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Query</span>:      <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">Query</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Precision</span>:  <span style="color:#a6e22e">precision</span>(<span style="color:#a6e22e">resultIDs</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">RelevantIDs</span>),
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">Recall</span>:     <span style="color:#a6e22e">recall</span>(<span style="color:#a6e22e">resultIDs</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">RelevantIDs</span>),
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">MRR</span>:        <span style="color:#a6e22e">meanReciprocalRank</span>(<span style="color:#a6e22e">resultIDs</span>, <span style="color:#a6e22e">c</span>.<span style="color:#a6e22e">RelevantIDs</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">report</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Run this on every change: different chunk sizes, embedding models, ranking weights, filter logic. Without it you&rsquo;re optimizing by vibes.</p>
<h2 id="what-id-do-differently">What I&rsquo;d do differently</h2>
<p><strong>Start with hybrid from day one.</strong> I spent a week on pure vector search, hit the exact-match problem, and had to retrofit keyword scoring. Should have started hybrid.</p>
<p><strong>Invest in chunk quality earlier.</strong> My first pass used fixed-size chunks and the results were mediocre. I spent more time debugging &ldquo;why did this irrelevant result show up&rdquo; than I would have spent writing a proper chunker.</p>
<p><strong>Cache embeddings for common queries.</strong> We get a lot of repeat queries. Caching the query embedding saves an API call and ~200ms per request. Obvious in retrospect.</p>
<p><strong>Don&rsquo;t over-index on the vector database choice.</strong> I spent three days evaluating Pinecone vs. Weaviate vs. pgvector. Should have spent three hours. At our scale, they all work. Pick the one that fits your existing stack and move on.</p>
<h2 id="what-matters">What matters</h2>
<p>Semantic search is production-ready infrastructure now. The hard parts aren&rsquo;t the vector math &ndash; they&rsquo;re the same boring engineering problems as always: data quality (chunking), evaluation (do you actually measure relevance?), and hybrid approaches (because no single signal is enough).</p>
<p>Build the eval set first. Chunk on document structure. Use hybrid retrieval. Everything else is tuning.</p>
]]></content:encoded></item><item><title>AI Code Review: What It Actually Catches (And What It Misses)</title><link>https://lawzava.com/blog/2023-05-29-ai-code-review/</link><pubDate>Mon, 29 May 2023 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2023-05-29-ai-code-review/</guid><description>After three months of using AI-assisted code review across multiple projects, here&amp;amp;rsquo;s what actually works and what&amp;amp;rsquo;s just noise.</description><content:encoded><![CDATA[<p>We started running AI-assisted code review on PRs about three months ago: first on one project, then on a few internal Go services. I was skeptical going in &ndash; my national cyber-defense background gave me a healthy distrust of automated security tools that promise more than they deliver. But I wanted to give it an honest shot.</p>
<p>Here&rsquo;s where I landed: it&rsquo;s useful. It&rsquo;s just not what the marketing says it is.</p>
<h2 id="the-stuff-its-genuinely-good-at">The stuff it&rsquo;s genuinely good at</h2>
<p>Pattern matching. That&rsquo;s the core strength, and it&rsquo;s not nothing. Across our Go codebases, the AI reviewer consistently catches:</p>
<ul>
<li>Unchecked errors. Go makes this easy to miss, and the AI never gets tired of pointing it out. Worth it for this alone, honestly.</li>
<li>Resource leaks. Deferred closes that should happen but don&rsquo;t. Missing context cancellation.</li>
<li>Naming inconsistencies. It remembers the conventions better than most humans on the team.</li>
<li>Import ordering. Boring but useful. It catches what <code>goimports</code> misses when people configure their editors differently.</li>
</ul>
<p>It&rsquo;s basically a very thorough linter that can read English comments. For the mechanical stuff, it saves real time. The junior devs on one of my teams told me it cut their &ldquo;stupid mistake&rdquo; PR cycles in half. I believe them.</p>
<h2 id="the-stuff-it-confidently-gets-wrong">The stuff it confidently gets wrong</h2>
<p>Here&rsquo;s where it gets interesting: the AI has no idea <em>why</em> code exists. It can tell you code has a race condition, but it can&rsquo;t tell you that the race condition is a known trade-off the team accepted because the alternative was a 3x latency hit.</p>
<p>Real examples from the last month:</p>
<ul>
<li>It flagged a &ldquo;redundant&rdquo; nil check that was actually guarding against a known upstream bug we hadn&rsquo;t fixed yet. Removing it would have caused a production incident.</li>
<li>It suggested refactoring a function that was intentionally verbose because three different teams needed to understand it during an incident.</li>
<li>It recommended moving to a newer API version that had a subtle breaking change in our edge case. The model had no idea about our specific integration constraints.</li>
</ul>
<p>The pattern is consistent: AI reviews the diff. Humans review the context. These are different jobs.</p>
<h2 id="how-we-actually-use-it">How we actually use it</h2>
<p>Two-pass review. Non-negotiable.</p>
<ol>
<li>AI runs when the PR opens. Posts a summary and flags. All non-blocking.</li>
<li>Human reviewer looks at the PR with the AI comments as background context. They can dismiss, agree, or dig deeper.</li>
</ol>
<p>The critical rule: AI comments are suggestions, not approvals. A clean AI report means nothing about whether the change is safe to merge. I&rsquo;ve seen clean AI reports on PRs that would have taken down a production service.</p>
<p>We also explicitly exclude certain paths from AI review: auth code, cryptographic operations, permission logic. The cost of a confident-but-wrong suggestion in those areas is too high. A developer reads &ldquo;looks good&rdquo; from the AI and their guard drops. That&rsquo;s the real danger.</p>
<h2 id="noise-is-the-killer">Noise is the killer</h2>
<p>The single biggest problem isn&rsquo;t accuracy. It&rsquo;s noise. If the tool posts 15 comments and 12 are trivial style nitpicks, developers stop reading the other 3. I&rsquo;ve seen this happen on every team that adopts these tools without tuning.</p>
<p>Our fix: aggressive filtering. We configured the tool to only surface medium-and-above severity issues. Style enforcement stays in the linter where it belongs. The AI reviewer gets to have opinions about logic, error handling, and security patterns. That&rsquo;s it.</p>
<p>This reduced comment volume by about 70% and increased the rate at which developers actually engaged with the remaining comments. Less is more. The boring lesson, again.</p>
<h2 id="the-honest-assessment">The honest assessment</h2>
<p>AI code review saves maybe 15-20 minutes per PR cycle on mechanical issues. It doesn&rsquo;t save any time on the hard reviews &ndash; the ones involving architecture decisions, performance trade-offs, or cross-service implications. Those still take the same amount of time, and they should.</p>
<p>If you&rsquo;re considering adding AI review to your workflow, go in with clear expectations. It&rsquo;s a tireless pattern matcher. It&rsquo;s not a senior engineer. Configure it tight, keep it non-blocking, and don&rsquo;t let anyone treat a clean AI report as a substitute for thinking.</p>
<p>The best code review tool is still someone who understands the system, the users, and the trade-offs. AI just helps them focus on the parts that matter.</p>
]]></content:encoded></item><item><title>AI Safety Is Just Security Engineering With Extra Steps</title><link>https://lawzava.com/blog/2023-03-20-ai-safety-for-engineers/</link><pubDate>Mon, 20 Mar 2023 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2023-03-20-ai-safety-for-engineers/</guid><description>AI safety is not a philosophy problem for engineers. It is reliability, security, and accountability applied to a new kind of system.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Treat AI safety like you treat security: define what must never happen, layer your defenses, assume the system will be attacked, and keep an audit trail. The threat model is different but the discipline is the same.</p>
<p>I spent time working in national cyber defense before moving into startups. That background shapes how I think about AI safety, which is probably why I find most of the current discourse frustrating. The AI safety conversation is dominated by philosophers debating existential risk and executives writing policy memos nobody reads. Meanwhile, engineers are shipping AI features into production with no input validation, no output filtering, and no fallback plan.</p>
<p>AI safety isn&rsquo;t an ethics seminar. It&rsquo;s a security engineering problem. And we already know how to do security engineering. We just need to apply it.</p>
<h2 id="safety-is-an-engineering-property">Safety Is an Engineering Property</h2>
<p>In security, we don&rsquo;t hope the system behaves correctly. We define what must never happen, build controls to prevent it, and monitor for violations. AI safety should work the same way.</p>
<p>Three properties matter:</p>
<p><strong>Reliability.</strong> The system should avoid confident errors and make uncertainty visible. A model that says &ldquo;I don&rsquo;t know&rdquo; is safer than one that confidently fabricates an answer. Design for that.</p>
<p><strong>Security.</strong> Prompts, tools, and outputs are untrusted surfaces. Full stop. Every principle you apply to user input in a web application applies here. Input validation, output sanitization, least-privilege access.</p>
<p><strong>Accountability.</strong> Keep decisions traceable. Log what went in, what came out, and why the system chose what it chose. When something goes wrong &ndash; and it will &ndash; you need to reconstruct the chain.</p>
<h2 id="the-threat-model">The Threat Model</h2>
<p>From my security background, the threats break down into four categories. None of them are hypothetical.</p>
<h3 id="confidently-wrong-answers">Confidently Wrong Answers</h3>
<p>Language models produce fluent, authoritative-sounding text even when they&rsquo;re completely wrong. This is the AI equivalent of a phishing email that looks legitimate. The fluency is the attack vector. Users trust confident text, and the model is always confident.</p>
<p>Defense: ground outputs in retrieved sources, prefer narrow tasks, and add fallback paths when the system can&rsquo;t verify its own answer.</p>
<h3 id="prompt-injection">Prompt Injection</h3>
<p>This is the big one, and I&rsquo;m surprised how few teams take it seriously. User inputs can contain hostile instructions that override your system prompt, extract sensitive data, or manipulate the model into doing things you didn&rsquo;t intend.</p>
<p>This is SQL injection for the AI era. We spent twenty years learning to never trust user input in database queries. Now we&rsquo;re concatenating user text directly into model prompts and hoping for the best.</p>
<p>Defense: separate user content from system instructions. Validate outputs before they reach users. Limit tool access. Assume every user input is adversarial, because eventually one will be.</p>
<h3 id="data-leakage">Data Leakage</h3>
<p>AI features touch everything: logs, documents, user text, internal wikis. Every piece of data you send to a model is data that could leak &ndash; through the model&rsquo;s responses, through provider logs, through training data in the next version.</p>
<p>Defense: minimize what you send. Redact sensitive fields. Never feed private content back into prompts where other users might see the output. Apply the same data classification you use for any other third-party service.</p>
<h3 id="bias-and-uneven-performance">Bias and Uneven Performance</h3>
<p>Models perform differently across languages, demographics, and domains. This isn&rsquo;t theoretical &ndash; I&rsquo;ve seen it in production. A summarization feature that works well in English and falls apart in other languages. A classification model that performs inconsistently across user groups.</p>
<p>Defense: test with diverse inputs. Don&rsquo;t use AI for high-stakes decisions without human review. Monitor performance across segments, not just in aggregate.</p>
<h2 id="defense-in-depth">Defense in Depth</h2>
<p>No single control solves this. Layer your defenses:</p>
<ul>
<li>Ground generation in trusted, retrieved data</li>
<li>Filter and validate both inputs and outputs</li>
<li>Apply least-privilege to tool and data access</li>
<li>Build refusal paths for requests that exceed the system&rsquo;s safe operating range</li>
<li>Add human review for high-stakes decisions</li>
</ul>
<p>This is exactly how we design secure systems. The specifics are different but the pattern is identical.</p>
<h2 id="testing-like-you-mean-it">Testing Like You Mean It</h2>
<p>Safety is a lifecycle, not a checklist you run once before launch. Build evaluation sets that include adversarial prompts, confusing edge cases, and realistic user flows. Run them continuously. Monitor production for error spikes, unusual output patterns, and refusal rate changes.</p>
<p>At a financial infrastructure company, where we deal with financial data, I apply the same principle to every system: if you can&rsquo;t detect when it fails, you can&rsquo;t call it safe. AI features are no different.</p>
<h2 id="start-here">Start Here</h2>
<p>Four steps that every team should take before shipping an AI feature:</p>
<ol>
<li><strong>Define what the system must never do.</strong> Write it down. Make it specific. &ldquo;Must not leak PII&rdquo; is better than &ldquo;must be safe.&rdquo;</li>
<li><strong>Limit scope to a narrow task.</strong> The broader the capability, the larger the attack surface.</li>
<li><strong>Add guardrails before shipping, not after.</strong> Input validation, output filtering, fallback paths. All of it. Before the first user touches it.</li>
<li><strong>Measure failures and iterate.</strong> Track what went wrong, fix the defense, repeat.</li>
</ol>
<p>This isn&rsquo;t glamorous work. It&rsquo;s the same kind of boring, essential engineering that keeps every other system from falling apart. The model is new. The discipline isn&rsquo;t.</p>
]]></content:encoded></item><item><title>LLM Integration Patterns That Actually Survive Production</title><link>https://lawzava.com/blog/2023-01-23-llm-integration-patterns/</link><pubDate>Mon, 23 Jan 2023 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2023-01-23-llm-integration-patterns/</guid><description>Practical patterns for integrating LLMs into real applications &amp;amp;ndash; prompt management, structured outputs, caching, fallbacks, and tool use &amp;amp;ndash; with Go examples.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>LLMs aren&rsquo;t APIs. Same input, different output. Seconds of latency instead of milliseconds. Costs that scale with how much you talk. Treat them like the weird, expensive, unreliable-but-powerful dependency they are, and design accordingly.</p>
<p>I&rsquo;ve been integrating LLMs into production systems since late 2022, and the one thing I keep telling teams is this: the model isn&rsquo;t the hard part. The integration is.</p>
<p>An LLM call looks like an API call. It isn&rsquo;t. It&rsquo;s probabilistic, slow, expensive, and context-limited. If you design your system assuming deterministic behavior, you&rsquo;ll have a bad time. If you design for variability from the start, everything gets easier.</p>
<p>Here are the patterns I keep reaching for.</p>
<h2 id="the-constraints-you-cant-ignore">The Constraints You Can&rsquo;t Ignore</h2>
<p>Before patterns, constraints. These shape every decision:</p>
<p><strong>Non-determinism.</strong> Same prompt, different response. Small wording changes shift behavior. This is a feature of the technology, not a bug. But it means you need validation layers that traditional API integrations don&rsquo;t.</p>
<p><strong>Latency.</strong> We&rsquo;re talking seconds, not milliseconds. Your UX needs to account for streaming, progress indicators, and the possibility that a request simply takes too long.</p>
<p><strong>Cost.</strong> Tokens in, tokens out, money gone. Long prompts and verbose outputs are the biggest cost drivers. I&rsquo;ve seen teams 10x their bill by not paying attention to prompt length.</p>
<p><strong>Context limits.</strong> Everything &ndash; instructions, data, output &ndash; competes for the same window. Summarization and retrieval aren&rsquo;t nice-to-haves. They&rsquo;re architectural requirements.</p>
<p><strong>Hallucinations.</strong> The model will confidently make things up. Without grounding or validation, you&rsquo;ll ship lies to users.</p>
<h2 id="pattern-1-prompt-templates-as-code">Pattern 1: Prompt Templates as Code</h2>
<p>Prompts aren&rsquo;t strings you tweak in a UI. They&rsquo;re code. Version them. Test them. Review them.</p>
<p>Here is what a basic prompt template looks like in Go:</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">PromptTemplate</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Name</span>       <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Version</span>    <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">SystemMsg</span>  <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">UserMsgFmt</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">pt</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">PromptTemplate</span>) <span style="color:#a6e22e">Render</span>(<span style="color:#a6e22e">vars</span> <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#66d9ef">string</span>) (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">string</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">userMsg</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">pt</span>.<span style="color:#a6e22e">UserMsgFmt</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">k</span>, <span style="color:#a6e22e">v</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">vars</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">userMsg</span> = <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">ReplaceAll</span>(<span style="color:#a6e22e">userMsg</span>, <span style="color:#e6db74">&#34;{{&#34;</span><span style="color:#f92672">+</span><span style="color:#a6e22e">k</span><span style="color:#f92672">+</span><span style="color:#e6db74">&#34;}}&#34;</span>, <span style="color:#a6e22e">v</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">pt</span>.<span style="color:#a6e22e">SystemMsg</span>, <span style="color:#a6e22e">userMsg</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">var</span> <span style="color:#a6e22e">SummarizeTemplate</span> = <span style="color:#a6e22e">PromptTemplate</span>{
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Name</span>:       <span style="color:#e6db74">&#34;summarize-v2&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Version</span>:    <span style="color:#e6db74">&#34;2.1.0&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">SystemMsg</span>:  <span style="color:#e6db74">&#34;You are a concise summarizer. Output only the summary, no preamble.&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">UserMsgFmt</span>: <span style="color:#e6db74">&#34;Summarize this text in {{max_sentences}} sentences:\n\n{{text}}&#34;</span>,
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The version field matters. When model behavior shifts &ndash; and it will, with provider updates &ndash; you need to know which prompt version was running. Log it with every request. Thank me later.</p>
<p>I run lightweight tests against a small eval set: representative inputs, expected outputs, and a set of &ldquo;this must never appear&rdquo; strings. Not perfect, but it catches regressions before they reach users.</p>
<h2 id="pattern-2-structured-output-validation">Pattern 2: Structured Output Validation</h2>
<p>When the output feeds another system, you need it to be structured and correct. Request JSON, validate against a schema, retry on failure.</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">ExtractedEntity</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Name</span>       <span style="color:#66d9ef">string</span>  <span style="color:#e6db74">`json:&#34;name&#34; validate:&#34;required&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Category</span>   <span style="color:#66d9ef">string</span>  <span style="color:#e6db74">`json:&#34;category&#34; validate:&#34;required&#34;`</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Confidence</span> <span style="color:#66d9ef">float64</span> <span style="color:#e6db74">`json:&#34;confidence&#34; validate:&#34;gte=0,lte=1&#34;`</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">extractEntities</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">client</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">LLMClient</span>, <span style="color:#a6e22e">text</span> <span style="color:#66d9ef">string</span>) ([]<span style="color:#a6e22e">ExtractedEntity</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">prompt</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">`Extract entities from this text. Return valid JSON array.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Each object must have: name (string), category (string), confidence (0-1).
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Text: %s`</span>, <span style="color:#a6e22e">text</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">attempt</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">attempt</span> &lt; <span style="color:#ae81ff">3</span>; <span style="color:#a6e22e">attempt</span><span style="color:#f92672">++</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">client</span>.<span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">prompt</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;completion failed: %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">var</span> <span style="color:#a6e22e">entities</span> []<span style="color:#a6e22e">ExtractedEntity</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">json</span>.<span style="color:#a6e22e">Unmarshal</span>([]byte(<span style="color:#a6e22e">resp</span>), <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">entities</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">continue</span> <span style="color:#75715e">// retry on parse failure</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">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">validate</span>.<span style="color:#a6e22e">Struct</span>(<span style="color:#a6e22e">entities</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">continue</span> <span style="color:#75715e">// retry on validation failure</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">entities</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">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;failed to extract valid entities after 3 attempts&#34;</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Three retries is my default. If it can&rsquo;t produce valid JSON in three attempts, something is wrong with the prompt, not the retry count. I also add the schema to the prompt itself &ndash; the model needs to see what &ldquo;correct&rdquo; looks like.</p>
<h2 id="pattern-3-retrieval-grounding-rag">Pattern 3: Retrieval Grounding (RAG)</h2>
<p>This is the default pattern for anything knowledge-heavy. The model&rsquo;s training data is stale and generic. Your data is current and specific. Bridge the gap with retrieval.</p>
<p>The flow: index your documents, retrieve relevant chunks at query time, stuff them into the context, and instruct the model to answer only from what it sees.</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">RAGPipeline</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Retriever</span>  <span style="color:#a6e22e">DocumentRetriever</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">LLM</span>        <span style="color:#f92672">*</span><span style="color:#a6e22e">LLMClient</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">MaxChunks</span>  <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">MaxTokens</span>  <span style="color:#66d9ef">int</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">r</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">RAGPipeline</span>) <span style="color:#a6e22e">Answer</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">question</span> <span style="color:#66d9ef">string</span>) (<span style="color:#66d9ef">string</span>, []<span style="color:#a6e22e">Source</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">chunks</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">Retriever</span>.<span style="color:#a6e22e">Search</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">question</span>, <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">MaxChunks</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:#e6db74">&#34;&#34;</span>, <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;retrieval failed: %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">if</span> len(<span style="color:#a6e22e">chunks</span>) <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;I don&#39;t have enough information to answer that.&#34;</span>, <span style="color:#66d9ef">nil</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:#a6e22e">context</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">formatChunks</span>(<span style="color:#a6e22e">chunks</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">prompt</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Sprintf</span>(<span style="color:#e6db74">`Answer based ONLY on the provided context.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">If the context doesn&#39;t contain the answer, say so.
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Context:
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">%s
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">
</span></span></span><span style="display:flex;"><span><span style="color:#e6db74">Question: %s`</span>, <span style="color:#a6e22e">context</span>, <span style="color:#a6e22e">question</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">answer</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">r</span>.<span style="color:#a6e22e">LLM</span>.<span style="color:#a6e22e">Complete</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">prompt</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:#e6db74">&#34;&#34;</span>, <span style="color:#66d9ef">nil</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;generation failed: %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">sources</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">extractSources</span>(<span style="color:#a6e22e">chunks</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">answer</span>, <span style="color:#a6e22e">sources</span>, <span style="color:#66d9ef">nil</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Two things I always enforce: the &ldquo;answer only from context&rdquo; instruction, and returning sources alongside the answer. The first reduces hallucinations. The second makes them detectable.</p>
<h2 id="pattern-4-tool-use-with-guardrails">Pattern 4: Tool Use With Guardrails</h2>
<p>Tool-using models are powerful and dangerous. A model that can call your search API, query your database, or trigger workflows needs tight constraints.</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">ToolDefinition</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Name</span>        <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Description</span> <span style="color:#66d9ef">string</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Handler</span>     <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">args</span> <span style="color:#a6e22e">json</span>.<span style="color:#a6e22e">RawMessage</span>) (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">error</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">MaxCalls</span>    <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Timeout</span>     <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</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">type</span> <span style="color:#a6e22e">ToolOrchestrator</span> <span style="color:#66d9ef">struct</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Tools</span>      <span style="color:#66d9ef">map</span>[<span style="color:#66d9ef">string</span>]<span style="color:#a6e22e">ToolDefinition</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">MaxSteps</span>   <span style="color:#66d9ef">int</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">StepTimeout</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Duration</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">o</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">ToolOrchestrator</span>) <span style="color:#a6e22e">Execute</span>(<span style="color:#a6e22e">ctx</span> <span style="color:#a6e22e">context</span>.<span style="color:#a6e22e">Context</span>, <span style="color:#a6e22e">task</span> <span style="color:#66d9ef">string</span>) (<span style="color:#66d9ef">string</span>, <span style="color:#66d9ef">error</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">step</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">step</span> &lt; <span style="color:#a6e22e">o</span>.<span style="color:#a6e22e">MaxSteps</span>; <span style="color:#a6e22e">step</span><span style="color:#f92672">++</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">toolCall</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">o</span>.<span style="color:#a6e22e">decideNextAction</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">task</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 style="color:#f92672">||</span> <span style="color:#a6e22e">toolCall</span> <span style="color:#f92672">==</span> <span style="color:#66d9ef">nil</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">tool</span>, <span style="color:#a6e22e">ok</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">o</span>.<span style="color:#a6e22e">Tools</span>[<span style="color:#a6e22e">toolCall</span>.<span style="color:#a6e22e">Name</span>]
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> !<span style="color:#a6e22e">ok</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> <span style="color:#e6db74">&#34;&#34;</span>, <span style="color:#a6e22e">fmt</span>.<span style="color:#a6e22e">Errorf</span>(<span style="color:#e6db74">&#34;unknown tool: %s&#34;</span>, <span style="color:#a6e22e">toolCall</span>.<span style="color:#a6e22e">Name</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">stepCtx</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:#a6e22e">tool</span>.<span style="color:#a6e22e">Timeout</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">result</span>, <span style="color:#a6e22e">err</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">tool</span>.<span style="color:#a6e22e">Handler</span>(<span style="color:#a6e22e">stepCtx</span>, <span style="color:#a6e22e">toolCall</span>.<span style="color:#a6e22e">Args</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">cancel</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:#75715e">// Log and continue -- don&#39;t let one tool failure kill the chain</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;tool %s failed at step %d: %v&#34;</span>, <span style="color:#a6e22e">toolCall</span>.<span style="color:#a6e22e">Name</span>, <span style="color:#a6e22e">step</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></span><span style="display:flex;"><span>        <span style="color:#a6e22e">task</span> = <span style="color:#a6e22e">appendResult</span>(<span style="color:#a6e22e">task</span>, <span style="color:#a6e22e">toolCall</span>.<span style="color:#a6e22e">Name</span>, <span style="color:#a6e22e">result</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">o</span>.<span style="color:#a6e22e">generateFinalAnswer</span>(<span style="color:#a6e22e">ctx</span>, <span style="color:#a6e22e">task</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Key constraints: maximum steps, per-tool timeouts, and an explicit tool whitelist. I&rsquo;ve seen demos where the model calls 47 tools in a loop and racks up a $200 bill. Don&rsquo;t be that team. A smaller toolset with stronger constraints beats an open-ended agent every time.</p>
<h2 id="pattern-5-caching-and-fallbacks">Pattern 5: Caching and Fallbacks</h2>
<p>Cache everything you can. Many prompts are repetitive, especially in search and classification workloads. A warm cache cuts both cost and latency.</p>
<p>For failures, use a fallback chain:</p>
<ol>
<li>Try the primary model</li>
<li>Fall back to a cheaper, faster model</li>
<li>Fall back to a rule-based response</li>
<li>Return a safe default (&ldquo;I couldn&rsquo;t process that request&rdquo;)</li>
</ol>
<p>Reliability matters more than perfect answers. Users forgive &ldquo;I don&rsquo;t know.&rdquo; They don&rsquo;t forgive confidently wrong.</p>
<h2 id="operating-in-production">Operating in Production</h2>
<p>Once this is running, you need visibility. I track four signals:</p>
<ul>
<li><strong>Quality</strong> on a curated eval set, run weekly. Not vibes &ndash; actual measured accuracy.</li>
<li><strong>Latency percentiles</strong> (p50 and p95) for user-facing calls.</li>
<li><strong>Cost per request</strong>, broken down by prompt size so you can spot bloat.</li>
<li><strong>Safety exceptions</strong> &ndash; anything the output filter catches.</li>
</ul>
<p>Combine automated checks with periodic human review. This is a living system. You&rsquo;ll be tuning it weekly for the first few months.</p>
<h2 id="the-uncomfortable-truth">The Uncomfortable Truth</h2>
<p>LLM integration is a new discipline. It borrows from API design, data engineering, and observability, but it&rsquo;s its own thing. The teams shipping well are the ones who accepted that early and designed for the constraints instead of pretending they don&rsquo;t exist.</p>
<p>Build for variability. Ground in real data. Validate everything that matters. And keep your fallbacks warm.</p>
]]></content:encoded></item><item><title>AI in Production Is Just Engineering. Treat It That Way.</title><link>https://lawzava.com/blog/2023-01-09-ai-in-production/</link><pubDate>Mon, 09 Jan 2023 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2023-01-09-ai-in-production/</guid><description>ChatGPT changed expectations overnight, but shipping AI features that actually work is an engineering problem, not a model problem.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Everyone wants AI in their product now. The model is the easy part. The hard part is making a probabilistic system behave like reliable software. Ship the smallest useful thing, wrap it in guardrails, and instrument everything.</p>
<p>ChatGPT dropped in late 2022 and suddenly every product manager I know had &ldquo;add AI&rdquo; at the top of their backlog. Fair enough. The technology is genuinely impressive. But I&rsquo;ve been in enough rooms now &ndash; calls with telecom companies, internal discussions at a financial infrastructure company &ndash; to notice a pattern. Teams that treat AI as a product feature ship well. Teams that treat it as magic ship demos.</p>
<p>The gap isn&rsquo;t model quality. The gap is engineering discipline.</p>
<h2 id="the-demo-trap">The Demo Trap</h2>
<p>Here is what happens. Someone builds a demo over a weekend. It works beautifully with curated inputs. Leadership gets excited. &ldquo;Ship it.&rdquo; Then real users show up with messy prompts, edge cases, and the kind of creative abuse that no one anticipated.</p>
<p>I&rsquo;ve seen this movie before, just with different technology. Microservices had the same arc. Kubernetes had the same arc. The technology works. The problem is people skip the boring parts.</p>
<h2 id="what-production-actually-demands">What Production Actually Demands</h2>
<p>After watching several teams go through this in January 2023, the requirements are depressingly consistent:</p>
<p><strong>Reliability under partial failure.</strong> Your model provider will have outages. Your requests will time out. You need retries with backoff, circuit breakers, and a fallback that doesn&rsquo;t leave users staring at a spinner. Standard distributed systems stuff, but teams forget it applies here too.</p>
<p><strong>Quality gates that are explicit.</strong> If you expect JSON back from the model, validate it like an API contract. Reject malformed responses. This isn&rsquo;t optional. I&rsquo;ve watched teams debug production issues for hours because they trusted the model to always return valid structured data. It won&rsquo;t.</p>
<p><strong>Cost awareness from day one.</strong> Usage grows fast once the feature is visible. I mean really fast. Make the cost model visible to product owners early, because the conversation about &ldquo;we need to turn this off, the bill is insane&rdquo; isn&rsquo;t fun to have retroactively.</p>
<p><strong>Observability.</strong> If you aren&rsquo;t measuring latency distributions, error rates, and cost per request, you&rsquo;re flying blind. And you&rsquo;ll discover problems from user complaints instead of dashboards.</p>
<h2 id="the-architecture-that-survives">The Architecture That Survives</h2>
<p>The model call itself is rarely where teams struggle. The surrounding lifecycle is the hard part:</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>request -&gt; normalize -&gt; cache check -&gt; model call with timeout
</span></span><span style="display:flex;"><span>        -&gt; validate -&gt; accept or fallback -&gt; log and meter
</span></span></code></pre></div><p>Three patterns keep showing up in teams that ship successfully:</p>
<ol>
<li><strong>Separate sync from async.</strong> User-facing calls need streaming and tight timeouts. Background processing can be batched and retried. Don&rsquo;t mix them.</li>
<li><strong>Cache aggressively.</strong> Many inputs are repetitive. A warm cache cuts cost and latency dramatically.</li>
<li><strong>Degrade gracefully.</strong> When the model fails, return something useful. &ldquo;No result&rdquo; is better than a hallucinated answer that looks confident.</li>
</ol>
<h2 id="validation-isnt-optional">Validation Isn&rsquo;t Optional</h2>
<p>I want to stress this because I keep seeing it skipped. If a response might contain sensitive data, run detection and redaction before it hits users or logs. If the output feeds into another system, validate the schema. For anything high-stakes, add a human review step.</p>
<p>The model doesn&rsquo;t know what&rsquo;s sensitive. That&rsquo;s your job.</p>
<h2 id="prompts-are-code-version-them">Prompts Are Code. Version Them.</h2>
<p>Treat prompts, templates, and model settings as versioned assets. Roll out changes gradually. Measure the impact. Performance drifts with provider updates and data shifts, and if you aren&rsquo;t tracking versions, you won&rsquo;t be able to tell why quality changed last Tuesday.</p>
<p>Set up alerts on error rate, latency regressions, and usage spikes. Those are your early warning system.</p>
<h2 id="start-small-stay-honest">Start Small, Stay Honest</h2>
<p>Pick one narrow use case with clear success criteria and an obvious fallback. Instrument everything. Learn from real traffic. Expand only after the behavior is stable.</p>
<p>AI in production isn&rsquo;t magic. It&rsquo;s engineering. The teams that respect that reality are the ones actually shipping.</p>
]]></content:encoded></item><item><title>My Honest Take on GitHub Copilot After Six Months</title><link>https://lawzava.com/blog/2022-11-28-ai-code-assistants-evolution/</link><pubDate>Mon, 28 Nov 2022 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2022-11-28-ai-code-assistants-evolution/</guid><description>Six months with Copilot in real projects. What it actually helps with, where it quietly makes things worse, and why the productivity claims are overblown.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Copilot is genuinely useful for boilerplate and pattern completion. It&rsquo;s genuinely dangerous for anything involving business logic, security, or correctness. Net effect: I write first drafts faster and spend more time reviewing. Whether that&rsquo;s a win depends on the task.</p>
<hr>
<p>I&rsquo;ve been using GitHub Copilot daily for about six months now, across Go projects, Terraform modules, and some TypeScript. Enough time for the novelty to wear off and an honest assessment to form.</p>
<p>The short version: it&rsquo;s good at autocomplete on steroids. It&rsquo;s bad at thinking. And the gap between those two things is where bugs live.</p>
<h2 id="where-it-actually-helps">Where It Actually Helps</h2>
<p>Copilot shines on boring, repetitive code that follows well-established patterns. The stuff you know how to write but don&rsquo;t enjoy writing.</p>
<p><strong>Boilerplate.</strong> Struct definitions, HTTP handler scaffolding, test table setup in Go. Copilot generates these faster than I can type them, and the output is usually close enough that a quick edit gets it right. For Go specifically, it handles the <code>if err != nil</code> pattern well, which is both useful and slightly depressing.</p>
<p><strong>API recall.</strong> I don&rsquo;t have every AWS SDK method memorized. Copilot often suggests the right function signature, saving me a trip to the docs. Not always correct, but correct enough to jog my memory.</p>
<p><strong>Glue code.</strong> Converting between types, building request objects, wiring dependencies. The code that connects the interesting parts together. Copilot handles this well because it&rsquo;s highly contextual to the current file and follows obvious patterns.</p>
<p><strong>Comments to code.</strong> Writing a clear function signature or a descriptive comment and letting Copilot fill in the implementation works surprisingly well for straightforward functions. The key word is &ldquo;straightforward.&rdquo;</p>
<h2 id="where-it-quietly-makes-things-worse">Where It Quietly Makes Things Worse</h2>
<p>The problems aren&rsquo;t obvious. That&rsquo;s what makes them dangerous. Copilot doesn&rsquo;t crash or throw errors. It produces plausible-looking code that passes a casual review. The failure mode is subtle wrongness.</p>
<p><strong>Business logic.</strong> Copilot has no idea what your business rules are. It will happily generate a discount calculation that looks reasonable but uses the wrong formula because the actual logic lives in a requirements doc it has never seen. I caught one of these in a PR review &ndash; the code looked fine, compiled fine, and would have silently applied the wrong pricing.</p>
<p><strong>Error handling.</strong> In Go, Copilot handles the mechanical <code>if err != nil</code> return pattern, but it&rsquo;s bad at deciding <em>what to do</em> with errors. Should you retry? Log and continue? Wrap with context and propagate? Copilot picks whatever pattern it has seen most often, which is usually &ldquo;return err&rdquo; without any context wrapping. For a language that relies on error handling discipline, that&rsquo;s a real problem.</p>
<p><strong>Security.</strong> This is where I draw a hard line. I don&rsquo;t use Copilot for authentication flows, permission checks, input validation, or anything involving cryptography. The cost of a plausible-but-wrong suggestion in these areas is too high. With my national cyber-defense background, I&rsquo;m probably more paranoid about this than most, but I&rsquo;ve seen what &ldquo;looks correct&rdquo; security code can cost an organization.</p>
<p><strong>Naming and consistency.</strong> Copilot doesn&rsquo;t know your codebase&rsquo;s naming conventions. It will suggest <code>getUserData</code> in a codebase that uses <code>fetchUserInfo</code>. Small inconsistencies compound into a codebase that feels like it was written by a dozen different people. Which, with Copilot, it effectively was.</p>
<h2 id="the-review-problem">The Review Problem</h2>
<p>Here is what nobody talks about in the productivity benchmarks: Copilot shifts work from writing to reviewing.</p>
<p>I draft faster. But I also spend more time reading and verifying what was generated. For simple boilerplate, the net is positive. For anything with logic, the net is roughly break-even. For complex systems work, it can be negative because I&rsquo;ve to review generated code that I would have written differently and more carefully from scratch.</p>
<p>Code review at the team level changes too. Reviewers can&rsquo;t assume the author understands every line, because some lines were generated and accepted quickly. The review burden increases, and the conversations become more about &ldquo;did you actually verify this?&rdquo; than &ldquo;is the style correct?&rdquo;</p>
<p>That shift is healthy in one way &ndash; we should be reviewing logic and risk, not formatting. But it only works if teams explicitly acknowledge the change and adjust expectations.</p>
<h2 id="my-workflow">My Workflow</h2>
<p>After six months, I&rsquo;ve settled into a pattern:</p>
<ol>
<li>Write a clear function signature with good names and types. Copilot uses these as context, so better inputs produce better outputs.</li>
<li>Accept small suggestions, one or two lines at a time. Reject multi-line completions that I would need to audit carefully.</li>
<li>Edit aggressively for naming, error handling, and edge cases. Copilot gets the shape right and the details wrong.</li>
<li>Run tests immediately. If there are no tests for the code I just generated, write them before moving on. This is non-negotiable.</li>
</ol>
<p>I explicitly don&rsquo;t use Copilot for: auth code, permission checks, crypto, complex algorithms, or any code where I can&rsquo;t immediately verify correctness by reading it.</p>
<h2 id="for-teams-considering-adoption">For Teams Considering Adoption</h2>
<p>Treat Copilot as a new team member who&rsquo;s fast, eager, and doesn&rsquo;t understand your domain. You wouldn&rsquo;t merge their PRs without review. Same rules apply.</p>
<p>Set explicit boundaries about where it should and shouldn&rsquo;t be used. Security-sensitive code is the obvious no-go. But also consider: do you want AI-generated code in your core domain logic? The answer might be yes for a CRUD API and no for a payment processing engine.</p>
<p>Keep your quality bar unchanged. &ldquo;Copilot wrote it&rdquo; isn&rsquo;t an excuse for a bug. The person who accepted the suggestion owns the code.</p>
<h2 id="where-this-is-going">Where This Is Going</h2>
<p>Copilot is the starting point, not the ceiling. Broader project context, better refactoring support, and multi-file awareness are coming. But the fundamental challenge will remain: these tools generate code that looks right. Knowing whether it <em>is</em> right still requires a human who understands the problem.</p>
<p>The engineers who will benefit most are the ones who already have strong fundamentals &ndash; system design, testing discipline, domain knowledge. Copilot amplifies competence. It also amplifies carelessness. Which one you get depends on how you use it.</p>
]]></content:encoded></item><item><title>Your Engineering Docs Are Probably Useless</title><link>https://lawzava.com/blog/2022-06-13-engineering-documentation-practices/</link><pubDate>Mon, 13 Jun 2022 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2022-06-13-engineering-documentation-practices/</guid><description>Most engineering documentation is ignored for predictable reasons. Here is how to write docs that people actually read.</description><content:encoded><![CDATA[<p>Every team I&rsquo;ve joined &ndash; Decloud, a fintech startup, a large consumer platform &ndash; had a Confluence graveyard. Hundreds of pages. Nobody reading any of them. The onboarding guide references a deploy process that changed eight months ago. The architecture doc describes a system that was replaced last quarter. A new engineer follows the getting-started page and hits a dead end on step three.</p>
<p>This isn&rsquo;t a tooling problem. Notion, Confluence, GitHub wikis, markdown in the repo &ndash; the tool doesn&rsquo;t matter if nobody owns the content.</p>
<h2 id="why-nobody-reads-your-docs">Why Nobody Reads Your Docs</h2>
<p>It&rsquo;s always the same reasons:</p>
<p><strong>They can&rsquo;t find them.</strong> Docs live in three different places, none of them searchable in the way engineers actually search. Slack threads become the real documentation, and Slack threads expire.</p>
<p><strong>They don&rsquo;t trust them.</strong> One bad experience with an outdated runbook and the engineer stops checking docs entirely. Rational behavior. If the doc might be wrong, it&rsquo;s faster to ask a human.</p>
<p><strong>They&rsquo;re written for the wrong audience.</strong> A tutorial mixed with reference material mixed with architectural rationale. The reader can&rsquo;t tell what kind of doc they&rsquo;re looking at, so they close the tab.</p>
<h2 id="the-fix-is-boring">The Fix Is Boring</h2>
<p>Separate your doc types. This is the Divio framework and it works:</p>
<ul>
<li><strong>Tutorials</strong> &ndash; learning-oriented, for newcomers. &ldquo;Follow these steps and you&rsquo;ll have a running service.&rdquo;</li>
<li><strong>How-to guides</strong> &ndash; task-oriented, for people who already know the basics. &ldquo;How to rotate database credentials.&rdquo;</li>
<li><strong>Explanations</strong> &ndash; understanding-oriented. &ldquo;Why we chose event sourcing for the payment system.&rdquo;</li>
<li><strong>Reference</strong> &ndash; information-oriented. API specs, config options, environment variables.</li>
</ul>
<p>When you mix these, you get a doc that&rsquo;s too long for lookup and too shallow for learning. Pick one purpose per page.</p>
<h2 id="structure-that-earns-trust">Structure That Earns Trust</h2>
<p>Readers scan. They decide in five seconds whether to keep reading. Help them:</p>
<ul>
<li>State the purpose and audience in the first line.</li>
<li>List prerequisites up front.</li>
<li>One action per step. Expected output after each step.</li>
<li>A troubleshooting section for the two or three things that always go wrong.</li>
<li>An owner name and a &ldquo;last verified&rdquo; date.</li>
</ul>
<p>That last one matters more than people think. A doc with &ldquo;Last verified: 2022-06-01 by @law&rdquo; tells the reader someone is paying attention. A doc with no date and no owner tells the reader nobody is.</p>
<h2 id="ownership-is-the-whole-game">Ownership Is the Whole Game</h2>
<p>At a large consumer platform, we started requiring that every doc in the engineering wiki had exactly one owner. Not a team. A person. That person got a quarterly reminder to verify or archive.</p>
<p>The result: we deleted about 40% of our docs in the first quarter. That was a good thing. The remaining 60% were accurate, and engineers started trusting them again.</p>
<p>No owner means no maintenance. No maintenance means the doc will lie to you. A lying doc is worse than no doc.</p>
<h2 id="docs-as-code">Docs as Code</h2>
<p>Put docs in the repo. Require doc updates in PRs when behavior changes. Run link checks in CI. This isn&rsquo;t a radical idea &ndash; it&rsquo;s the same review process you already use for code. If the deploy process changes and the PR doesn&rsquo;t update the runbook, the reviewer should block it.</p>
<p>I know this sounds heavy. It&rsquo;s lighter than answering the same question in Slack every week.</p>
<h2 id="stop-measuring-page-views">Stop Measuring Page Views</h2>
<p>Page views tell you nothing useful. A doc that gets 500 views might be getting 500 confused visitors who leave after ten seconds.</p>
<p>Better signals: How many support questions come in on topics you have documented? How long does onboarding take? How often do incidents get extended because the runbook was wrong?</p>
<p>If the answer to &ldquo;where is the doc for X&rdquo; is always &ldquo;ask Sarah,&rdquo; your docs have failed regardless of how many pages you have.</p>
<h2 id="the-short-version">The Short Version</h2>
<p>Delete the docs nobody maintains. Give every surviving doc an owner. Separate tutorials from reference from how-to guides. Put them in the repo and review them like code.</p>
<p>Documentation is a product. Ship it or kill it.</p>
]]></content:encoded></item><item><title>TypeScript: A Go Developer's Honest Take</title><link>https://lawzava.com/blog/2022-05-16-typescript-best-practices/</link><pubDate>Mon, 16 May 2022 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2022-05-16-typescript-best-practices/</guid><description>TypeScript is the best thing to happen to JavaScript. That bar is lower than people think. Here&amp;amp;rsquo;s what actually matters for large codebases.</description><content:encoded><![CDATA[<p>I write Go most of the time. When I work on TypeScript projects &ndash; which happens regularly &ndash; I appreciate what it brings to the JavaScript ecosystem. I also think the community overcomplicates it. Generics four levels deep, utility type gymnastics, type-level programming that requires a PhD to review. The language is at its best when the types are simple and the boundaries are strict.</p>
<p>Here is what I think actually matters for large TypeScript codebases, from someone who would rather be writing Go.</p>
<h2 id="turn-on-strict-mode-keep-it-on">Turn on strict mode. Keep it on.</h2>
<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;compilerOptions&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;strict&#34;</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This is non-negotiable. <code>strict: true</code> catches null errors, implicit <code>any</code> types, and a whole class of runtime surprises. If your project doesn&rsquo;t have this enabled, every type annotation is half a lie because the compiler isn&rsquo;t actually enforcing the contract.</p>
<p>Retrofitting strict mode into an existing codebase is painful. Do it early or pay for it later. There&rsquo;s no third option.</p>
<h2 id="validate-at-the-boundaries-trust-the-interior">Validate at the boundaries, trust the interior</h2>
<p>The biggest insight from Go that translates directly to TypeScript: treat external data as hostile. API responses, form inputs, queue messages, file contents &ndash; all of it&rsquo;s <code>unknown</code> until you validate 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-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">parseUser</span>(<span style="color:#a6e22e">data</span>: <span style="color:#66d9ef">unknown</span>)<span style="color:#f92672">:</span> <span style="color:#a6e22e">User</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#66d9ef">typeof</span> <span style="color:#a6e22e">data</span> <span style="color:#f92672">!==</span> <span style="color:#e6db74">&#34;object&#34;</span> <span style="color:#f92672">||</span> <span style="color:#a6e22e">data</span> <span style="color:#f92672">===</span> <span style="color:#66d9ef">null</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> Error(<span style="color:#e6db74">&#34;expected object&#34;</span>);
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span>(<span style="color:#e6db74">&#34;id&#34;</span> <span style="color:#66d9ef">in</span> <span style="color:#a6e22e">data</span>) <span style="color:#f92672">||</span> <span style="color:#f92672">!</span>(<span style="color:#e6db74">&#34;email&#34;</span> <span style="color:#66d9ef">in</span> <span style="color:#a6e22e">data</span>)) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">throw</span> <span style="color:#66d9ef">new</span> Error(<span style="color:#e6db74">&#34;missing required fields&#34;</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">data</span> <span style="color:#66d9ef">as</span> <span style="color:#a6e22e">User</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Use a validation library like Zod if you want something less manual. The point is: validate once at the edge, then keep the interior of your codebase strongly typed. No <code>any</code> leaking through from API calls. No <code>as unknown as Whatever</code> casts to paper over the fact that you don&rsquo;t know what shape the data is.</p>
<h2 id="discriminated-unions-are-the-best-feature">Discriminated unions are the best feature</h2>
<p>This is the one thing TypeScript does better than Go. (I said what I said.)</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-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">type</span> <span style="color:#a6e22e">Result</span>&lt;<span style="color:#f92672">T</span>&gt; <span style="color:#f92672">=</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">|</span> { <span style="color:#a6e22e">ok</span>: <span style="color:#66d9ef">true</span>; <span style="color:#a6e22e">value</span>: <span style="color:#66d9ef">T</span> }
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">|</span> { <span style="color:#a6e22e">ok</span>: <span style="color:#66d9ef">false</span>; <span style="color:#a6e22e">error</span>: <span style="color:#66d9ef">string</span> };
</span></span></code></pre></div><p>Pattern match on the discriminant. The compiler enforces exhaustiveness. No forgotten error branches. No <code>if err != nil</code> &ndash; although honestly, I&rsquo;ve come to appreciate Go&rsquo;s explicitness there too.</p>
<p>Use discriminated unions for state machines, API responses, and anything with multiple possible shapes. They&rsquo;re readable, the tooling support is excellent, and they prevent an entire category of bugs.</p>
<h2 id="stop-overusing-generics">Stop overusing generics</h2>
<p>This is where TypeScript culture drives me up a wall. I regularly see utility types like <code>DeepPartial&lt;Omit&lt;Pick&lt;T, K&gt;, &quot;id&quot;&gt; &amp; Required&lt;Whatever&gt;&gt;</code> in production code. Nobody can read that. Nobody can review it confidently. It&rsquo;s type-level cleverness for its own sake.</p>
<p>In Go, the philosophy is: if you can&rsquo;t explain it in one sentence, it&rsquo;s too complicated. The same should apply to TypeScript generics. Constrain your type parameters. Keep generic functions to one or two type parameters. If a type definition needs a comment to explain what it does, simplify it.</p>
<h2 id="keep-modules-clean">Keep modules clean</h2>
<p>Cyclic dependencies are the slow death of large TypeScript projects. Use type-only imports to make intent clear:</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-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">createUser</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#34;./user.js&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#66d9ef">type</span> { <span style="color:#a6e22e">User</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#34;./user.js&#34;</span>;
</span></span></code></pre></div><p>Export types and values from the same module when they belong together. Don&rsquo;t create a <code>types.ts</code> file that every module imports from &ndash; that&rsquo;s a dependency magnet.</p>
<h2 id="the-honest-assessment">The honest assessment</h2>
<p>TypeScript is a significant improvement over JavaScript for anything beyond a small script. The compiler catches real bugs. Discriminated unions and strict null checks prevent real production incidents. The tooling is genuinely good.</p>
<p>But it isn&rsquo;t a type system that enforces correctness the way Go or Rust does. It&rsquo;s a type system bolted onto a dynamic language, with escape hatches everywhere. <code>any</code>, type assertions, <code>@ts-ignore</code> &ndash; the temptation to bypass the system is always one keystroke away.</p>
<p>The discipline isn&rsquo;t in the language. It&rsquo;s in the team. Strict mode on, <code>any</code> banned in lint rules, boundaries validated, generics kept simple. Do that and TypeScript earns its complexity budget. Skip it and you get all the overhead of a type system with half the guarantees.</p>
]]></content:encoded></item><item><title>Log4j Is on Fire. Here's What to Do Right Now.</title><link>https://lawzava.com/blog/2021-12-13-log4j-vulnerability-response/</link><pubDate>Mon, 13 Dec 2021 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2021-12-13-log4j-vulnerability-response/</guid><description>CVE-2021-44228 is the worst vulnerability I have seen in a decade. If you run Java anywhere, stop reading the news and start inventorying.</description><content:encoded><![CDATA[<p>If you&rsquo;re reading this on December 13, 2021 and you haven&rsquo;t started your Log4j response, stop reading after this section and go do it. Come back later.</p>
<p>CVE-2021-44228 is a remote code execution vulnerability in Apache Log4j 2. It&rsquo;s trivially exploitable. An attacker sends a crafted string to anything that gets logged, and your server can execute arbitrary code. No authentication needed. No special access. Just a string in a header, a search field, a username - anything that hits a log statement.</p>
<p>This is the worst vulnerability I&rsquo;ve seen in a decade of doing this work. Worse than Heartbleed. Worse than Shellshock. The attack surface is enormous because Log4j is everywhere and logging happens everywhere.</p>
<h2 id="what-to-do-right-now">What to Do Right Now</h2>
<p>I&rsquo;m helping three teams respond to this simultaneously. Here is the playbook I&rsquo;m running.</p>
<p><strong>Hour 1: Assign ownership and open a war room.</strong></p>
<p>Name an incident lead. Open a single tracking document &ndash; not a Slack thread, not three Jira boards. One document with one owner. This is a security incident, not a normal bug. Treat it accordingly.</p>
<p><strong>Hour 2-4: Inventory.</strong></p>
<p>This is the hardest and most important step. You need to know where Log4j exists in your environment. The problem is that it&rsquo;s a transitive dependency for hundreds of Java libraries and frameworks. You might not use Log4j directly. Spring Boot pulls it in. Apache Solr bundles it. Elasticsearch includes it. Your vendor&rsquo;s SaaS product might run it.</p>
<p>Where to look:</p>
<ul>
<li>Dependency manifests (<code>pom.xml</code>, <code>build.gradle</code>, <code>package-lock.json</code> for JVM wrappers)</li>
<li>Build artifacts &ndash; <code>find / -name &quot;log4j*.jar&quot;</code> on your servers, yes, really</li>
<li>Container images &ndash; scan them, don&rsquo;t assume</li>
<li>Vendor products and SaaS tools you deploy internally</li>
<li>CI/CD infrastructure itself (Jenkins is Java-based)</li>
</ul>
<p>Don&rsquo;t assume &ldquo;we don&rsquo;t use Java.&rdquo; I&rsquo;ve watched three separate organizations say this and then discover Log4j in their Elasticsearch cluster, their Jenkins server, and a vendor appliance nobody remembered existed.</p>
<p><strong>Hour 4-8: Mitigate the exposed services.</strong></p>
<p>Patch where you can. Log4j 2.16.0 is the fix as of today (2.15.0 had an incomplete fix, update to 2.16.0). For services you can&rsquo;t patch immediately:</p>
<ul>
<li>Set the JVM flag <code>-Dlog4j2.formatMsgNoLookups=true</code> (works for 2.10+)</li>
<li>Restrict outbound network access from application servers. If your Java service can&rsquo;t reach the internet, the JNDI lookup fails. This isn&rsquo;t a fix. It&rsquo;s a mitigation.</li>
<li>Add WAF rules to block <code>${jndi:</code> patterns in request headers and parameters. This is defense in depth, not a solution. Attackers are already finding bypass patterns.</li>
<li>For older versions (below 2.10), remove the <code>JndiLookup</code> class from the classpath entirely</li>
</ul>
<p>Prioritize internet-facing services first. Then internal services that process external input (email, file uploads, webhooks). Then everything else.</p>
<p><strong>Day 2+: Vendor pressure and verification.</strong></p>
<p>Email every vendor that runs Java-based products in your environment. Ask three questions:</p>
<ol>
<li>Does your product include Log4j? Which version?</li>
<li>Is a patch available now? If not, when?</li>
<li>What mitigations should we apply while waiting?</li>
</ol>
<p>Some vendors won&rsquo;t know yet. Track their status and follow up daily.</p>
<h2 id="why-this-is-so-bad">Why This Is So Bad</h2>
<p>Log4j is a logging library. Logging is one of those things every application does, everywhere, all the time. User input gets logged constantly &ndash; request parameters, headers, error messages, form fields. The vulnerability turns every log statement that touches user input into a potential RCE.</p>
<p>The JNDI lookup feature that enables the exploit was a feature, not a bug. It was designed to let log messages pull dynamic content from remote sources. Nobody anticipated that this would become a trivially exploitable code execution path. But here we are.</p>
<p>The blast radius isn&rsquo;t just your code. It&rsquo;s every dependency that uses Log4j. It&rsquo;s every vendor product. It&rsquo;s every internal tool. Exploitation is happening in the wild right now and automated scanning is widespread.</p>
<h2 id="the-inventory-problem-is-the-real-problem">The Inventory Problem Is the Real Problem</h2>
<p>The technical fix is straightforward: update Log4j. The hard part is knowing where Log4j lives.</p>
<p>Most organizations can&rsquo;t answer &ldquo;what software do we run and what are its dependencies&rdquo; quickly. This has been a known gap for years and it bites hardest during events exactly like this one.</p>
<p>If you come out of this incident without building a software bill of materials (SBOM) practice, you&rsquo;ll have the same problem next time. And there will be a next time.</p>
<p>What an SBOM practice looks like:</p>
<ul>
<li>Generate dependency manifests as part of your build pipeline</li>
<li>Store them in a searchable registry</li>
<li>Include transitive dependencies, not just direct ones</li>
<li>Cover vendor products and container base images</li>
<li>Be able to answer &ldquo;which services use library X at version Y&rdquo; in minutes, not days</li>
</ul>
<h2 id="communication-dont-go-silent">Communication: Don&rsquo;t Go Silent</h2>
<p>I&rsquo;ve seen organizations go quiet externally during this response because they&rsquo;re &ldquo;still assessing.&rdquo; That&rsquo;s the wrong call. Customers and partners are asking. Silence reads as &ldquo;they don&rsquo;t know&rdquo; or worse &ldquo;they don&rsquo;t care.&rdquo;</p>
<p>Send updates on a fixed cadence. Every 12 hours minimum. Even if the update is &ldquo;we&rsquo;re still inventorying and have mitigated N of M known-affected services.&rdquo; Structured communication builds trust. Silence destroys it.</p>
<p>Keep an internal status page with every service listed and its status: unknown, investigating, affected, mitigated, patched. Update it as you go. &ldquo;Unknown&rdquo; is a valid status &ndash; better than pretending you have checked things you haven&rsquo;t.</p>
<h2 id="after-the-fire">After the Fire</h2>
<p>When the acute response is over (and it will take weeks, not days), don&rsquo;t just move on. Some things that need to happen:</p>
<ul>
<li>Remove temporary mitigations and verify patches are applied end-to-end</li>
<li>Audit your response: how long did it take to produce a credible inventory? What did you miss? Where were the gaps?</li>
<li>Build the SBOM practice you should have had before this happened</li>
<li>Review your vendor management process &ndash; did you know which vendors to contact?</li>
<li>Update your incident response playbook with what you learned</li>
</ul>
<p>This vulnerability is a stress test for your entire security posture. How you respond to it says more about your organization&rsquo;s operational maturity than any compliance audit ever will.</p>
<p>Patch your systems. Inventory your dependencies. Communicate clearly. And when the dust settles, invest in the visibility that would have made this response faster.</p>
]]></content:encoded></item><item><title>Hybrid Work Is Harder Than Full Remote</title><link>https://lawzava.com/blog/2021-05-03-hybrid-work-engineering/</link><pubDate>Mon, 03 May 2021 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2021-05-03-hybrid-work-engineering/</guid><description>Everyone thinks hybrid is the compromise between remote and office. It is actually harder to get right than either extreme.</description><content:encoded><![CDATA[<p>Last month I was on a call with an engineering team. Eight people. Five in a conference room, three on video. The five in the room were having a conversation among themselves &ndash; body language, side comments, whiteboard sketches. The three on video were watching a slightly out-of-focus camera pointed at the whiteboard, hearing about half of the side conversations, and contributing maybe 10% of the ideas.</p>
<p>After the call, one of the remote engineers messaged me privately: &ldquo;I&rsquo;ve no idea what we decided.&rdquo;</p>
<p>That&rsquo;s hybrid work in one sentence.</p>
<h2 id="the-worst-of-both-worlds">The worst of both worlds</h2>
<p>I&rsquo;ve been fully remote for years. I ran distributed teams at Decloud. I worked with remote engineers at the fintech startup. Remote works because everyone operates under the same constraints. Nobody has hallway access. Nobody has a whiteboard advantage. All context lives in written artifacts because it has to.</p>
<p>Hybrid breaks that symmetry. Some people are in the room. Some aren&rsquo;t. And the people in the room have massive advantages they don&rsquo;t even notice.</p>
<p>Decisions happen in hallway conversations after the meeting ends. Context gets shared over lunch. Feedback happens in passing &ndash; &ldquo;hey, nice work on that PR&rdquo; &ndash; which sounds trivial until you realize the remote people never hear it.</p>
<p>The office people aren&rsquo;t doing this maliciously. It&rsquo;s just how humans work when they share physical space. Information flows through proximity. Hybrid means some people get that flow and some don&rsquo;t.</p>
<h2 id="remote-first-is-the-only-fix">Remote-first is the only fix</h2>
<p>If you&rsquo;re going hybrid, you have to operate as remote-first. Not &ldquo;remote-friendly.&rdquo; Not &ldquo;we&rsquo;ve good video conferencing.&rdquo; Remote-first.</p>
<p>That means: if one person in a meeting is remote, everyone joins from their own device. Even the five people sitting in the same conference room. Yes, it feels awkward. Yes, it works. Because now everyone has the same audio quality, the same view, and the same ability to participate.</p>
<p>That means: decisions are documented in writing. Not &ldquo;we&rsquo;ll share notes later.&rdquo; The notes are the decision. If it&rsquo;s not written down, it didn&rsquo;t happen. This was already true for remote teams. Hybrid teams need to adopt it explicitly because the temptation to rely on in-person context is strong.</p>
<p>That means: async communication is the default for status updates, progress sharing, and non-urgent questions. Sync time is reserved for things that genuinely benefit from real-time interaction &ndash; architecture discussions, debugging sessions, retrospectives.</p>
<h2 id="what-the-office-is-actually-good-for">What the office is actually good for</h2>
<p>I&rsquo;m not anti-office. The office is great for specific things. Onboarding new engineers who need face time to build relationships. Design workshops where you need a whiteboard and rapid iteration. Team retrospectives where emotional nuance matters. Pairing sessions on hard problems.</p>
<p>The office is terrible for status meetings. It&rsquo;s terrible for solo focus work. It&rsquo;s terrible for anything that could be a written update but instead becomes a 30-minute calendar block because someone wanted &ldquo;face time.&rdquo;</p>
<p>If your team&rsquo;s office days are filled with meetings that could have been documents, you&rsquo;re using the office wrong.</p>
<h2 id="the-meeting-problem">The meeting problem</h2>
<p>Meetings are where hybrid equity dies. I have a strict rule for the teams I advise:</p>
<p>Every meeting needs a written agenda shared before the meeting starts. If there&rsquo;s no agenda, there&rsquo;s no meeting. Every meeting gets notes published within an hour of ending. Action items have names and dates. Follow-up conversations that happen in the hallway get summarized in the notes channel.</p>
<p>This is annoying. It&rsquo;s also the minimum viable process for making hybrid fair. Without it, the remote engineers are always operating on incomplete information and always a step behind.</p>
<h2 id="performance-management-by-green-dot">Performance management by green dot</h2>
<p>Here&rsquo;s the thing that makes me angry about how some organizations do hybrid: they evaluate performance by presence. Who is in the office. Who responds fastest on Slack. Who is &ldquo;always available.&rdquo;</p>
<p>That&rsquo;s not measuring performance. That&rsquo;s measuring proximity. And it systematically disadvantages remote engineers, parents with caregiving responsibilities, anyone in a different time zone, and anyone who does their best work in focused blocks without interruption.</p>
<p>Measure outcomes. Code shipped. Problems solved. Systems improved. Incidents handled. If the work is good, I don&rsquo;t care where the person was sitting when they did it.</p>
<h2 id="why-this-is-harder-than-full-remote">Why this is harder than full remote</h2>
<p>Full remote forces discipline. You have to write things down because there&rsquo;s no alternative. You have to make meetings inclusive because everyone is remote. You have to trust people to manage their time because you can&rsquo;t see them.</p>
<p>Hybrid lets you be lazy. The in-office group gets context through osmosis and assumes everyone else has it too. Meetings are &ldquo;inclusive&rdquo; because there&rsquo;s a camera in the conference room. The written documentation is &ldquo;good enough&rdquo; because the office people fill in the gaps verbally.</p>
<p>That laziness creates a two-tier system. And it compounds over months. The remote engineers gradually lose context. They get passed over for projects that start with in-person conversations. They burn out from constantly feeling behind.</p>
<p>I&rsquo;ve seen this happen at three different organizations this year alone.</p>
<h2 id="the-simple-version">The simple version</h2>
<p>If you&rsquo;re going hybrid, commit to remote-first practices. Written decisions. Async defaults. Equal meeting participation. Outcome-based evaluation. Intentional office time for things the office is actually good at.</p>
<p>Or just go full remote and skip the hardest coordination problem in engineering management. Honestly? For most teams, that&rsquo;s the better call.</p>
]]></content:encoded></item><item><title>Rust for Cloud Services: A Go Developer's Honest Take</title><link>https://lawzava.com/blog/2021-02-22-rust-for-cloud-services/</link><pubDate>Mon, 22 Feb 2021 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2021-02-22-rust-for-cloud-services/</guid><description>I write Go for a living. Rust is not replacing it. But I have to be honest about where Rust wins.</description><content:encoded><![CDATA[<p>I&rsquo;m tired of the Rust discourse.</p>
<p>Every week there&rsquo;s a new blog post about how Rust is going to replace everything: Go, Java, Python, C++, your kitchen appliance firmware. The Rust evangelism strike force is relentless. As someone who writes Go professionally, contributes to Go projects, and has built production systems in Go for years, my default reaction is to roll my eyes.</p>
<p>But I&rsquo;ve been writing some Rust lately, and I have to be honest: for certain things, it&rsquo;s genuinely better than what I use.</p>
<p>That sentence hurt to type.</p>
<h2 id="where-go-wins-and-its-not-close">Where Go wins and it&rsquo;s not close</h2>
<p>Go wins on velocity. I can write a production-ready HTTP service in Go in an afternoon. The standard library is excellent. The tooling is fast. <code>go build</code> gives me a static binary. <code>go test</code> just works. The language is intentionally simple, which means new team members are productive in days, not weeks.</p>
<p>At Decloud, at the fintech startup, in everything I&rsquo;ve built, Go is the default. It&rsquo;s boring in the best possible way. The compile times are fast. The deployment story is trivial. The hiring pool is large and growing.</p>
<p>For the vast majority of cloud services &ndash; API servers, background workers, CLI tools, infrastructure automation &ndash; Go is the right choice. I&rsquo;ll die on this hill.</p>
<h2 id="where-rust-wins-and-i-have-to-admit-it">Where Rust wins and I have to admit it</h2>
<p>Tail latency. That&rsquo;s the killer argument.</p>
<p>Go&rsquo;s garbage collector has gotten dramatically better over the years. Sub-millisecond pauses in most cases. But &ldquo;most cases&rdquo; isn&rsquo;t good enough when you have a service with strict p99 latency targets and you&rsquo;re processing thousands of requests per second. Those GC pauses show up in your tail latency, and no amount of tuning eliminates them completely.</p>
<p>Rust doesn&rsquo;t have a garbage collector. Its ownership model handles memory at compile time. The result is predictable latency with no runtime surprises. For services where p99 matters as much as p50 &ndash; high-frequency data processing, real-time bidding, network proxies &ndash; that&rsquo;s a legitimate advantage.</p>
<p>Memory footprint is the other one. I&rsquo;ve got a Go service that idles at 40MB of RSS. The equivalent Rust service? 8MB. For edge deployments or anything running thousands of instances, that difference translates directly to infrastructure cost.</p>
<p>And then there&rsquo;s safety. Go has data race detection with <code>-race</code>, but only at runtime. Rust catches data races at compile time. For security-sensitive code that processes untrusted input, having the compiler do that work for you is genuinely valuable.</p>
<h2 id="the-rust-ecosystem-in-2021">The Rust ecosystem in 2021</h2>
<p>Tokio 1.0 landed. That&rsquo;s a big deal: it means the async runtime is stable and you aren&rsquo;t going to have the rug pulled out from under you on a major API change. Hyper, Actix Web, and Warp are all viable for HTTP services. Serde is excellent for serialization. The <code>tracing</code> crate is the right approach to structured observability.</p>
<p>It&rsquo;s usable. It&rsquo;s not turnkey.</p>
<p>Want an ORM? Diesel exists, but async support is clunky. SQLx is better for async, but newer. Want something like Go&rsquo;s <code>net/http</code>, where you import one package and have a production-ready server? You&rsquo;re assembling it from five crates and hoping version compatibility holds.</p>
<h2 id="my-actual-problem-with-rust-adoption">My actual problem with Rust adoption</h2>
<p>It&rsquo;s not the language. The language is well designed. My problem is the adoption pattern I keep seeing.</p>
<p>A team has a Go service. The service works fine. Someone reads a blog post about Rust performance. The team rewrites the service in Rust. It takes three months instead of two weeks. The performance improvement is 15% on a service that wasn&rsquo;t performance-constrained. The team now has one person who can maintain the Rust code and four who can&rsquo;t.</p>
<p>That&rsquo;s not a Rust problem. That&rsquo;s a decision-making problem. But Rust&rsquo;s community actively encourages this pattern by framing everything as &ldquo;rewrite it in Rust&rdquo; without asking whether the rewrite solves a real problem.</p>
<p>If you&rsquo;re considering Rust for a cloud service, answer these questions first:</p>
<ul>
<li>Is there a measured performance or safety problem that Go (or whatever you use) can&rsquo;t solve?</li>
<li>Do you have at least two people who can write and review Rust code?</li>
<li>Have you accounted for compile times in your CI pipeline? A Rust build from scratch takes minutes, not seconds.</li>
<li>Can you hire for Rust in your market?</li>
</ul>
<p>If any answer is no, profile your existing code first. You&rsquo;ll probably find that the bottleneck is a bad algorithm or an unnecessary allocation, not the language runtime.</p>
<h2 id="where-i-would-actually-use-rust">Where I would actually use Rust</h2>
<p>I would use Rust for a network proxy that needs microsecond-level latency consistency. I would use it for a data processing pipeline that&rsquo;s CPU-bound and memory-constrained. I would use it for anything running on embedded hardware or at the edge, where every megabyte counts. I would use it for security-critical parsers that handle untrusted input.</p>
<p>I wouldn&rsquo;t use it for a CRUD API. I wouldn&rsquo;t use it for a CLI tool. I wouldn&rsquo;t use it for a service where time-to-market matters more than raw performance.</p>
<p>Go is my tool. Rust is a tool I respect. The trick is knowing which problem you actually have before you pick the tool.</p>
]]></content:encoded></item><item><title>eBPF Is Interesting. I Am Not Sold Yet.</title><link>https://lawzava.com/blog/2021-01-25-ebpf-observability/</link><pubDate>Mon, 25 Jan 2021 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2021-01-25-ebpf-observability/</guid><description>eBPF promises kernel-level observability without the pain of kernel modules. The tech is real. The hype-to-adoption ratio concerns me.</description><content:encoded><![CDATA[<p>eBPF is the most overhyped technology in the observability space right now, and it might also be the most important.</p>
<p>That&rsquo;s not a contradiction. I&rsquo;ve been running Linux in production since before containers were a thing. The idea of safely running custom programs inside the kernel &ndash; attaching to tracepoints, kprobes, uprobes, without writing a kernel module or rebooting anything &ndash; is genuinely exciting. When I first ran <code>bpftrace</code> against a production system and got per-process syscall counts in real time with near-zero overhead, I understood the appeal immediately.</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-bash" data-lang="bash"><span style="display:flex;"><span>bpftrace -e <span style="color:#e6db74">&#39;tracepoint:raw_syscalls:sys_enter { @[comm] = count(); }&#39;</span>
</span></span></code></pre></div><p>That one-liner gives you more insight into what your system is actually doing than most monitoring stacks costing six figures a year. The kernel verifier checks your program for safety. Data flows through maps and perf buffers. No agent bloat. No sampling artifacts. Just direct observation at the source.</p>
<p>So why am I skeptical?</p>
<p>Because the gap between &ldquo;this is technically possible&rdquo; and &ldquo;my team can operate this in production&rdquo; is enormous. And the eBPF community seems uninterested in acknowledging that gap.</p>
<h2 id="the-promise-is-real">The promise is real</h2>
<p>I don&rsquo;t want to undersell what eBPF enables. Traditional monitoring gives you counters, logs, and coarse sampling. Fine for dashboards. Terrible for understanding why a specific request took 800ms when the p50 is 12ms. eBPF lets you attach instrumentation at the exact point where something happens. Syscall latency. TCP retransmits by destination. Filesystem I/O by process. All with filtering done in-kernel so you aren&rsquo;t drowning user space in data.</p>
<p>For container-dense environments &ndash; which is everything I work with these days &ndash; the ability to map kernel events to cgroups and namespaces is a game changer. Short-lived processes that vanish before your log collector notices? eBPF sees them.</p>
<h2 id="the-reality-check">The reality check</h2>
<p>Here&rsquo;s my problem. Every conference talk shows eBPF solving elegant debugging puzzles. Nobody talks about the operational burden.</p>
<p>Kernel version compatibility is a real issue. eBPF features vary across kernel versions, and the enterprise Linux distributions I see in production aren&rsquo;t exactly bleeding edge. A program that works on kernel 5.10 might not work on 4.18. BTF (BPF Type Format) availability is inconsistent. CO-RE (Compile Once, Run Everywhere) helps but isn&rsquo;t universally supported yet.</p>
<p>Then there&rsquo;s the expertise problem. Writing eBPF programs isn&rsquo;t like writing application code. You need to understand kernel internals, verifier constraints, and the performance implications of your hook points. Most engineering teams I work with can&rsquo;t spare someone to become the eBPF specialist. They need tools that work out of the box.</p>
<p>BCC, bpftrace, and the growing ecosystem of pre-built tools help. Brendan Gregg&rsquo;s work has been invaluable. But &ldquo;install bcc-tools and run execsnoop&rdquo; is a long way from &ldquo;build a production observability pipeline backed by eBPF.&rdquo;</p>
<h2 id="where-i-land">Where I land</h2>
<p>eBPF is infrastructure technology. It&rsquo;ll become the foundation that observability vendors build on. Cilium is already proving this for networking. The profiling tools are getting there. Give it two or three more years and it will be invisible plumbing that powers your monitoring stack.</p>
<p>But right now, in early 2021, if someone tells me they&rsquo;re building their observability strategy around eBPF, I ask two questions: what kernel version are you running, and who on your team understands the verifier? If they can&rsquo;t answer both, they should start with existing tools &ndash; <code>opensnoop</code>, <code>tcpconnect</code>, <code>biolatency</code> &ndash; and build intuition before writing custom programs.</p>
<p>The technology is sound. The ecosystem is maturing. I&rsquo;m watching closely. I&rsquo;m just not rewriting my monitoring stack around it today.</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>Most Developer Productivity Metrics Are Management Theater</title><link>https://lawzava.com/blog/2020-08-31-developer-productivity-metrics/</link><pubDate>Mon, 31 Aug 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-08-31-developer-productivity-metrics/</guid><description>Lines of code, velocity charts, commit counts — most developer productivity metrics are garbage. DORA metrics are the only ones worth your time.</description><content:encoded><![CDATA[<p>Someone on LinkedIn this week posted a dashboard showing lines of code per developer per sprint. Color-coded. Red for the &ldquo;underperformers.&rdquo; Green for the heroes. I wanted to throw my laptop out the window.</p>
<p>We&rsquo;re in 2020. Half the industry just went remote overnight and managers are panicking. I get it. You can&rsquo;t see people at their desks anymore. So now there&rsquo;s this desperate scramble to measure <em>something</em> to prove people are working. And the tools are happy to sell you that something.</p>
<p>Lines of code is the worst offender but it&rsquo;s far from the only one. Commits per day. Tickets closed. Story points completed. Velocity. All garbage when used to evaluate individual developer performance. Every single one.</p>
<h2 id="why-these-metrics-are-garbage">Why these metrics are garbage</h2>
<p>Here&rsquo;s the thing. I once mass-deleted 12,000 lines of dead code from a Rails monolith in a single afternoon. Most productive day I&rsquo;d had in months. By a lines-of-code metric, I went negative. Fire me, I guess.</p>
<p>Commits per day? I know devs who commit every time they save. I know devs who work on a complex problem for three days and push one beautiful, well-tested commit. The second developer is almost always doing better work.</p>
<p>Story points are the sneakiest one. They were invented as a <em>planning</em> tool. A way for teams to forecast how much they could take on. Somewhere along the way, managers started treating velocity as a performance metric. &ldquo;Your velocity dropped this sprint, what happened?&rdquo; What happened is Goodhart&rsquo;s Law. The moment you use velocity to judge people, teams inflate their estimates. Suddenly a two-pointer becomes a five-pointer. Velocity goes up. Actual output stays the same. Everyone pretends not to notice.</p>
<p>I&rsquo;ve watched this play out at three different companies now. Same story every time.</p>
<h2 id="the-only-metrics-i-actually-care-about">The only metrics I actually care about</h2>
<p>DORA metrics. That&rsquo;s it. Four numbers. Boring, validated by actual research, and extremely hard to game.</p>
<p><strong>Deployment frequency.</strong> How often are you shipping to production? Not &ldquo;how often are you merging PRs.&rdquo; Actually shipping. If your team deploys once a month, you have a delivery problem. Fix it before you measure anything else.</p>
<p><strong>Lead time for changes.</strong> From commit to production. This tells you how much friction is in your pipeline. Long lead times mean slow code review, flaky CI, painful deployments, or all three. Every one of those is worth fixing.</p>
<p><strong>Change failure rate.</strong> What percentage of deployments cause a failure? This is the quality counterweight. You can&rsquo;t just deploy fast and break everything. Well, you can. But this number will catch you.</p>
<p><strong>Time to restore service.</strong> When something breaks, how fast do you fix it? This is the one that actually matters at 3am. And it tells you more about your team&rsquo;s operational maturity than any sprint dashboard ever will.</p>
<p>These four together give you a real picture. Speed <em>and</em> stability. You can&rsquo;t game them easily because they pull in opposite directions. Ship faster but break things? Your failure rate goes up. Over-index on stability? Your deployment frequency drops.</p>
<h2 id="what-i-actually-do">What I actually do</h2>
<p>At my teams, I track DORA metrics at the team level. Not individual. Never individual. I don&rsquo;t care which developer deployed what. I care whether the system is getting better or worse.</p>
<p>We look at the numbers monthly. Trend lines, not snapshots. If lead time is creeping up, we dig into why. Usually it&rsquo;s a CI problem or a review bottleneck. Fix the system, not the people.</p>
<p>I also ask one question in retros: &ldquo;What slowed you down this week?&rdquo; The answers are always more useful than any dashboard. Flaky tests. Unclear requirements. Waiting two days for a review. That&rsquo;s where the real productivity wins are hiding.</p>
<h2 id="stop-measuring-people-fix-the-system">Stop measuring people, fix the system</h2>
<p>The entire premise of individual developer productivity metrics is broken. Software is collaborative. The best developer on your team might spend half their time unblocking others, reviewing code, and mentoring juniors. By any activity metric, they look &ldquo;less productive&rdquo; than the person grinding out solo features. But remove them and watch the whole team slow down.</p>
<p>If your response to remote work is to install monitoring software or build LOC dashboards, you don&rsquo;t have a productivity problem. You have a trust problem. And no metric is going to fix that.</p>
<p>Measure the system. Use DORA. Fix the bottlenecks. Talk to your team. Everything else is theater.</p>
]]></content:encoded></item><item><title>What I Actually Changed About Engineering Interviews Over Zoom</title><link>https://lawzava.com/blog/2020-05-25-virtual-interviewing-engineering/</link><pubDate>Mon, 25 May 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-05-25-virtual-interviewing-engineering/</guid><description>Whiteboard coding over Zoom is broken. Here&amp;amp;rsquo;s what I do instead when hiring engineers virtually.</description><content:encoded><![CDATA[<p>Last month I watched a senior backend candidate freeze for forty-five seconds on a Zoom call. Not because of the problem. Because CoderPad lagged, ate half his code, and he didn&rsquo;t know if I could see his screen. By the time we sorted it out, his headspace was gone. We ended the session early and I felt like an idiot for not testing the tool beforehand.</p>
<p>That was week three of Decloud. We were hiring our first engineers and doing it fully remote from day one. Not because we were visionary. Because it was May 2020 and there was no other option.</p>
<p>Since then I&rsquo;ve run maybe fifty virtual interviews across Decloud and the fintech startup. I&rsquo;ve gotten some things very wrong and a few things right. Here&rsquo;s what actually matters.</p>
<h2 id="whiteboard-coding-over-zoom-is-broken">Whiteboard coding over Zoom is broken</h2>
<p>I&rsquo;ll die on this hill. Asking someone to solve an algorithm on a shared whiteboard tool while you watch through a webcam is a terrible experience for everyone involved. The candidate can&rsquo;t think naturally. You can&rsquo;t read their body language. The tool always has some lag or quirk. And what are you even measuring? Their ability to perform under artificial pressure on a laggy canvas?</p>
<p>I stopped doing it. Completely.</p>
<p>Instead I send a small take-home problem. Two to three hours of real work, a week to finish it. Then the live session is a code review of their submission. We walk through their decisions, I push on trade-offs, they refactor something live. This tells me ten times more than watching someone implement a linked list reversal on Miro.</p>
<h2 id="the-setup-matters-more-than-you-think">The setup matters more than you think</h2>
<p>The single biggest variable in virtual interview quality isn&rsquo;t the questions. It&rsquo;s whether the tech works. Sounds obvious. Almost nobody actually prepares for it.</p>
<p>My checklist before every interview block:</p>
<ul>
<li>Open the shared editor, paste something, delete it. Confirm it works on both sides.</li>
<li>Test screen sharing. Every. Single. Time.</li>
<li>Have a phone number or Telegram handle ready as backup. Zoom dies more often than people admit.</li>
<li>Close Slack, email, everything. Notifications popping up during someone&rsquo;s interview is disrespectful.</li>
</ul>
<p>I also send candidates a prep email 48 hours out. Not a corporate template. A short note that says: here&rsquo;s the Zoom link, here&rsquo;s what we&rsquo;ll cover, here&rsquo;s the editor we&rsquo;ll use, install nothing, and if anything breaks just message me on this number. People visibly relax when they know the logistics.</p>
<h2 id="keep-it-short">Keep it short</h2>
<p>An hour-long virtual interview is thirty minutes of good signal and thirty minutes of diminishing returns. I cap everything at 45 minutes. The first five are small talk and tech check. The last five are their questions. That leaves 35 minutes of actual interview. Plenty.</p>
<p>If you need more depth for a senior role, split it into two sessions on different days. Two focused 45-minute conversations beat one draining 90-minute marathon.</p>
<h2 id="what-i-look-for-changed-too">What I look for changed too</h2>
<p>On-site, you get all these ambient signals. How someone walks into the office. How they interact with the receptionist. Whether they ask good questions during the tour. That&rsquo;s all gone now.</p>
<p>So I leaned harder into things I can actually observe on a call:</p>
<p><strong>Communication under ambiguity.</strong> I intentionally leave parts of the problem vague and see how they handle it. Do they ask? Do they assume? Do they state their assumptions clearly?</p>
<p><strong>Debugging live.</strong> During the code review session, I&rsquo;ll point at something and say &ldquo;this will break if X happens.&rdquo; Watching someone debug in real time, talking through their reasoning, is the most reliable signal I&rsquo;ve found for engineering skill.</p>
<p><strong>Written communication.</strong> I added a short async component. After the take-home, before the live call, I ask candidates to write a paragraph or two about one decision they made and why. At a remote company, writing isn&rsquo;t optional. If someone can&rsquo;t explain a technical choice in a few sentences, that&rsquo;s a problem.</p>
<h2 id="the-bias-trap">The bias trap</h2>
<p>One thing that caught me off guard: I started unconsciously judging people&rsquo;s home setups. Nice bookshelf, good lighting, quality microphone — must be a serious person. Messy background, laptop mic, kids in the next room — less so.</p>
<p>This is garbage thinking and I had to actively catch myself. Someone&rsquo;s apartment has zero correlation with their engineering ability. We made it a rule: cameras optional, background irrelevant, connection issues get a reschedule with no questions asked.</p>
<h2 id="what-id-tell-you-to-do">What I&rsquo;d tell you to do</h2>
<p>If you&rsquo;re setting up virtual interviews for the first time, do three things:</p>
<ol>
<li>Kill the live whiteboard coding. Replace it with take-home plus code review. Your signal will improve overnight.</li>
<li>Test your tools before every single session. Not once. Every time.</li>
<li>Make the process shorter than you think it needs to be. Respect people&rsquo;s energy.</li>
</ol>
<p>Virtual interviewing isn&rsquo;t a downgrade. It&rsquo;s a different format. Once I stopped trying to replicate the on-site experience over Zoom and started designing for the medium, the quality of our hires went up. Not down.</p>
]]></content:encoded></item><item><title>State Of Linux Usability 2020</title><link>https://lawzava.com/blog/state-of-linux-usability-2020/</link><pubDate>Mon, 04 May 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/state-of-linux-usability-2020/</guid><description>We&amp;amp;rsquo;ve carried out a series of daily tasks on TOP 20 Linux distros as well as Windows and macOS to test whether Linux has a chance to compete in daily use space.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>We&rsquo;ve carried out a series of daily tasks on TOP 20 Linux distros as well as Windows and macOS to test whether Linux has a chance to compete in daily use space. And the answer is - Yes since neither Mac nor Windows came on the top. There&rsquo;s still a very long way to go for all of them, however.</p>
<h3 id="motivation">Motivation</h3>
<p>I&rsquo;ve been a Linux evangelist and a strong user (as in regular usage, not just for servers) for almost two decades since I&rsquo;ve been introduced to Mandrake Linux with a mysterious penguin in the background.</p>
<p>That&rsquo;s why I&rsquo;ve been the one who consistently annoys family, friends,
and colleagues until they&rsquo;re forced to give up their bellowed non-Linux life.</p>
<p>Of course, the change is often difficult since they are thrown in a never-before-seen world with never-before-understood rules. Being a CLI-first person, I often overlook what is so hard about it.</p>
<p>But over the years, I&rsquo;ve received fewer and fewer complaints, and since there have been almost no cases of people going back unless it&rsquo;s for work, I&rsquo;ve decided to do a proper test to see whether someone who&rsquo;s not a technician can live a proper Linux life.</p>
<h3 id="the-subject">The Subject</h3>
<p>I wanted the subject to be someone with little-to-no technical skills/background as well as available for close supervision during the tests (and quarantine) to get accurate results.</p>
<p>So the background of the chosen subject is: a human, an illustrator, a long-time Mac-only user, spends 40% of work drawing on paper, 30% on Adobe suite and 30% on social media, saw me working numerous times and referred to the terminal as a &ldquo;mysterious black window,&rdquo; never installed an OS before, required assistance towards installing Adobe software, not a native English speaker.</p>
<p>What I want to emphasize is that the subject isn&rsquo;t a tech-friendly person at all.</p>
<p>So before we started I had to teach the subject a few simple things: what ISO is, how to use Etcher ( <a href="https://www.balena.io/etcher/"
    title="Etcher"
   
target="_blank" rel="noopener">https://balena.io</a>
) and how to select USB as a bootable device.</p>
<p>No further advice or assistance has been given.</p>
<p>Of course, we need to put a disclaimer here - the subject is human after all and humans learn over time so that test results in similar desktop environments or derivatives can be influenced by previous experience, but from the obvious dissatisfaction that has been witnessed, I can confirm that previous experience hasn&rsquo;t benefited too much.</p>
<h3 id="rules">Rules</h3>
<p>General rules for making the results more relevant:</p>
<ul>
<li>Any outside support to solve a problem is forbidden, including other people, blogs, and search engines (except for the chrome installation task). Only out-of-box resources shall be used.</li>
<li>Terminal use is also prohibited. Some apps provide terminal-based installation instructions - no one can expect newbies to rely on a &ldquo;mysterious black box&rdquo; to perform simple tasks.</li>
<li>If a single task reaches the 20min mark, usage of a search engine is permitted.</li>
<li>If a single task reaches the 60min mark, the entire distro is deemed unusable and the test group is considered to have failed.</li>
</ul>
<h3 id="tasks">Tasks</h3>
<p>There are 16 tasks in total:</p>
<ul>
<li>Download ISO | tests web navigation UX, mirrors availability, clarity of choices, ISO size</li>
<li>Flash ISO to USB | tests ISO size and file count</li>
<li>Install encrypted system | tests installation process experience, encryption can be omitted only if it isn&rsquo;t available</li>
<li>Change wallpaper | tests basic personalization interface and options</li>
<li>Play song on Spotify desktop | tests support for popular proprietary software, the initial internet connection</li>
<li>Send message from telegram desktop | tests support for popular proprietary software</li>
<li>Check email on google chrome | google chrome is rarely available in the app stores so this is the only exception where the installer can be downloaded, tests support for installing downloaded packages</li>
<li>Create a PDF document with your name in it | tests initial tools availability for document editing</li>
<li>Take a screenshot | tests initial tools availability for a screenshot</li>
<li>Add a circle in the screenshot | tests initial tools availability for basic image editing</li>
<li>Resize or crop the picture to 100x100px | tests initial tools availability for basic image editing</li>
<li>Rename the picture to &ldquo;my New Pic&rdquo; | tests interface behavior for basic file adjustment</li>
<li>Set picture as a user profile picture | tests interface availability for basic user personalization</li>
<li>Install Steam | tests support for popular software</li>
<li>Delete steam | tests ease of cleanup</li>
<li>Power off the computer | tests power off clarity, availability, and pre-shutdown behavior</li>
</ul>
<p>Each task is measured by the length of time it took to complete with additional subject scores for general UX (system intuitiveness, tool availability, etc.) and UI (out-of-box interface esthetics).</p>
<p>This will split evaluation into 4 total categories:</p>
<ul>
<li>The time required to set up (installing the system on the device)</li>
<li>The time required to complete tasks (all other tasks apart installation)</li>
<li>UX Score (subject to subject&rsquo;s subjective opinion)</li>
<li>UI Score (subject to subject&rsquo;s subjective opinion)</li>
</ul>
<h3 id="evaluation">Evaluation</h3>
<p>Each system is ranked from the best to the worst in each category and given a point for the respective reverse position (1-22). If the scores are equal, the maximum is given for the respective placement and continues from the minimum (e.g. 18, 17, 16, 16, 16, 13, 12, 11).</p>
<h3 id="tools">Tools</h3>
<p>To be completely consistent with the test results, the same machine and network were used for all tests.</p>
<p>Machine: Thinkpad T480s (i5-8250U, 256GB Samsung 970 Plus NVMe, 16GB RAM).
For macOS installation late-2017 MBP (i5, 128GB, 8GB RAM) was used.</p>
<p>Network: A dedicated (all other devices were disconnected) 100mbps, pretty stable, always through LAN.</p>
<h3 id="data-set">Data Set</h3>
<p>The data set consisted of Windows and macOS as a control group, then picked up TOP 20 distros from DistroWatch ( <a href="https://distrowatch.com/"
    title="DistroWatch"
   
target="_blank" rel="noopener">https://distrowatch.com</a>
) over 12 months, removed Arch as it would require CLI knowledge and included Regolith ( <a href="https://regolith-linux.org"
    title="Regolith Linux"
   
target="_blank" rel="noopener">https://regolith-linux.org</a>
) instead (just because it&rsquo;s my go-to distro).</p>
<p>So the final list is macOS, Windows, MX Linux, Manjaro, Mint, Debian, Ubuntu, Elementary, Solus, Fedora, Zorin, Deepin, Antix, KDE Neon, OpenSUSE, CentOS, Pop OS, ArcoLinux, PCLinuxOS, Kali, ReactOS, Regolith.</p>
<p>I&rsquo;m aware that most of the tasks are software-specific activity, so the test is mostly about OS compatibility with day-to-day apps and means to obtain them.</p>
<h3 id="what-went-wrong">What Went Wrong</h3>
<p>Since all tasks were expected to go smoothly by default, I&rsquo;ve only noted failures/pain points when using a particular system that I noticed when observing the subject.</p>
<p>I know there are workarounds for some problems, different paths and so on.
But if they haven&rsquo;t been discovered by the subject in time, they are too hidden from the user and are therefore considered invalid.</p>
<p>So the results are as follows:</p>
<h4 id="failed-tests">Failed Tests:</h4>
<h5 id="antix">Antix</h5>
<ul>
<li>Installation sometimes froze during install, if it succeeded it never booted (tried all grub options).</li>
</ul>
<h5 id="opensuse">openSUSE</h5>
<ul>
<li>Connecting to already attached LAN cable was hell, still required to manually &ldquo;configure&rdquo; the connection to use it, even if &ldquo;automatic&rdquo; mode was selected.</li>
<li>The struggle to find Spotify ended up in multiple restarts and even the &ldquo;Discovery&rdquo;  app store failed to launch.</li>
</ul>
<h5 id="centos">CentOS</h5>
<ul>
<li>There was no progress at all with finding Spotify - no 3rd party repositories or snap or Flatpak were available.</li>
</ul>
<h5 id="arcolinux">ArcoLinux</h5>
<ul>
<li>Software installation utility started crashing because of some file conflict, failed to find any way to repair through GUI.</li>
</ul>
<h5 id="kali">Kali</h5>
<ul>
<li>No software managing app was available, and it wasn&rsquo;t possible to install one without CLI usage.</li>
</ul>
<h5 id="reactos">ReactOS</h5>
<ul>
<li>Download was at ~70% when it hit the 60min mark (SourceForge auto-mirror &amp; no issues with the network).</li>
</ul>
<h4 id="successful-tests">Successful Tests:</h4>
<h5 id="macos">macOS</h5>
<ul>
<li>Installation took a long time. As I understood it&rsquo;s a general issue with all macOS installs.</li>
<li>When installing Telegram it was unclear what &ldquo;drag to Applications&rdquo; meant after mounting the downloaded image. Telegram from the app store wasn&rsquo;t being installed for some reason, no errors were shown.</li>
</ul>
<h5 id="windows">Windows</h5>
<ul>
<li>During the installation a permission to collect/user personal data was asked too many times (at least 8), it was annoying.</li>
<li>When powering off the system, surprise - windows update was enforced without a visible option to bypass it.</li>
</ul>
<h5 id="mx-linux">MX Linux</h5>
<ul>
<li>It was unclear how to find anything remotely close to the app store, the subject kept bumping into the software update page when finally found the &ldquo;software installer&rdquo;.</li>
<li>When installing anything live-action log with hard to comprehend errors was too overwhelming.</li>
<li>It took a long time until the &ldquo;Flatpak&rdquo; tab was discovered as a software source. It&rsquo;s confusing when it&rsquo;s the first time seeing this word.</li>
</ul>
<h5 id="manjaro">Manjaro</h5>
<ul>
<li>It was very confusing and time-consuming to find out that the desired apps are either in &ldquo;AUR&rdquo; or &ldquo;Snap&rdquo; sections, which from the names doesn&rsquo;t sound like places to find apps.</li>
</ul>
<h5 id="mint">Mint</h5>
<ul>
<li>Setting the profile picture when selecting the file redirected to /root by default, not /home/$USER so it took time to find the required file in the file system.</li>
</ul>
<h5 id="debian">Debian</h5>
<ul>
<li>It was a long fight but essentially to install a Spotify app without using CLI you have to install the &ldquo;Discovery&rdquo; app store from which you need to install a &ldquo;snap plugin for gnome store&rdquo; and then go back to gnome store to find Spotify.</li>
</ul>
<h5 id="ubuntu">Ubuntu</h5>
<ul>
<li>The default ISO mirror location isn&rsquo;t optimized. Custom mirror selection isn&rsquo;t obvious.</li>
</ul>
<h5 id="elementary">Elementary</h5>
<ul>
<li>Changing the background is impossible through a usual right-click on the desktop and no app handles remotely close to that, the journey to the system settings for this simple matter was a painful one.</li>
<li>After downloading chrome .deb, trying to open it (double-click) doesn&rsquo;t bring the window of the installer to the front if it&rsquo;s already open (discovery app store) - this was especially frustrating.</li>
<li>No default office suite is available</li>
</ul>
<h5 id="solus">Solus</h5>
<ul>
<li>Install process hung up on detecting local storage devices. After restarting and failing multiple times it finally worked (same settings).</li>
<li>Spotify was available in the software app under 3rd party apps but after installing it didn&rsquo;t appear anywhere so it was impossible to launch until &ldquo;check for updates&rdquo; was pressed and it finally appeared in the app list.</li>
<li>After installing telegram all apps stopped working nothing would open until after the complete reboot of the system</li>
<li>No native tools available for image editing since using LibreOffice Draw kept crashing.</li>
</ul>
<h5 id="fedora">Fedora</h5>
<ul>
<li>Spotify was available only after enabling 3rd party repositories, installing updates and rebooting the whole system then installing discovery on which installing snap backend, rebooting again and fallback to search engine usage where rpm package was found, lots of loops until Flatpak backend was discovered for Discovery and Spotify Flathub URL was added (tutorial online)</li>
<li>When opening system settings for 2nd time it stayed on the last open section (note - the system was restarted before), it wasn&rsquo;t clear how to go back to the settings menu to find where to change the user picture.</li>
</ul>
<h5 id="zorin">Zorin</h5>
<ul>
<li>Very slow ISO download.</li>
<li>Installing steam errored several times regarding &ldquo;unmet dependencies&rdquo;, took a while to try to update the system, then reboot and only then to succeed in steam install.</li>
</ul>
<h5 id="deepin">Deepin</h5>
<ul>
<li>It was confusing which app actually was meant for document creation and even then it was hard to find how to export to pdf.</li>
<li>Steam wasn&rsquo;t in the app store, had to be downloaded separately, afterward when doing right-click on the app to uninstall, the app store would throw an error, eventually reopening the same .deb and then finding the &ldquo;remove&rdquo; solved the issue.</li>
</ul>
<h5 id="kde-neon">KDE Neon</h5>
<ul>
<li>Very slow ISO download.</li>
<li>No default picture editing software had to download separately.</li>
</ul>
<h5 id="pclinuxos">PCLinuxOS</h5>
<ul>
<li>Spotify was preinstalled but the software manager UI was so overwhelming that it took a long time to notice it.</li>
<li>After installing Telegram it didn&rsquo;t appear in the app list until the reinstall.</li>
<li>Installing Steam required using a search engine to find that the only way to get it is to install Playonlinux first and only then to install Steam inside it.</li>
</ul>
<h5 id="regolith">Regolith</h5>
<ul>
<li>It was a first time the subject used tiling window manager, no onboarding was available so all actions were incredibly slow</li>
</ul>
<h3 id="final-results">Final Results</h3>
<p>I&rsquo;ve decided to provide two sets of results - for overall score and score without installation evaluation for cases where the long-term use is more important than the time to get it running.</p>
<p>Bear in mind that this study only affected the general light-use cases of daily software consumption. Therefore, I won&rsquo;t give any further opinions as to why one is better than the other, and vice versa, since my personal insights may not be consistent with the general needs of everyday users.</p>
<p>The key problem tends to be a basic UX of software installation and management. Proper section naming or broader default repositories would solve most of the problems that day-to-day users are facing if sticking to Linux.</p>
<p>A thought regarding Windows/macOS score - there may be some issues with the way the test was designed, but I think that these results are pretty accurate.</p>
<h4 id="overall-score">Overall Score</h4>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><strong>Distro/OS</strong></th>
          <th style="text-align: center"><strong>Version</strong></th>
          <th style="text-align: center"><strong>DE</strong></th>
          <th style="text-align: center"><strong>Score</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">Pop OS</td>
          <td style="text-align: center">20.04</td>
          <td style="text-align: center">Gnome</td>
          <td style="text-align: center">81</td>
      </tr>
      <tr>
          <td style="text-align: center">Mint</td>
          <td style="text-align: center">19.3</td>
          <td style="text-align: center">Cinnamon</td>
          <td style="text-align: center">75</td>
      </tr>
      <tr>
          <td style="text-align: center">Ubuntu</td>
          <td style="text-align: center">20.04</td>
          <td style="text-align: center">Gnome</td>
          <td style="text-align: center">72</td>
      </tr>
      <tr>
          <td style="text-align: center">Deepin</td>
          <td style="text-align: center">20 Beta</td>
          <td style="text-align: center">Deepin</td>
          <td style="text-align: center">71</td>
      </tr>
      <tr>
          <td style="text-align: center">Zorin</td>
          <td style="text-align: center">15.2</td>
          <td style="text-align: center">Gnome</td>
          <td style="text-align: center">65</td>
      </tr>
      <tr>
          <td style="text-align: center">Manjaro</td>
          <td style="text-align: center">20</td>
          <td style="text-align: center">XFCE</td>
          <td style="text-align: center">61</td>
      </tr>
      <tr>
          <td style="text-align: center">MacOS</td>
          <td style="text-align: center">Catalina</td>
          <td style="text-align: center">Default</td>
          <td style="text-align: center">59</td>
      </tr>
      <tr>
          <td style="text-align: center">Regolith</td>
          <td style="text-align: center">1.4</td>
          <td style="text-align: center">i3</td>
          <td style="text-align: center">58</td>
      </tr>
      <tr>
          <td style="text-align: center">Elementary</td>
          <td style="text-align: center">5.1.3</td>
          <td style="text-align: center">Pantheon</td>
          <td style="text-align: center">54</td>
      </tr>
      <tr>
          <td style="text-align: center">KDE Neon</td>
          <td style="text-align: center">20200430</td>
          <td style="text-align: center">KDE</td>
          <td style="text-align: center">53</td>
      </tr>
      <tr>
          <td style="text-align: center">Windows</td>
          <td style="text-align: center">10</td>
          <td style="text-align: center">Default</td>
          <td style="text-align: center">53</td>
      </tr>
      <tr>
          <td style="text-align: center">Debian</td>
          <td style="text-align: center">10.3</td>
          <td style="text-align: center">Gnome</td>
          <td style="text-align: center">53</td>
      </tr>
      <tr>
          <td style="text-align: center">Solus</td>
          <td style="text-align: center">4.1</td>
          <td style="text-align: center">Budgie</td>
          <td style="text-align: center">51</td>
      </tr>
      <tr>
          <td style="text-align: center">Fedora</td>
          <td style="text-align: center">32</td>
          <td style="text-align: center">Gnome</td>
          <td style="text-align: center">50</td>
      </tr>
      <tr>
          <td style="text-align: center">PCLinuxOS</td>
          <td style="text-align: center">2020.03</td>
          <td style="text-align: center">MATE</td>
          <td style="text-align: center">42</td>
      </tr>
      <tr>
          <td style="text-align: center">MX Linux</td>
          <td style="text-align: center">19.1</td>
          <td style="text-align: center">XFCE</td>
          <td style="text-align: center">34</td>
      </tr>
      <tr>
          <td style="text-align: center">ArcoLinux</td>
          <td style="text-align: center">20.4.11</td>
          <td style="text-align: center">XFCE</td>
          <td style="text-align: center">15</td>
      </tr>
      <tr>
          <td style="text-align: center">CentOS</td>
          <td style="text-align: center">8.1</td>
          <td style="text-align: center">Gnome</td>
          <td style="text-align: center">12</td>
      </tr>
      <tr>
          <td style="text-align: center">Kali</td>
          <td style="text-align: center">2020.1b</td>
          <td style="text-align: center">Gnome</td>
          <td style="text-align: center">10</td>
      </tr>
      <tr>
          <td style="text-align: center">openSUSE</td>
          <td style="text-align: center">15.1</td>
          <td style="text-align: center">Gnome</td>
          <td style="text-align: center">5</td>
      </tr>
      <tr>
          <td style="text-align: center">Antix</td>
          <td style="text-align: center">19</td>
          <td style="text-align: center">Fluxbox</td>
          <td style="text-align: center">0</td>
      </tr>
      <tr>
          <td style="text-align: center">ReactOS</td>
          <td style="text-align: center">0.4.13</td>
          <td style="text-align: center">Explorer</td>
          <td style="text-align: center">0</td>
      </tr>
  </tbody>
</table>
<h4 id="overall-score-without-install">Overall Score Without Install</h4>
<table>
  <thead>
      <tr>
          <th style="text-align: center"><strong>Distro/OS</strong></th>
          <th style="text-align: center"><strong>Version</strong></th>
          <th style="text-align: center"><strong>DE</strong></th>
          <th style="text-align: center"><strong>Score</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td style="text-align: center">Deepin</td>
          <td style="text-align: center">20 Beta</td>
          <td style="text-align: center">Deepin</td>
          <td style="text-align: center">64</td>
      </tr>
      <tr>
          <td style="text-align: center">Pop OS</td>
          <td style="text-align: center">20.04</td>
          <td style="text-align: center">Gnome</td>
          <td style="text-align: center">61</td>
      </tr>
      <tr>
          <td style="text-align: center">Ubuntu</td>
          <td style="text-align: center">20.04</td>
          <td style="text-align: center">Gnome</td>
          <td style="text-align: center">61</td>
      </tr>
      <tr>
          <td style="text-align: center">Zorin</td>
          <td style="text-align: center">15.2</td>
          <td style="text-align: center">Gnome</td>
          <td style="text-align: center">56</td>
      </tr>
      <tr>
          <td style="text-align: center">MacOS</td>
          <td style="text-align: center">Catalina</td>
          <td style="text-align: center">Default</td>
          <td style="text-align: center">56</td>
      </tr>
      <tr>
          <td style="text-align: center">Mint</td>
          <td style="text-align: center">19.3</td>
          <td style="text-align: center">Cinnamon</td>
          <td style="text-align: center">53</td>
      </tr>
      <tr>
          <td style="text-align: center">Windows</td>
          <td style="text-align: center">10</td>
          <td style="text-align: center">Default</td>
          <td style="text-align: center">49</td>
      </tr>
      <tr>
          <td style="text-align: center">KDE Neon</td>
          <td style="text-align: center">20200430</td>
          <td style="text-align: center">KDE</td>
          <td style="text-align: center">47</td>
      </tr>
      <tr>
          <td style="text-align: center">Debian</td>
          <td style="text-align: center">10.3</td>
          <td style="text-align: center">Gnome</td>
          <td style="text-align: center">45</td>
      </tr>
      <tr>
          <td style="text-align: center">Manjaro</td>
          <td style="text-align: center">20</td>
          <td style="text-align: center">XFCE</td>
          <td style="text-align: center">42</td>
      </tr>
      <tr>
          <td style="text-align: center">Regolith</td>
          <td style="text-align: center">1.4</td>
          <td style="text-align: center">i3</td>
          <td style="text-align: center">37</td>
      </tr>
      <tr>
          <td style="text-align: center">Solus</td>
          <td style="text-align: center">4.1</td>
          <td style="text-align: center">Budgie</td>
          <td style="text-align: center">37</td>
      </tr>
      <tr>
          <td style="text-align: center">Elementary</td>
          <td style="text-align: center">5.1.3</td>
          <td style="text-align: center">Pantheon</td>
          <td style="text-align: center">36</td>
      </tr>
      <tr>
          <td style="text-align: center">Fedora</td>
          <td style="text-align: center">32</td>
          <td style="text-align: center">Gnome</td>
          <td style="text-align: center">33</td>
      </tr>
      <tr>
          <td style="text-align: center">PCLinuxOS</td>
          <td style="text-align: center">2020.03</td>
          <td style="text-align: center">MATE</td>
          <td style="text-align: center">26</td>
      </tr>
      <tr>
          <td style="text-align: center">MX Linux</td>
          <td style="text-align: center">19.1</td>
          <td style="text-align: center">XFCE</td>
          <td style="text-align: center">21</td>
      </tr>
      <tr>
          <td style="text-align: center">ArcoLinux</td>
          <td style="text-align: center">20.4.11</td>
          <td style="text-align: center">XFCE</td>
          <td style="text-align: center">0</td>
      </tr>
      <tr>
          <td style="text-align: center">CentOS</td>
          <td style="text-align: center">8.1</td>
          <td style="text-align: center">Gnome</td>
          <td style="text-align: center">0</td>
      </tr>
      <tr>
          <td style="text-align: center">Kali</td>
          <td style="text-align: center">2020.1b</td>
          <td style="text-align: center">Gnome</td>
          <td style="text-align: center">0</td>
      </tr>
      <tr>
          <td style="text-align: center">openSUSE</td>
          <td style="text-align: center">15.1</td>
          <td style="text-align: center">Gnome</td>
          <td style="text-align: center">0</td>
      </tr>
      <tr>
          <td style="text-align: center">Antix</td>
          <td style="text-align: center">19</td>
          <td style="text-align: center">Fluxbox</td>
          <td style="text-align: center">0</td>
      </tr>
      <tr>
          <td style="text-align: center">ReactOS</td>
          <td style="text-align: center">0.4.13</td>
          <td style="text-align: center">Explorer</td>
          <td style="text-align: center">0</td>
      </tr>
  </tbody>
</table>
<p> <a href="/assets/posts/state-of-linux-usability-2020-raw-data.pdf"
    title="Raw Results"
   
target="_blank" rel="noopener">Raw Results</a>
</p>
<h3 id="final-thoughts">Final Thoughts</h3>
<p>In the end, one of the key reasons why we have a handful of Linux distributions available is that each of them is designed to solve a specific problem and serves a specific function for a specific audience.</p>
<p>A lot of multi-purpose distros aim to be Windows-like or MacOS-like to ease the Linux transition. Although there&rsquo;s nothing wrong with it, the true strength of Linux lies in its uniqueness, not its similarity, and I think that this uniqueness could be &ldquo;sold&rdquo; to the consumer as long as it&rsquo;s presented/explained correctly (i.e. recall your first move from Symbian to Android or Windows to macOS).</p>
<p>However, I do believe that the Linux ecosystem as a whole is certainly ready to replace Windows / macOS not just for day-to-day users, but also for educational/corporate/government use.</p>
<p>On a final note, I&rsquo;d like to encourage you to try using a Linux system for a week and if you&rsquo;re already a Linux user then go and make someone else use Linux for a week!</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-bash" data-lang="bash"><span style="display:flex;"><span>$ echo <span style="color:#e6db74">&#39;#preachingpenguins&#39;</span>
</span></span><span style="display:flex;"><span>$ exit
</span></span></code></pre></div>]]></content:encoded></item><item><title>Your Team Isn't Remote. It's Just on Zoom.</title><link>https://lawzava.com/blog/2020-04-13-async-communication-practices/</link><pubDate>Mon, 13 Apr 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-04-13-async-communication-practices/</guid><description>Most teams claiming to work remotely are just recreating the office over video calls. Async communication is the actual unlock, and almost nobody is doing it right.</description><content:encoded><![CDATA[<p>Most companies didn&rsquo;t go remote this year. They went to Zoom jail.</p>
<p>I&rsquo;ve been running Decloud as a remote-first company for a while now. When the pandemic hit, I watched every company around us scramble to &ldquo;go remote&rdquo; and immediately do it wrong. Their playbook was simple: take every in-person meeting, slap it on a video call, and declare victory. Then wonder why everyone is exhausted by 3pm.</p>
<p>That&rsquo;s not remote work. That&rsquo;s the office with worse lighting.</p>
<h2 id="the-point-is-async">The point is async</h2>
<p>Remote work&rsquo;s real advantage isn&rsquo;t location flexibility. It&rsquo;s time flexibility. Your engineers can do deep work when their brain is on fire instead of when the calendar says so. But that only works if your default communication mode is async.</p>
<p>Async means I write something clear enough that you can read it, understand it, and act on it &ndash; without needing me online at the same time. That&rsquo;s it. Not complicated in theory. Apparently very complicated in practice.</p>
<h2 id="why-most-teams-get-this-wrong">Why most teams get this wrong</h2>
<p>Because writing is harder than talking. In a meeting, you can ramble for ten minutes and people nod along. In async, you actually have to think before you communicate. You have to structure your thoughts. You have to anticipate questions.</p>
<p>Most people have never been asked to do this at work. So they default to what&rsquo;s easy: &ldquo;Hey, got a minute for a quick call?&rdquo;</p>
<p>No. I don&rsquo;t. And neither does any engineer in the middle of debugging a production issue.</p>
<h2 id="what-actually-works">What actually works</h2>
<p>At Decloud, we have a few rules that keep async working:</p>
<p><strong>Write complete thoughts.</strong> Don&rsquo;t send &ldquo;hey&rdquo; and wait. Don&rsquo;t drip-feed context across fifteen messages. One message. Full context. Clear ask. Done.</p>
<p><strong>Decisions go in writing.</strong> If it happened in a call and nobody wrote it down, it didn&rsquo;t happen. We write short decision docs &ndash; what we decided, why, what we considered and rejected. Takes five minutes. Saves weeks of &ldquo;wait, I thought we agreed to&hellip;&rdquo;</p>
<p><strong>Response times are explicit.</strong> Chat gets same-day responses. PR reviews within 24 hours. Decision docs get a couple of days. On-call stuff is immediate. No ambiguity.</p>
<p><strong>Meetings are the exception.</strong> We use sync time for things that are genuinely hard to resolve in text &ndash; conflict, brainstorming, incident response. Not for status updates. Never for status updates. Write those down and give people their mornings back.</p>
<h2 id="the-hard-part">The hard part</h2>
<p>Async requires trust. You have to trust that someone not responding immediately is working, not slacking. You have to judge people by what they ship, not by their green dot in Slack.</p>
<p>Most managers aren&rsquo;t ready for that. They&rsquo;ve spent their careers managing by presence. Seeing butts in seats. Remote forces a reckoning with that, and a lot of them are responding by demanding cameras-on all day. Which is somehow worse than the office.</p>
<p>If you&rsquo;re a leader and your reaction to remote work was &ldquo;more meetings,&rdquo; you missed the point entirely. The unlock is fewer interruptions, better writing, and trusting your team to be adults.</p>
<p>Stop recreating the office on Zoom. Start writing things down.</p>
]]></content:encoded></item><item><title>Your Business Continuity Plan Is Corporate Theater</title><link>https://lawzava.com/blog/2020-04-06-business-continuity-engineering/</link><pubDate>Mon, 06 Apr 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-04-06-business-continuity-engineering/</guid><description>Most BCP documents are shelf-ware written by consultants. Here&amp;amp;rsquo;s what actually keeps engineering teams running when everything breaks.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Most business continuity plans are useless binder filler. Real continuity is about people, not servers. Know who can do what when half your team is gone, kill your single points of failure, and actually practice the plan. The 50-page PDF nobody reads won&rsquo;t save you.</p>
<hr>
<p>It&rsquo;s April 2020 and the world is on fire. COVID just stress-tested every company&rsquo;s business continuity plan, and the results are&hellip; not great.</p>
<p>I&rsquo;ve been through a few versions of this movie. At the fintech startup we had engineers across multiple countries, so the &ldquo;what if someone disappears&rdquo; scenario wasn&rsquo;t hypothetical. At a mobility startup we had hardware in the field and a tiny team. I&rsquo;ve read more BCP documents than any human should. Most of them are garbage.</p>
<p>The typical BCP is written by a management consultant who has never ssh&rsquo;d into a production server. It&rsquo;s 40 pages of org charts and escalation matrices living in a SharePoint folder nobody remembers. When the actual crisis hits, people ignore it and open Slack.</p>
<p>Let me talk about what actually works. From an engineering perspective. Over coffee, not a boardroom.</p>
<h2 id="continuity-is-about-people-not-systems">Continuity is about people, not systems</h2>
<p>Disaster recovery and business continuity are different things. DR is &ldquo;the database is gone, restore from backup.&rdquo; BCP is &ldquo;three of your five engineers are sick, the office is closed, and your biggest client needs a deployment by Friday.&rdquo;</p>
<p>The second one is harder. Way harder.</p>
<p>Most engineering teams have never seriously asked: if person X gets hit by a bus tomorrow, what breaks? Not the servers. The <em>knowledge</em>. The person who knows why that cron job runs at 3am. The person with the credentials to the legacy payment gateway. The person who understands what happens if you flip that config flag.</p>
<p>I&rsquo;ve seen teams lose weeks because one person went on vacation and nobody else knew how to deploy to staging. Vacation. Not a pandemic. Vacation.</p>
<h2 id="kill-your-single-points-of-failure">Kill your single points of failure</h2>
<p>You already know about redundant servers and multi-region deployments. I&rsquo;m talking about the <em>human</em> single points of failure that every team pretends don&rsquo;t exist.</p>
<p>Audit these right now:</p>
<ul>
<li><strong>The deploy gatekeeper.</strong> Only one person can push to production? Fix it today.</li>
<li><strong>The credentials hoarder.</strong> Critical passwords live in one person&rsquo;s head or their personal 1Password vault? You&rsquo;re one resignation away from a lockout.</li>
<li><strong>The tribal knowledge holder.</strong> That engineer who built the system three years ago and never documented anything? Pair someone with them. This week.</li>
<li><strong>The single VPN.</strong> Your whole remote access goes through one gateway? Add another one and actually test the failover.</li>
</ul>
<p>None of this is glamorous. Nobody gets promoted for writing runbooks. But it&rsquo;s the difference between &ldquo;we handled it&rdquo; and &ldquo;we lost a week.&rdquo;</p>
<h2 id="documentation-as-a-survival-tool">Documentation as a survival tool</h2>
<p>I know, I know. Engineers hate writing docs. I hate writing docs. But documentation isn&rsquo;t a compliance artifact. It&rsquo;s a survival tool. You&rsquo;re writing it for the panicked version of your teammate at 2am who can&rsquo;t reach you.</p>
<p>Focus on three things:</p>
<ol>
<li><strong>Runbooks for the scary stuff.</strong> Not &ldquo;how to use git.&rdquo; How to restore the database. How to roll back a bad deploy. How to rotate compromised credentials. The stuff you can&rsquo;t afford to figure out in real time.</li>
<li><strong>Access maps.</strong> Who has access to what, and how does someone else get it if that person is gone. Sounds basic. Most teams can&rsquo;t answer it completely.</li>
<li><strong>Decision records.</strong> Why did we pick Postgres over Mongo? Why is this service deployed separately? When the person who made the call leaves, the context leaves with them. Write it down.</li>
</ol>
<p>Cross-training is the other half. At the fintech startup we made it a rule that at least two people could handle any critical system. We rotated on-call and deployments so the knowledge was real, not theoretical.</p>
<h2 id="remote-readiness-is-continuity">Remote-readiness is continuity</h2>
<p>Three weeks ago, companies were debating whether engineers could work from home. Now everyone&rsquo;s remote and half the VPNs are melting.</p>
<p>If your CI/CD pipeline requires someone to be physically in the office, that&rsquo;s a continuity failure. If your auth depends on the office network, continuity failure. If your docs live behind a VPN that handles 20 concurrent connections and you have 200 engineers&hellip; you get it.</p>
<p>The fix isn&rsquo;t complicated. Cloud CI/CD. Zero-trust auth. Accessible docs. Most modern teams have this already. But &ldquo;most modern teams&rdquo; is a smaller group than the industry likes to admit.</p>
<h2 id="know-your-tiers">Know your tiers</h2>
<p>When capacity drops — and it will — you need to already know what matters. Don&rsquo;t figure this out during the crisis.</p>
<p><strong>Keep alive at all costs:</strong> Production uptime. Security incident response. Data integrity and backups.</p>
<p><strong>Important but can flex:</strong> Bug fixes for paying customers. Billing. Support tooling.</p>
<p><strong>Can wait:</strong> New features. Refactors. That Kubernetes migration you&rsquo;ve been planning.</p>
<p>Have this conversation with product and leadership <em>before</em> things go sideways. In crisis mode, every stakeholder thinks their thing is critical. Set the tiers in advance. Get sign-off. Write it down.</p>
<h2 id="your-vendors-are-your-problem">Your vendors are your problem</h2>
<p>If Stripe goes down, your checkout goes down. That&rsquo;s your problem, not Stripe&rsquo;s. Customers don&rsquo;t care about your vendor&rsquo;s SLA.</p>
<p>For every critical vendor: Can you degrade gracefully? Can you queue and retry? Is there a fallback, even a manual one?</p>
<p>You don&rsquo;t need a hot standby for every service. But you need to have <em>thought about it</em>. &ldquo;We&rsquo;ll figure it out&rdquo; isn&rsquo;t a plan.</p>
<h2 id="practice-or-it-doesnt-count">Practice or it doesn&rsquo;t count</h2>
<p>The plan that&rsquo;s never been tested is just a wish. I&rsquo;ve seen beautifully written BCP documents fall apart the first time someone tried to follow them. Steps were wrong. Access had changed. The backup contact had left the company six months ago.</p>
<p>Run drills. Restore from backup and time it. Do a deploy from home on a random Tuesday. Simulate a vendor outage. Even a 30-minute tabletop exercise (&ldquo;okay, AWS us-east-1 is down, what do we do?&rdquo;) will expose gaps you didn&rsquo;t know about.</p>
<p>When you find gaps — and you will — fix them with specific action items, owners, and deadlines. Not &ldquo;we should improve this.&rdquo; That means nothing.</p>
<h2 id="the-real-talk">The real talk</h2>
<p>Business continuity isn&rsquo;t a document. It&rsquo;s a muscle. You build it by doing the boring work: writing runbooks, rotating knowledge, testing backups, having the uncomfortable conversation about what happens when key people are unavailable.</p>
<p>The companies handling this pandemic well aren&rsquo;t the ones with the best BCP binders. They&rsquo;re the ones who already operated like things could break at any time. Because things always break. The timing is the only surprise.</p>
]]></content:encoded></item><item><title>Your Team Just Went Remote. Here's What to Do Right Now.</title><link>https://lawzava.com/blog/2020-03-16-rapid-remote-work-transition/</link><pubDate>Mon, 16 Mar 2020 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2020-03-16-rapid-remote-work-transition/</guid><description>COVID forced your engineering team remote overnight. Here&amp;amp;rsquo;s the no-fluff version of what actually matters in the first two weeks.</description><content:encoded><![CDATA[<p>Half the CTOs I know are scrambling right now. Offices closed Friday, everyone&rsquo;s home Monday, and nobody has a plan. I&rsquo;ve been fielding calls all weekend.</p>
<p>Here&rsquo;s the thing &ndash; at  <a href="https://decloud.io"
   
   
target="_blank" rel="noopener">Decloud</a>
, we&rsquo;ve been fully remote since day one. Not as a pandemic response. By design. So I&rsquo;m not theorizing here. This is what actually works.</p>
<p>You don&rsquo;t need a 40-page remote work policy. You need to unblock your team this week and set three or four norms that prevent chaos. That&rsquo;s it.</p>
<h2 id="week-one-just-unblock-people">Week one: just unblock people</h2>
<p>Nothing else matters if your engineers can&rsquo;t access the systems they need from home. This is your only priority for the first few days.</p>
<ul>
<li><strong>VPN or zero-trust access</strong> &ndash; test it. Half your team has never used it outside the office.</li>
<li><strong>MFA on everything critical.</strong> If you haven&rsquo;t done this yet, now&rsquo;s the time. Security doesn&rsquo;t get a pass because things are hectic.</li>
<li><strong>Make sure people have decent hardware.</strong> Laptop, headset, stable internet. Ship monitors to anyone who needs one. Don&rsquo;t be cheap about this &ndash; a $300 monitor pays for itself in a week of productivity.</li>
<li><strong>Set up a single support channel</strong> for &ldquo;I can&rsquo;t access X&rdquo; problems. Staff it. The first week will be a flood of small blockers. Clear them fast.</li>
</ul>
<h2 id="set-three-norms-and-actually-enforce-them">Set three norms and actually enforce them</h2>
<p>You don&rsquo;t need a communication manifesto. You need these three things:</p>
<p><strong>1. Async by default.</strong> Write your updates so someone in a different timezone (or just eating lunch) can read them and act without scheduling a call. A daily standup message in Slack is worth more than a 30-minute video call where half the team zones out.</p>
<p><strong>2. Meetings need agendas.</strong> No agenda, no meeting. This was good practice before. Now it&rsquo;s survival. I&rsquo;ve watched teams fill every hour with Zoom calls within three days of going remote. The result is zero deep work and everyone&rsquo;s exhausted by Wednesday.</p>
<p><strong>3. It&rsquo;s okay to not respond instantly.</strong> This is the one most managers get wrong. They see someone&rsquo;s Slack dot go grey and panic. Stop. Measure output, not presence. If your engineers are shipping, leave them alone.</p>
<h2 id="the-stuff-people-forget">The stuff people forget</h2>
<p><strong>PRs become your main communication channel.</strong> Treat them that way. Good descriptions, clear context on <em>why</em> something changed, how to test it. Hallway conversations don&rsquo;t exist anymore &ndash; the PR is where knowledge transfer happens now.</p>
<p><strong>People will burn out faster than you expect.</strong> Working from home isn&rsquo;t a vacation. The boundary between work and life evaporates. I&rsquo;ve seen it. Encourage your team to set hard stop times. Lead by example &ndash; don&rsquo;t send messages at midnight and expect nobody to notice.</p>
<p><strong>New hires are the most vulnerable.</strong> They can&rsquo;t lean over and ask someone a question. Assign them a buddy. Give them a small, well-scoped first task. Check in daily for the first two weeks. This is where remote-first companies either win or lose.</p>
<h2 id="one-more-thing">One more thing</h2>
<p>Stop trying to recreate the office over Zoom. You&rsquo;re not going to replicate the whiteboard or the water cooler. That&rsquo;s fine. Remote work is a different mode, not a worse one.</p>
<p>The teams that accept this and build new habits around async communication, written decisions, and trust-based management will come out of this stronger. The teams that spend all day on video calls trying to simulate an open floor plan will burn out in a month.</p>
<p>I&rsquo;ve been doing this for years. It works. But only if you commit to it instead of treating it as a temporary inconvenience.</p>
<p>Got questions?  <a href="/contact"
   
   >Reach out.</a>
 Happy to help.</p>
]]></content:encoded></item><item><title>Your Onboarding Is Broken and Everyone Knows It</title><link>https://lawzava.com/blog/2019-10-07-engineering-onboarding/</link><pubDate>Mon, 07 Oct 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-10-07-engineering-onboarding/</guid><description>Most engineering onboarding is a polite abandonment ritual. What I&amp;amp;rsquo;ve learned across three startups about getting new engineers shipping fast.</description><content:encoded><![CDATA[<p>My first day at a fintech startup, I was handed a laptop with Ubuntu pre-installed, pointed at a Confluence page titled &ldquo;Getting Started (OUTDATED - see Dave)&rdquo;, and told Dave was on holiday. The Confluence page referenced a Docker Compose setup that hadn&rsquo;t worked since the previous quarter. By 4pm I&rsquo;d cloned six repos, broken my local Postgres twice, and shipped exactly nothing.</p>
<p>I became CTO of that company. The fintech startup. And the first thing I fixed was onboarding.</p>
<p>Not because it was the sexiest problem. Because it was the most expensive one nobody was measuring.</p>
<h2 id="the-real-cost-nobody-tracks">The Real Cost Nobody Tracks</h2>
<p>When I was building Decloud during my founder-program cohort in 2019, every week a new engineer spent confused was a week we didn&rsquo;t have. At a startup, a slow ramp-up isn&rsquo;t an inconvenience. It&rsquo;s an existential threat. But even at larger places, the math is brutal: a senior engineer who takes eight weeks to ship independently instead of three just cost you five weeks of salary for zero output. Multiply that by every hire per year.</p>
<p>The usual excuse is &ldquo;our codebase is complex.&rdquo; No. Your onboarding is lazy. Complexity is a reason to invest <em>more</em> in onboarding, not less.</p>
<h2 id="what-good-actually-looks-like">What &ldquo;Good&rdquo; Actually Looks Like</h2>
<p>I&rsquo;ve onboarded engineers at the fintech startup, at a mobility startup, and at Decloud. Different products, different stacks, different countries. The pattern that works is boringly consistent.</p>
<p><strong>Day one: the new engineer opens a PR.</strong> Not a meaningful one. Not a feature. A typo fix, a config tweak, a test improvement. The point is they&rsquo;ve touched the full cycle &ndash; clone, build, branch, change, test, review, merge, deploy &ndash; before they go home. If your setup takes longer than a morning, it&rsquo;s broken.</p>
<p><strong>Week one: they own a real bug fix.</strong> Something with a clear repro, a limited blast radius, and enough context that they learn the codebase by reading the code that matters, not by reading a stale architecture doc.</p>
<p><strong>Month one: they&rsquo;re independently picking up work.</strong> They know who to ask, where the bodies are buried, and how deploys actually work (not how the wiki says they work).</p>
<p>That&rsquo;s it. No elaborate 90-day plans. No onboarding &ldquo;program&rdquo; with a PowerPoint deck. Just a tight loop of ship, learn, ship.</p>
<h2 id="make-setup-or-go-home"><code>make setup</code> Or Go Home</h2>
<p>At the mobility startup, we ran Go services talking to a handful of microservices. When I joined (before eventually leading the engineering), the local dev setup involved seventeen manual steps spread across a README that contradicted itself in three places. I rewrote it as a Makefile.</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-bash" data-lang="bash"><span style="display:flex;"><span>git clone git@github.com:company/app.git
</span></span><span style="display:flex;"><span>make setup
</span></span><span style="display:flex;"><span>make dev
</span></span></code></pre></div><p>Three commands. If it takes more than three commands to go from zero to a running local environment, you have a setup problem, not a complexity problem. Script it, containerize the dependencies, seed the database automatically. The new hire&rsquo;s laptop is a test of your infrastructure&rsquo;s maturity. If you can&rsquo;t reproduce your dev environment reliably, what makes you think production is any better?</p>
<p>When I set up the dev environment at Decloud, I literally timed it. Fifteen minutes from <code>git clone</code> to running tests. If it crept past that, we treated it as a bug.</p>
<h2 id="the-buddy-system-done-right">The Buddy System (Done Right)</h2>
<p>Every new engineer gets a buddy. Not their manager. Not the tech lead. Someone close enough to the codebase to answer &ldquo;where does this thing live?&rdquo; questions but not so senior that the new hire feels stupid asking.</p>
<p>The buddy&rsquo;s job is simple: be interruptible for the first two weeks. That&rsquo;s the whole job description. They don&rsquo;t need to teach. They need to unblock.</p>
<p>At the fintech startup, I paired each new hire with whoever had last touched the area of code they&rsquo;d be working in. Not because that person was the best teacher, but because they had the freshest context. Onboarding is a transfer of context, not knowledge. The knowledge is in the code. The context &ndash; why this weird pattern exists, which service is actually deprecated but still running, where the real config lives &ndash; that&rsquo;s in people&rsquo;s heads.</p>
<h2 id="starter-tasks-that-arent-insulting">Starter Tasks That Aren&rsquo;t Insulting</h2>
<p>The worst onboarding pattern is the &ldquo;starter project&rdquo; that&rsquo;s actually a garbage task nobody else wanted to do. New engineers can smell it. It tells them their time isn&rsquo;t valued.</p>
<p>Good starter work has three properties:</p>
<ol>
<li><strong>It touches production.</strong> The whole point is building confidence that they can ship safely.</li>
<li><strong>It has a clear definition of done.</strong> Not &ldquo;explore the payment service and propose improvements.&rdquo; That&rsquo;s a research project disguised as onboarding.</li>
<li><strong>It teaches something specific about your system.</strong> A bug in the notification service teaches them about the event pipeline. An error message improvement teaches them about the API contract.</li>
</ol>
<p>At Decloud, I kept a backlog of tagged issues specifically for this. Not fake work. Real bugs and improvements that were scoped to one or two days and came with enough context that someone new could pick them up without a forty-minute walkthrough.</p>
<h2 id="documentation-less-is-more">Documentation: Less Is More</h2>
<p>I&rsquo;ve seen onboarding wikis with fifty pages of architecture history, org charts, and &ldquo;philosophy&rdquo; documents. Nobody reads them. The new hire skims the first two, gets overwhelmed, and asks their buddy anyway.</p>
<p>Here&rsquo;s what new engineers actually need written down:</p>
<ul>
<li>How to set up their machine (the Makefile)</li>
<li>How to deploy and how to roll back</li>
<li>How to ask for help and who reviews what</li>
<li>A one-page system overview with a data flow diagram</li>
</ul>
<p>That&rsquo;s four documents. Keep them short. Update them every time someone joins and finds something wrong. The onboarding docs should be the most maintained docs in your entire company, because they&rsquo;re the only ones that get tested regularly by people who have no prior context.</p>
<h2 id="the-feedback-loop-most-teams-skip">The Feedback Loop Most Teams Skip</h2>
<p>Here&rsquo;s the thing that separates decent onboarding from great onboarding: you actually ask the new hire what was broken.</p>
<p>Every new engineer who joins should file at least one issue against the onboarding process itself. Not as a suggestion. As a requirement. &ldquo;What was confusing? What was wrong? What did you have to ask someone that should have been written down?&rdquo;</p>
<p>At the fintech startup, we turned this into a standing item in the 30-day check-in. Every single round of onboarding made the next one better. By the time I left, the setup was tight enough that contractors could be productive on day two.</p>
<h2 id="quick-take">Quick take</h2>
<p>Stop treating onboarding as &ldquo;show them the wiki and hope for the best.&rdquo; Script the dev setup to three commands. Pair them with a buddy who has fresh context. Give them real work that touches production in week one. Then ask them what was broken and fix it. Repeat until your onboarding is your competitive advantage, not your embarrassment.</p>
]]></content:encoded></item><item><title>Data Mesh Is an Org Chart Fix, Not a Tech One</title><link>https://lawzava.com/blog/2019-07-29-data-mesh-principles/</link><pubDate>Mon, 29 Jul 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-07-29-data-mesh-principles/</guid><description>Most data problems are ownership problems. Data mesh gets that right. But adopting it as an architecture diagram exercise misses the point entirely.</description><content:encoded><![CDATA[<p>At the fintech startup, we ingested financial data from dozens of sources. News feeds, market data, alternative data providers. The pipelines were my problem as CTO. Every schema change, every quality issue, every &ldquo;why is this number wrong&rdquo; question funneled through the same two people.</p>
<p>That doesn&rsquo;t scale. It barely functions.</p>
<p>Data mesh, as Zhamak Dehghani describes it, is the first framework I&rsquo;ve seen that names the actual disease instead of treating symptoms. The disease is centralized ownership of something that&rsquo;s inherently distributed.</p>
<h2 id="the-real-principle">The real principle</h2>
<p>Data mesh has four pillars. Domain ownership. Data as a product. Self-serve platform. Federated governance. You can read the theory anywhere. Here is what actually matters.</p>
<p><strong>The people who produce data must own it.</strong> Not &ldquo;be consulted about it.&rdquo; Own it. Define its schema. Guarantee its freshness. Document what it means. Support the teams that consume it.</p>
<p>Everything else is implementation detail.</p>
<h2 id="why-this-is-hard">Why this is hard</h2>
<p>Most organizations resist this because it means domain teams need data engineering skills. That&rsquo;s expensive. It&rsquo;s also the only thing that works past a certain scale.</p>
<p>I watched this play out firsthand. At the fintech startup, our financial data pipelines broke constantly at domain boundaries. The market data team understood tick data. The NLP team understood sentiment scores. The central pipeline team understood neither. They just glued CSVs together and prayed.</p>
<p>The fix wasn&rsquo;t better tooling. It was making each team responsible for publishing clean, documented, versioned data products. The moment producers had skin in the game, quality improved overnight.</p>
<h2 id="where-people-get-this-wrong">Where people get this wrong</h2>
<p>Three failure modes I keep seeing.</p>
<p><strong>Renaming the central team.</strong> Calling your existing data warehouse team a &ldquo;platform team&rdquo; changes nothing. If every pipeline still routes through them, you haven&rsquo;t decentralized. You have rebranded.</p>
<p><strong>Contracts without enforcement.</strong> A YAML file describing your data product is worthless if nobody checks freshness, schema compatibility, or whether the docs are current. Contracts need automated checks or they are fiction.</p>
<p><strong>Premature adoption.</strong> If you have one data team serving three consumers, you don&rsquo;t need data mesh. You need that team to do its job well. Data mesh solves scaling problems. If you aren&rsquo;t at scale, you&rsquo;re importing complexity for free.</p>
<h2 id="when-it-fits">When it fits</h2>
<p>You probably need data mesh thinking when requests to your central data team take weeks instead of days. When pipeline incidents trace back to domain knowledge that nobody on the data team has. When producers and consumers of the same dataset have never spoken.</p>
<p>You don&rsquo;t need it if your team is small, your domains are fuzzy, or you&rsquo;re still figuring out what your product even is. I&rsquo;m in a founder-program cohort right now building Decloud from scratch. We don&rsquo;t need data mesh. We need a Postgres table and some discipline.</p>
<h2 id="the-real-blocker">The real blocker</h2>
<p>Data mesh asks domain teams to take on work they didn&rsquo;t sign up for. That&rsquo;s a management conversation, not an architecture decision. If leadership won&rsquo;t staff domain teams with data skills and hold them accountable for data quality, no framework will save you.</p>
<p>The technology is the easy part. Ownership is the hard part. Always has been.</p>
]]></content:encoded></item><item><title>Your Monolith Is Probably Fine</title><link>https://lawzava.com/blog/2019-07-01-microservices-migration-strategy/</link><pubDate>Mon, 01 Jul 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-07-01-microservices-migration-strategy/</guid><description>Most teams shouldn&amp;amp;rsquo;t be migrating to microservices. Here&amp;amp;rsquo;s how to tell if you actually should, and how to do it without wrecking your delivery for eighteen months.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Stop. You probably don&rsquo;t need microservices. If you do, the strangler pattern is the only sane approach, your shared database is the real problem, and any team that can&rsquo;t operate a healthy monolith will absolutely drown in a distributed system.</p>
<p>I&rsquo;ve been building the backend for Decloud in Go since joining a deep-tech founder program earlier this year. Before that I ran engineering at a fintech startup and a mobility startup. Across all three, the microservices question came up. In two of those cases, the correct answer was &ldquo;not yet.&rdquo; Here&rsquo;s what I&rsquo;ve learned about knowing the difference.</p>
<h2 id="do-you-actually-have-a-monolith-problem">Do you actually have a monolith problem?</h2>
<p>Most teams that want microservices have a process problem dressed up as an architecture problem. Slow deploys, blocked releases, painful coordination &ndash; these feel like monolith issues, but usually they&rsquo;re symptoms of missing automation, weak tests, or unclear ownership.</p>
<p>Ask yourself:</p>
<ul>
<li>Are multiple teams genuinely blocked by shared release cycles? Not annoyed. Blocked.</li>
<li>Do different parts of your system need fundamentally different scaling or uptime guarantees?</li>
<li>Do clear product boundaries already exist in your codebase, or are you hoping microservices will create them?</li>
</ul>
<p>If you answered &ldquo;no&rdquo; to most of these, you don&rsquo;t need microservices. You need a better monolith. Invest in CI/CD, write tests, add feature flags, and stop doing synchronized deploys.</p>
<h2 id="the-uncomfortable-prerequisite">The uncomfortable prerequisite</h2>
<p>Here&rsquo;s the part nobody wants to hear. Before you can successfully run microservices, your monolith needs to already be healthy. Specifically:</p>
<ul>
<li>Automated tests that run fast and actually catch regressions</li>
<li>Deploys measured in minutes, not hours</li>
<li>Centralized logging and alerting that someone actually looks at</li>
<li>Clear module boundaries in the codebase</li>
<li>Feature flags for safe releases</li>
</ul>
<p>If you can&rsquo;t do these things with one service, you won&rsquo;t magically do them with twelve. Microservices multiply your operational surface area. Every gap becomes a canyon.</p>
<p>At the fintech startup we had a monolith handling market data, user portfolios, and news aggregation. The temptation to split was strong. But our deploy pipeline was slow and our test coverage had gaps. Splitting would have made both problems worse. We fixed the fundamentals first. Most of the pressure to split evaporated once deploys were fast and modules were cleanly separated.</p>
<h2 id="when-its-actually-time">When it&rsquo;s actually time</h2>
<p>Sometimes it&rsquo;s genuinely time. At Decloud, we&rsquo;re building a cloud infrastructure product where the billing service has completely different scaling characteristics than the provisioning engine. Billing needs to be rock-solid and auditable. Provisioning needs to be fast and horizontally scalable. Different teams own them. That&rsquo;s a real reason to split.</p>
<p>The strangler pattern is the only approach I&rsquo;d recommend:</p>
<pre tabindex="0"><code>Phase 1: All traffic hits the monolith
Phase 2: /billing routes to billing service, everything else stays
Phase 3: Most traffic hits services, small core remains
</code></pre><p>You extract one domain at a time. The rest stays stable. You can reverse any step. Progress is visible. This isn&rsquo;t glamorous work. It takes months. But it&rsquo;s the kind of work that actually ships.</p>
<h2 id="pick-your-first-extraction-carefully">Pick your first extraction carefully</h2>
<p>Your first service should be boring. Not your most critical business logic &ndash; that&rsquo;s your worst option. Pick something that:</p>
<ul>
<li>Has a small API surface</li>
<li>Changes frequently (so you see the benefits fast)</li>
<li>Has a clear owner</li>
<li>Has obvious scaling needs separate from the rest</li>
</ul>
<p>At the mobility startup, if we had gone the microservices route, the GPS tracking ingestion pipeline would have been the obvious first candidate. High throughput, clear boundary, independent scaling needs, and a single team owned it. The user management system? Terrible candidate. It touched everything.</p>
<h2 id="the-database-is-the-real-problem">The database is the real problem</h2>
<p>Everybody focuses on splitting code. The hard part is splitting data.</p>
<p>Shared databases are what turn &ldquo;microservices&rdquo; into a distributed monolith. If two services read from the same table, you haven&rsquo;t decoupled anything. You&rsquo;ve just added network hops to your coupling.</p>
<p>Before you extract a service:</p>
<ol>
<li>Map every table to its consumers. All of them.</li>
<li>Assign a single owner per dataset.</li>
<li>Expose shared data through an API, not a shared connection string.</li>
<li>Run dual-writes temporarily during migration. Emphasis on temporarily.</li>
</ol>
<p>A transitional read view can buy you time:</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:#66d9ef">CREATE</span> <span style="color:#66d9ef">VIEW</span> payments_order_read <span style="color:#66d9ef">AS</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">SELECT</span> id, total_cents, currency, user_id, status
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> orders
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> status <span style="color:#66d9ef">IN</span> (<span style="color:#e6db74">&#39;pending_payment&#39;</span>, <span style="color:#e6db74">&#39;paid&#39;</span>);
</span></span></code></pre></div><p>But set a date to kill it. Transitional things that don&rsquo;t have end dates become permanent things.</p>
<h2 id="integration-keep-it-boring">Integration: keep it boring</h2>
<p>For service-to-service communication, my strong preference is gRPC for synchronous calls and a durable message queue for async. In Go, the gRPC tooling is excellent and the generated clients mean one less thing to get wrong.</p>
<p>A few rules:</p>
<ul>
<li>Every sync call needs a timeout, a retry budget, and a circuit breaker. No exceptions.</li>
<li>Async handlers must be idempotent. You will get duplicate messages. Plan for it.</li>
<li>Request IDs for tracing. Everywhere. Non-negotiable.</li>
</ul>
<p>Skip the complex choreography patterns until your observability is genuinely mature. Start with simple orchestration. You can get clever later. You probably won&rsquo;t need to.</p>
<h2 id="how-you-know-it-worked">How you know it worked</h2>
<p>You&rsquo;ll know the migration is working when deployment frequency goes up per service, not when you have more services. More services with the same deploy cadence just means you added complexity for free.</p>
<p>Track lead time from commit to production. Track how often deploys fail and need rollback. Track incidents caused by cross-service dependencies. If those numbers aren&rsquo;t improving, the migration isn&rsquo;t helping regardless of how clean the architecture diagram looks.</p>
<p>Microservices can genuinely unlock velocity when the conditions are right. But &ldquo;our monolith feels messy&rdquo; isn&rsquo;t one of those conditions. Fix your house before you build an addition.</p>
]]></content:encoded></item><item><title>Your Staging Environment Is Lying to You</title><link>https://lawzava.com/blog/2019-06-03-testing-in-production/</link><pubDate>Mon, 03 Jun 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-06-03-testing-in-production/</guid><description>Staging never catches the real bugs. Here&amp;amp;rsquo;s how I learned to test in production without burning everything down.</description><content:encoded><![CDATA[<p>Two weeks into my founder-program cohort in 2019, I pushed a payments integration for Decloud that had passed every test we had. Unit tests. Integration tests against a sandbox API. Manual QA on staging. All green.</p>
<p>It broke within forty minutes of hitting real users.</p>
<p>The sandbox API returned amounts as integers. The production API returned them as strings. Our staging environment had been confirming our assumptions, not testing our code. That fifteen-minute fire drill taught me more about testing strategy than any conference talk ever will.</p>
<h2 id="staging-is-a-comfortable-lie">Staging is a comfortable lie</h2>
<p>I&rsquo;ve been CTO at a fintech startup, a mobility startup, and now I&rsquo;m building Decloud. Across all of them, the pattern repeats: staging looks just enough like production to give you confidence, but differs in all the ways that actually matter.</p>
<p>Different data shapes. Different load patterns. Different third-party API behaviors. Different timing. Different everything that&rsquo;s hard to fake and easy to ignore.</p>
<p>The bugs that wake you up at 3am are never the ones your test suite catches. They are the ones that only exist when a real user in a real timezone hits a real edge case with real data. Staging can&rsquo;t reproduce that. Full stop.</p>
<p>This isn&rsquo;t an argument against pre-release testing. It&rsquo;s an argument that pre-release testing is necessary but not sufficient. You need to verify behavior where it actually runs.</p>
<h2 id="the-rules-i-follow">The rules I follow</h2>
<p>After burning myself enough times &ndash; at the fintech startup, at the mobility startup, and now at Decloud &ndash; I&rsquo;ve landed on a few non-negotiable principles.</p>
<p><strong>Small blast radius, always.</strong> Start with one percent of traffic. Not ten. Not &ldquo;just the beta users.&rdquo; One percent. If something is broken, one percent is a learning opportunity. Ten percent is an incident. I learned this the hard way at the mobility startup, where a &ldquo;small&rdquo; rollout to a single city still meant thousands of angry riders.</p>
<p><strong>Define success before you ship, not after.</strong> If you can&rsquo;t write down what &ldquo;working&rdquo; looks like in two sentences, you don&rsquo;t understand the change well enough to ship it. Error rate below X. Latency under Y. Conversion not worse than Z. Write it down. Pin it in Slack. Make it boring.</p>
<p><strong>Rollback must be one click.</strong> If reverting a change requires a redeploy, a database migration, or waking someone up, your deployment pipeline isn&rsquo;t ready for production testing. Feature flags make this trivial. We use them for everything at Decloud, even infrastructure changes.</p>
<p><strong>Never create real side effects.</strong> Production tests must not send real emails, charge real cards, or corrupt real analytics. Synthetic users, idempotent operations, and explicit safety checks aren&rsquo;t optional. They are the whole point.</p>
<h2 id="how-i-actually-do-it">How I actually do it</h2>
<h3 id="feature-flags-and-progressive-rollout">Feature flags and progressive rollout</h3>
<p>This is the bread and butter. Ship the code dark, then turn it on for a sliver of traffic.</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">handleCheckout</span>(<span style="color:#a6e22e">user</span> <span style="color:#a6e22e">User</span>) <span style="color:#a6e22e">Response</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#a6e22e">featureFlags</span>.<span style="color:#a6e22e">Enabled</span>(<span style="color:#e6db74">&#34;new_checkout&#34;</span>, <span style="color:#a6e22e">user</span>.<span style="color:#a6e22e">ID</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">newCheckout</span>(<span style="color:#a6e22e">user</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">legacyCheckout</span>(<span style="color:#a6e22e">user</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>I write this in Go because that&rsquo;s what I reach for, but the idea is language-agnostic. The flag gives you a kill switch. The progressive rollout gives you data. Together, they turn a risky deployment into a controlled experiment.</p>
<p>We stage rollouts by percentage: 1%, 5%, 25%, 100%. Each step gets at least a few hours of observation. If the error rate ticks up at 5%, we kill it at 5%. No drama.</p>
<h3 id="canary-releases">Canary releases</h3>
<p>When a change is bigger than a single feature &ndash; say, a new version of a service &ndash; canary deployments do the same thing at the infrastructure level. Route a small slice of traffic to the new version, compare its behavior against the baseline, and promote only when the numbers look right.</p>
<h3 id="shadow-traffic">Shadow traffic</h3>
<p>For really scary changes, mirror production requests to the new code path without returning its results to users. Compare outputs offline. This is how I validated Decloud&rsquo;s pricing engine rewrite: the old engine served every request while the new one ran in shadow mode for two weeks. We caught three discrepancies that would have been billing bugs.</p>
<h3 id="synthetic-monitoring">Synthetic monitoring</h3>
<p>Always-on health checks that hit your critical paths every minute. Signup. Login. Checkout. API token refresh. If synthetic checks fail, you know before your users do. This is table stakes but I&rsquo;m amazed how many teams skip it.</p>
<h2 id="when-to-not-do-this">When to not do this</h2>
<p>Some things should never be tested in production, no matter how good your flags and rollbacks are.</p>
<p>Irreversible data migrations. Authorization changes. Billing logic where a bug means overcharging real people. Compliance workflows where a mistake has legal consequences. If you can&rsquo;t undo it in thirty seconds, don&rsquo;t experiment with it live.</p>
<p>These areas need exhaustive pre-release testing, careful code review, and a deployment plan that reads more like a checklist than a YOLO push.</p>
<h2 id="what-this-actually-requires">What this actually requires</h2>
<p>Testing in production isn&rsquo;t a shortcut. It isn&rsquo;t &ldquo;move fast and break things.&rdquo; It&rsquo;s the opposite: move deliberately, instrument everything, and learn from real conditions while keeping the blast radius small enough that learning doesn&rsquo;t become damage.</p>
<p>Every production system I&rsquo;ve run &ndash; from the fintech startup&rsquo;s financial news pipeline to the mobility startup&rsquo;s real-time fleet tracking to whatever Decloud becomes &ndash; has taught me the same lesson. The gap between staging and production is where your real bugs live. You can ignore that gap and be surprised, or you can instrument it and be prepared.</p>
<p>I prefer being prepared. It&rsquo;s less exciting, but I sleep better.</p>
]]></content:encoded></item><item><title>Your SLOs Are Probably Useless (Here's How to Fix Them)</title><link>https://lawzava.com/blog/2019-05-20-effective-slos/</link><pubDate>Mon, 20 May 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-05-20-effective-slos/</guid><description>Most SLOs are dashboards nobody acts on. Pick indicators that reflect real users, set targets from data, and make error budgets change how your team ships.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>If burning through your error budget doesn&rsquo;t change how your team ships, you don&rsquo;t have SLOs. You have decorative charts.</p>
<hr>
<p>I&rsquo;ve watched three different teams adopt SLOs in the past year. Two of them ended up with beautiful Grafana dashboards that nobody looked at after the first sprint. The third team actually used their error budget to cancel a feature release and fix a checkout regression instead. Guess which team had fewer incidents in Q4.</p>
<p>The difference wasn&rsquo;t tooling. It was whether the SLO changed behavior or just measured things.</p>
<h2 id="slos-are-decisions-not-dashboards">SLOs are decisions, not dashboards</h2>
<p>At the fintech startup, we tracked uptime for our financial data API. 99.9% availability on a fancy status page. Looked great. The problem? Our SLI was measuring HTTP 200s from a health endpoint. Meanwhile, users were getting stale stock prices because our data pipeline was silently lagging by 30 minutes. By our SLO, everything was fine. By our users&rsquo; experience, the product was broken.</p>
<p>An effective SLO is a contract between reliability and velocity. It answers one question: can we ship this week, or do we owe the users some stability work first? If it doesn&rsquo;t influence your sprint planning, kill it.</p>
<h2 id="measure-what-your-users-feel-not-what-your-infra-reports">Measure what your users feel, not what your infra reports</h2>
<p>Start with the user journey. Not the Kubernetes dashboard.</p>
<p><strong>These aren&rsquo;t user-facing indicators:</strong></p>
<ul>
<li>CPU utilization</li>
<li>Pod restart counts</li>
<li>Database connection pool size</li>
</ul>
<p><strong>These are:</strong></p>
<ul>
<li>Successful checkout completions</li>
<li>Search results returned under 400ms</li>
<li>API responses with correct, fresh data</li>
</ul>
<p>The distinction seems obvious written down, but I still see teams default to infrastructure metrics because they&rsquo;re easier to collect. Easier isn&rsquo;t the point. Accurate is.</p>
<h3 id="start-with-four-signals-then-get-specific">Start with four signals, then get specific</h3>
<p>For most services, you can begin with availability, latency, throughput, and error rate. The classic golden signals. But don&rsquo;t stop there. Availability that counts health check pings the same as checkout requests is lying to you.</p>
<p>A useful SLI is brutally specific:</p>
<ul>
<li><strong>The request:</strong> <code>POST /api/v1/checkout</code> from authenticated users</li>
<li><strong>What counts as success:</strong> HTTP status &lt; 500 AND order confirmation generated</li>
<li><strong>The population:</strong> production traffic only, excluding synthetic monitors</li>
<li><strong>The window:</strong> rolling 28 days</li>
</ul>
<p>That specificity is the difference between an SLI that catches real problems and one that hides them in averages.</p>
<h2 id="set-targets-from-data-not-ambition">Set targets from data, not ambition</h2>
<p>I see this constantly: a team picks 99.99% availability because it sounds professional. They&rsquo;ve been running at 99.2% for six months. The gap between target and reality is so large that the error budget is permanently exhausted, which means the policy attached to it&rsquo;s permanently triggered, which means everyone ignores it.</p>
<p>A target that&rsquo;s never met is noise. A target that&rsquo;s always met is invisible. Neither changes behavior.</p>
<p>Here&rsquo;s what actually works:</p>
<ol>
<li><strong>Measure your current performance for 2-4 weeks.</strong> No changes, just observation.</li>
<li><strong>Set the target slightly tighter than current reality.</strong> If you&rsquo;re running at 99.5%, try 99.7%.</li>
<li><strong>Adjust quarterly based on data and user feedback.</strong> Not based on what the VP saw at a conference.</li>
</ol>
<p>Different services deserve different targets. Your payment processing endpoint can justify 99.95%. Your internal admin dashboard? 99% is probably generous. During the early Decloud days at a deep-tech founder program, we ran our dev tooling at targets that would horrify a payments team &ndash; and that was the right call. We needed to ship fast, not polish internal tools.</p>
<h3 id="windows-matter-more-than-you-think">Windows matter more than you think</h3>
<p>A 99.9% SLO over 30 days gives you about 43 minutes of allowed downtime. The same target over 7 days gives you about 10 minutes. Choose a window that matches how fast your team can actually detect and respond to problems. If your mean time to detect is 20 minutes, a 7-day window at 99.9% is a trap.</p>
<h2 id="error-budgets-the-part-everyone-gets-wrong">Error budgets: the part everyone gets wrong</h2>
<p>The math is simple:</p>
<pre tabindex="0"><code>error_budget = 1 - SLO_target
</code></pre><p>A 99.9% target over 30 days means you can tolerate roughly 43 minutes of downtime. A 99.5% target gives you about 3.6 hours. These numbers aren&rsquo;t interesting by themselves. What makes them powerful is the policy.</p>
<p><strong>Budget healthy (&gt; 50% remaining):</strong> Ship normally. Take calculated risks. Run that migration you&rsquo;ve been planning.</p>
<p><strong>Budget tight (10-50% remaining):</strong> Slow down releases. Require extra review on risky changes. Maybe skip the experimental feature flag rollout this week.</p>
<p><strong>Budget burned (&lt; 10% remaining):</strong> Stop feature work. The entire team focuses on reliability until the budget recovers.</p>
<p>That third state is where most teams fail. They write the policy, then when the budget actually burns, some product manager argues that the feature is too important to delay. If leadership won&rsquo;t enforce the budget policy, you don&rsquo;t have SLOs. You have aspirations.</p>
<h3 id="track-burn-rate-not-just-remaining-budget">Track burn rate, not just remaining budget</h3>
<p>A single bad deployment can eat your monthly budget in an hour. By the time you notice the remaining budget is low, the damage is done.</p>
<pre tabindex="0"><code>burn_rate = errors_in_window / budget_for_window
</code></pre><p>Alert on burn rate. If you&rsquo;re consuming budget at 10x the sustainable rate, you want to know in minutes, not at the Monday standup.</p>
<h2 id="keep-it-minimal">Keep it minimal</h2>
<p>You don&rsquo;t need an SLO for every endpoint. Pick the 3-5 user journeys that define whether your product is working. For most B2B SaaS, that&rsquo;s: login, core workflow, data export, and billing. Everything else is noise at this stage.</p>
<p>Instrumentation comes first. An SLO is just a query on top of good metrics. If you don&rsquo;t have request counts, status codes, and latency histograms, start there. A YAML definition can be as simple as:</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">slo</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">checkout-availability</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">objective</span>: <span style="color:#ae81ff">99.9</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">window</span>: <span style="color:#ae81ff">28d</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">indicator</span>: <span style="color:#ae81ff">success_rate</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">filter</span>: <span style="color:#e6db74">&#34;route = /checkout AND source = production&#34;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">success</span>: <span style="color:#e6db74">&#34;status_code &lt; 500 AND order_confirmed = true&#34;</span>
</span></span></code></pre></div><p>Your dashboard should answer three questions and nothing else: Are we meeting the SLO right now? How much budget is left? How fast are we burning it?</p>
<h2 id="proof-it-works">Proof it works</h2>
<p>Here&rsquo;s how you know if your SLOs are working: the last time your error budget got tight, did anything actually change? Did a release get delayed? Did someone shift from feature work to fixing that flaky dependency? Did the on-call rotation get extra support?</p>
<p>If the answer is no, go back to the error budget policy and make it real. Get sign-off from engineering leadership. Write it into your sprint process. Make the consequences automatic, not optional.</p>
<p>SLOs are a decision framework disguised as monitoring. The monitoring part is easy. The decision part is where most teams give up.</p>
<p>Don&rsquo;t be most teams.</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>Your Internal Platform Is Probably a Liability</title><link>https://lawzava.com/blog/2019-03-11-building-internal-developer-platforms/</link><pubDate>Mon, 11 Mar 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-03-11-building-internal-developer-platforms/</guid><description>Most internal developer platforms fail because nobody treated them like a product. Lessons from building (and scrapping) platform tooling at three startups.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Stop building internal platforms nobody asked for. Talk to your engineers first. If they&rsquo;d rather copy-paste bash scripts than use your platform, that tells you everything.</p>
<hr>
<p>I&rsquo;ve built internal tooling at three companies now. The fintech startup, a mobility startup, and most recently the early groundwork at Decloud. Two out of three times, I got it wrong.</p>
<p>At the fintech startup, we built a deployment system that was technically sound and architecturally clean. Nobody used it. Engineers kept SSHing into boxes and running scripts manually because our &ldquo;platform&rdquo; added steps instead of removing them. That was humbling.</p>
<p>The mistake was obvious in hindsight. We built what we thought engineers <em>should</em> want instead of solving what actually slowed them down.</p>
<h2 id="platforms-are-products-not-infrastructure-projects">Platforms Are Products, Not Infrastructure Projects</h2>
<p>This is the part most engineering leaders get backwards. You wouldn&rsquo;t ship a product feature without talking to users. But somehow, platform teams spin up for months in isolation, emerge with a Kubernetes abstraction layer, and then act surprised when adoption is flat.</p>
<p>Your developers are your users. They have deadlines, context-switching overhead, and zero patience for tools that add friction. If your platform doesn&rsquo;t make their Tuesday afternoon measurably better, it&rsquo;s shelf-ware.</p>
<p>At the mobility startup, I finally learned this. We started by sitting with engineers and listing every manual step in their deploy process. Not what we imagined the pain points were. What they actually were. Turned out the biggest time sink wasn&rsquo;t CI/CD at all &ndash; it was environment provisioning. Engineers were burning hours configuring staging environments that drifted from production within a week.</p>
<p>So we fixed that one thing first. Standardized environments, one command to spin up, automatic teardown. Adoption was immediate because it solved a real problem engineers hated.</p>
<h2 id="golden-paths-not-golden-cages">Golden Paths, Not Golden Cages</h2>
<p>The best platforms are opinionated but not authoritarian. You set strong defaults, make the common case trivial, and leave an escape hatch for the 10% of services that genuinely need something different.</p>
<p>The trap is mandating usage. The moment you force teams onto your platform without it being obviously better, you&rsquo;ve created resentment and shadow infrastructure. I&rsquo;ve seen this happen. Engineers are resourceful &ndash; they will find workarounds, and those workarounds will be worse than whatever you were trying to replace.</p>
<h2 id="the-only-metric-that-matters-early-on">The Only Metric That Matters Early On</h2>
<p>Forget deployment frequency dashboards and adoption funnels. Early on, there&rsquo;s one question: <strong>would engineers choose your platform if it wasn&rsquo;t required?</strong></p>
<p>If the answer is no, you have a product problem, not an adoption problem. No amount of migration mandates or executive sponsorship fixes bad developer experience.</p>
<h2 id="ship-small-listen-hard">Ship Small, Listen Hard</h2>
<p>Start with the most painful thing. Fix it. Ship it. Watch what happens. Then find the next most painful thing. That&rsquo;s it. That&rsquo;s the strategy.</p>
<p>I&rsquo;m three weeks into the founder-program cohort now, starting Decloud from scratch. And even at this stage &ndash; just me and a co-founder &ndash; I&rsquo;m thinking about what &ldquo;the default path to production&rdquo; looks like. Not because we need a platform. Because the habits you set in week one calcify fast, and undoing bad tooling decisions at 20 engineers is a nightmare I&rsquo;ve already lived through.</p>
<p>Build for the workflow your team already has. Then make the default path undeniably better. Everything else is noise.</p>
]]></content:encoded></item><item><title>Your API Is a Contract You Can't Take Back</title><link>https://lawzava.com/blog/2019-02-25-api-design-lessons-learned/</link><pubDate>Mon, 25 Feb 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-02-25-api-design-lessons-learned/</guid><description>Hard-won lessons on designing HTTP APIs that survive real integrations, drawn from building fintech and mobility platforms.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Stop mirroring your database in your API. Design for the people calling it, version before you think you need to, and accept that every field you ship is a promise you&rsquo;re stuck with.</p>
<hr>
<p>I&rsquo;ve been building APIs since before I knew enough to be scared of them. At the fintech startup I inherited an API surface that financial data consumers depended on daily. At a mobility startup we had mobile clients on two platforms hitting endpoints that had to be rock solid on sketchy cell connections. Now at a deep-tech founder program, starting Decloud from scratch, I finally get to apply all the scar tissue from day one.</p>
<p>Here&rsquo;s what I wish someone had told me earlier.</p>
<h2 id="your-internals-arent-your-api">Your internals aren&rsquo;t your API</h2>
<p>This is the mistake I see most often. Someone maps their Postgres schema straight to a JSON response and calls it a day. Three months later, you rename a column and half your integrations break.</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:#75715e">// This is your database leaking into the world
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;usr_id&#34;</span>: <span style="color:#ae81ff">12345</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;usr_nm&#34;</span>: <span style="color:#e6db74">&#34;Alice&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;crt_ts&#34;</span>: <span style="color:#ae81ff">1551052800</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">// This is an API response
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;id&#34;</span>: <span style="color:#e6db74">&#34;usr_12345&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Alice&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;createdAt&#34;</span>: <span style="color:#e6db74">&#34;2019-02-25T00:00:00Z&#34;</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Clients care about meaning. They don&rsquo;t care about your storage layer. Prefixed IDs (<code>usr_12345</code>) are a small investment that pay off immediately in debugging and log searching.</p>
<h2 id="be-boring-and-consistent">Be boring and consistent</h2>
<p>I know this sounds obvious. It isn&rsquo;t, in practice. The number of APIs I&rsquo;ve seen where <code>GET /users</code> returns a list but <code>GET /user/{id}</code> returns a single record, or where half the endpoints use camelCase and the other half use snake_case - it&rsquo;s depressing.</p>
<p>Pick a pattern. Stick to it everywhere.</p>
<pre tabindex="0"><code>GET    /users          # List
POST   /users          # Create
GET    /users/{id}     # Read
PUT    /users/{id}     # Replace
PATCH  /users/{id}     # Partial update
DELETE /users/{id}     # Delete
</code></pre><p>Boring is good. Boring means a new developer can guess your endpoint structure before reading the docs.</p>
<h2 id="error-responses-are-a-feature">Error responses are a feature</h2>
<p>At the fintech startup, our error responses used to be a single string message with a 400 status code. That&rsquo;s fine until you have a mobile client trying to highlight which form field failed validation, or a partner integration trying to programmatically retry on specific error types.</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;error&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;code&#34;</span>: <span style="color:#e6db74">&#34;validation_error&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;message&#34;</span>: <span style="color:#e6db74">&#34;Request validation failed&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;details&#34;</span>: [
</span></span><span style="display:flex;"><span>      {
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;field&#34;</span>: <span style="color:#e6db74">&#34;email&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;code&#34;</span>: <span style="color:#e6db74">&#34;invalid_format&#34;</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">&#34;message&#34;</span>: <span style="color:#e6db74">&#34;Must be a valid email address&#34;</span>
</span></span><span style="display:flex;"><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:#f92672">&#34;meta&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;requestId&#34;</span>: <span style="color:#e6db74">&#34;req_abc123&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The <code>requestId</code> alone will save you hours of debugging. Stable error codes let clients react without parsing human-readable messages. Field-level details let UIs be smart about what they show.</p>
<h2 id="wrap-everything-in-an-envelope">Wrap everything in an envelope</h2>
<p>Use a consistent response shape. Always. The reason is simple: if you ever need to add pagination metadata, rate limit info, or deprecation warnings, you don&rsquo;t have to restructure your entire response.</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;data&#34;</span>: { <span style="color:#f92672">&#34;id&#34;</span>: <span style="color:#e6db74">&#34;usr_123&#34;</span>, <span style="color:#f92672">&#34;name&#34;</span>: <span style="color:#e6db74">&#34;Alice&#34;</span> },
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;meta&#34;</span>: { <span style="color:#f92672">&#34;requestId&#34;</span>: <span style="color:#e6db74">&#34;req_abc123&#34;</span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>I&rsquo;ve seen teams ship a bare object as their response, then bolt on metadata by adding top-level fields next to the resource fields. Messy. An envelope avoids that from the start.</p>
<h2 id="version-before-you-need-to">Version before you need to</h2>
<p>You will make breaking changes. Accepting this early is cheaper than pretending you won&rsquo;t.</p>
<pre tabindex="0"><code>GET /v1/users
GET /v2/users
</code></pre><p>URL-based versioning is dead simple. It shows up in logs, caches, and monitoring dashboards. Header-based versioning is technically cleaner but practically harder to debug and easier to screw up. I&rsquo;ll take debuggability over purity every time.</p>
<p>The rule I follow: additive changes (new optional fields, new endpoints) don&rsquo;t need a version bump. Removing fields, renaming fields, or changing types - that&rsquo;s a new version. No exceptions.</p>
<h2 id="pagination-choose-and-commit">Pagination: choose and commit</h2>
<p>For the mobility startup&rsquo;s API, we started with offset pagination (<code>?page=2&amp;limit=20</code>) because it was simple. Worked great until our ride data grew and pages started shifting under users mid-scroll. Cursor pagination fixed that.</p>
<p>The heuristic is straightforward: offset for small, mostly-static datasets. Cursors for anything that&rsquo;s growing or changing frequently.</p>
<pre tabindex="0"><code># Offset - simple, good for admin dashboards
GET /orders?page=2&amp;limit=20

# Cursor - stable, good for feeds and timelines
GET /orders?limit=20&amp;after=cursor_abc123
</code></pre><h2 id="timestamps-iso-8601-in-utc-end-of-discussion">Timestamps: ISO 8601 in UTC, end of discussion</h2>
<pre tabindex="0"><code>2019-02-25T14:30:00Z
</code></pre><p>Not unix timestamps. Not localized formats. Not &ldquo;seconds since epoch as a string.&rdquo; One format, UTC, everywhere. This one decision prevents a class of bugs that are genuinely miserable to track down.</p>
<h2 id="rate-limiting-is-communication">Rate limiting is communication</h2>
<p>Expose your limits in headers. When you reject a request, tell the client exactly when they can retry. This is basic courtesy and it prevents retry storms.</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">X-RateLimit-Limit: 1000
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">X-RateLimit-Remaining: 998
</span></span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">Retry-After: 60
</span></span></span></code></pre></div><h2 id="after-a-few-hundred-endpoints">After a few hundred endpoints</h2>
<p>Security, stability, performance - in that order. That&rsquo;s my priority stack for everything, and APIs are no different. Use TLS everywhere. Use OAuth 2.0 or API keys with proper scoping (<code>read:orders</code>, <code>write:orders</code>). Don&rsquo;t invent your own auth scheme.</p>
<p>An API is a promise. Every field, every status code, every error shape becomes something another team depends on. The best time to think about that is before you ship. The second best time is right now.</p>
]]></content:encoded></item><item><title>Migrating to TypeScript Without Losing Your Mind</title><link>https://lawzava.com/blog/2019-01-28-migrating-to-typescript/</link><pubDate>Mon, 28 Jan 2019 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2019-01-28-migrating-to-typescript/</guid><description>How to introduce TypeScript to a real JavaScript codebase incrementally, without halting product work or annoying your entire team.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Stop treating TypeScript migration as a rewrite project. Flip one compiler flag, rename your boundary files to <code>.ts</code>, and tighten strictness over weeks. If the migration takes longer than shipping features, you&rsquo;re doing it wrong.</p>
<h3 id="the-codebase-that-convinced-me">The codebase that convinced me</h3>
<p>At the fintech startup we had a Node.js backend serving financial news to tens of thousands of users. JavaScript everywhere. It worked until it didn&rsquo;t.</p>
<p>The breaking point was a refactor to our news ranking pipeline. Someone renamed a field from <code>relevanceScore</code> to <code>score</code> in one module. The change looked clean, the tests passed (because the tests mocked the data with the new name), and it shipped. Two days later we noticed ranking was silently broken in production. The field name mismatch meant scores came through as <code>undefined</code>, and our sorting function treated <code>undefined</code> as zero. Every article ranked the same. Users saw noise instead of signal.</p>
<p>That kind of bug can&rsquo;t happen in TypeScript. The compiler catches it instantly. Not a fancy type trick. Just basic structural checking. After that incident I decided we were migrating.</p>
<h3 id="why-i-was-annoyed-about-it">Why I was annoyed about it</h3>
<p>I&rsquo;m primarily a Go developer. Go has had static types from day one. The idea that a language community needed years of debate to arrive at &ldquo;maybe we should check types before running code&rdquo; felt absurd to me. TypeScript shouldn&rsquo;t feel like a revelation. It should feel like the floor.</p>
<p>But here we are. JavaScript codebases exist, they power real products, and rewriting them in Go (tempting as that sounds) isn&rsquo;t always practical. So you migrate. Incrementally. Without drama.</p>
<h3 id="the-approach-that-actually-works">The approach that actually works</h3>
<p>Forget the blog posts showing a pristine greenfield TypeScript project with perfect types. Real migrations happen in messy codebases with deadlines.</p>
<p><strong>Step one: make TypeScript compile your existing JavaScript.</strong> Add a <code>tsconfig.json</code> that allows JS files and skips type checking them. This changes nothing about your build. It just proves the tooling works.</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;compilerOptions&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;target&#34;</span>: <span style="color:#e6db74">&#34;ES2017&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;module&#34;</span>: <span style="color:#e6db74">&#34;commonjs&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;strict&#34;</span>: <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;allowJs&#34;</span>: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;checkJs&#34;</span>: <span style="color:#66d9ef">false</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;esModuleInterop&#34;</span>: <span style="color:#66d9ef">true</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;skipLibCheck&#34;</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;include&#34;</span>: [<span style="color:#e6db74">&#34;src/**/*&#34;</span>]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>Step two: rename your boundary files.</strong> Not all of them. Start with the files that define the shapes of data crossing trust boundaries. API request handlers. Database query result types. Config loaders. These are where type mismatches actually cause production bugs. Rename them from <code>.js</code> to <code>.ts</code> and add types to the function signatures.</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-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">Article</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">title</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#a6e22e">relevanceScore</span>: <span style="color:#66d9ef">number</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">export</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">parseArticle</span>(<span style="color:#a6e22e">input</span>: <span style="color:#66d9ef">unknown</span>)<span style="color:#f92672">:</span> <span style="color:#a6e22e">Article</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">data</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">input</span> <span style="color:#66d9ef">as</span> <span style="color:#a6e22e">Record</span>&lt;<span style="color:#f92672">string</span>, <span style="color:#a6e22e">unknown</span>&gt;;
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">id</span>: <span style="color:#66d9ef">String</span>(<span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">id</span>),
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">title</span>: <span style="color:#66d9ef">String</span>(<span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">title</span>),
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">relevanceScore</span>: <span style="color:#66d9ef">Number</span>(<span style="color:#a6e22e">data</span>.<span style="color:#a6e22e">relevanceScore</span>),
</span></span><span style="display:flex;"><span>  };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That <code>parseArticle</code> function isn&rsquo;t fancy. It&rsquo;s a boundary. Everything downstream of it knows what shape it&rsquo;s working with. The field rename bug that bit us at the fintech startup becomes a compile error instead of a silent production failure.</p>
<p><strong>Step three: tighten the compiler gradually.</strong> Don&rsquo;t flip <code>strict: true</code> on day one. You will get a thousand errors and your team will revolt. Instead, enable checks one at a time over a few weeks:</p>
<ol>
<li><code>noImplicitAny</code> first. This catches the worst category of silent failures.</li>
<li><code>strictNullChecks</code> next. This eliminates the <code>undefined</code> surprise class of bugs.</li>
<li>Full <code>strict</code> once the team has momentum and the backlog of type errors is manageable.</li>
</ol>
<p>You can even run a stricter config on the directories you have already cleaned up while leaving the rest permissive:</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;extends&#34;</span>: <span style="color:#e6db74">&#34;./tsconfig.json&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;compilerOptions&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&#34;strict&#34;</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;include&#34;</span>: [<span style="color:#e6db74">&#34;src/models/**/*&#34;</span>, <span style="color:#e6db74">&#34;src/utils/**/*&#34;</span>]
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="what-kills-migrations">What kills migrations</h3>
<p>I&rsquo;ve seen three TypeScript migrations fail. Same pattern every time.</p>
<p><strong>Making it a side project.</strong> If migration work isn&rsquo;t part of normal sprints, it will never finish. The approach that works: new files are <code>.ts</code> by default, and any file you touch for a feature gets migrated as part of that feature work. No separate migration tickets that rot in the backlog.</p>
<p><strong>Sprinkling <code>any</code> everywhere and calling it done.</strong> Renaming <code>.js</code> to <code>.ts</code> and casting everything to <code>any</code> isn&rsquo;t a migration. It&rsquo;s a lie that makes the build green while providing zero safety. If you find yourself typing <code>as any</code> more than once per file, stop and think about what you&rsquo;re avoiding.</p>
<p><strong>Converting giant files first.</strong> A 2000-line controller file isn&rsquo;t where you start. Break it up first, then type the pieces. Trying to add types to a monolith module is miserable and teaches the team that TypeScript migration is miserable. Start with small utility files. Quick wins build momentum.</p>
<h3 id="third-party-types">Third-party types</h3>
<p>Most popular libraries ship types or have them on DefinitelyTyped. When they don&rsquo;t, declare the module and move 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-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#66d9ef">declare</span> <span style="color:#a6e22e">module</span> <span style="color:#e6db74">&#34;legacy-widget&#34;</span>;
</span></span></code></pre></div><p>This isn&rsquo;t ideal, but it&rsquo;s honest. You&rsquo;re saying &ldquo;I don&rsquo;t know the types for this dependency and I&rsquo;m not going to pretend.&rdquo; Better than a wrong type definition that gives false confidence.</p>
<h3 id="keep-the-pressure-on">Keep the pressure on</h3>
<p>Run <code>tsc --noEmit</code> in CI from day one. Make it a blocking check. Track your migration with something embarrassingly simple like counting file extensions:</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-bash" data-lang="bash"><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;JS: </span><span style="color:#66d9ef">$(</span>find src -name <span style="color:#e6db74">&#39;*.js&#39;</span> | wc -l<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>
</span></span><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;TS: </span><span style="color:#66d9ef">$(</span>find src -name <span style="color:#e6db74">&#39;*.ts&#39;</span> | wc -l<span style="color:#66d9ef">)</span><span style="color:#e6db74">&#34;</span>
</span></span></code></pre></div><p>When the numbers cross over, buy the team lunch. Seriously. Celebrate boring infrastructure wins. They are the ones that actually matter.</p>
<h3 id="the-honest-trade-off">The honest trade-off</h3>
<p>TypeScript adds friction. Build times go up. Editor tooling sometimes chokes on complex types. The type system has genuine holes (<code>any</code> is a backdoor, type assertions bypass checking, and some runtime patterns are hard to express statically).</p>
<p>But the trade-off is overwhelmingly worth it for any codebase with more than one contributor that will exist for more than six months. TypeScript directly improves stability by catching an entire class of defects before code reaches production. That alone justifies the migration cost.</p>
<p>Just do it incrementally. Do it as part of real work. And for the love of shipping, don&rsquo;t make it a three-month rewrite project.</p>
]]></content:encoded></item><item><title>How We Track and Prioritize Tech Debt at a Fintech Startup</title><link>https://lawzava.com/blog/2018-12-10-tech-debt-tracking-and-prioritization/</link><pubDate>Mon, 10 Dec 2018 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2018-12-10-tech-debt-tracking-and-prioritization/</guid><description>A framework for cataloging technical debt, scoring it by impact and risk, and scheduling paydown without stalling feature delivery.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Stop pretending tech debt will fix itself. Put it in a registry, score it, and schedule it like real work. We did this at the fintech startup and it turned a chaotic backlog of &ldquo;we should really fix that&rdquo; into something we actually ship against every sprint.</p>
<hr>
<p>Every engineering team I&rsquo;ve worked with has some version of the same conversation. Someone mentions a gnarly part of the codebase. Everyone nods. Someone says &ldquo;we should really clean that up.&rdquo; Nobody does.</p>
<p>At the fintech startup, we hit a point where this was genuinely hurting us. Our fintech data pipeline had accumulated enough shortcuts and half-finished migrations that feature work was getting slower every quarter. Not dramatically. Just enough friction that estimates kept creeping up and nobody could point to exactly why.</p>
<p>So I built a system for it. Nothing fancy. But it changed how we think about debt.</p>
<h2 id="what-tech-debt-actually-is">What tech debt actually is</h2>
<p>Tech debt is a future cost you created with a present decision. Sometimes that decision was smart &ndash; you shipped faster and the tradeoff was worth it. Sometimes it was accidental &ndash; the design was fine until requirements shifted. Either way, the cost is real.</p>
<p>The forms we see most often:</p>
<ul>
<li><strong>Deliberate shortcuts.</strong> You knew it was a hack. You shipped anyway. Fair enough.</li>
<li><strong>Accidental debt.</strong> Looked fine at the time. Scale or new requirements proved otherwise.</li>
<li><strong>Environmental shifts.</strong> A dependency gets deprecated. A compliance rule changes. Not your fault, still your problem.</li>
<li><strong>Operational gaps.</strong> Missing monitoring, thin tests, no runbook. The kind of thing that bites you at 2am.</li>
</ul>
<h2 id="our-debt-registry">Our debt registry</h2>
<p>Here is what actually worked for us. We created a debt registry &ndash; basically a shared list of known liabilities that lives right in our issue tracker. Not a separate doc. Not a wiki page nobody reads. Same board, same sprint planning, same visibility as feature work.</p>
<p>Each entry looks 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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">id</span>: <span style="color:#ae81ff">TD-021</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">title</span>: <span style="color:#ae81ff">Legacy auth flow lacks rate limiting</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">area</span>: <span style="color:#ae81ff">auth-service</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">impact</span>: <span style="color:#ae81ff">Security, reliability</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">risk</span>: <span style="color:#ae81ff">High</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">effort</span>: <span style="color:#ae81ff">3-4</span> <span style="color:#ae81ff">weeks</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">owner</span>: <span style="color:#ae81ff">Platform</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">status</span>: <span style="color:#ae81ff">Proposed</span>
</span></span></code></pre></div><p>The key insight was treating debt entries as first-class work items. When debt lives in a separate spreadsheet, it gets ignored. When it sits next to feature tickets in the same planning session, it gets discussed.</p>
<p>We review the registry every two weeks during sprint planning. Takes ten minutes. That alone changed how we worked.</p>
<h2 id="scoring-keep-it-dead-simple">Scoring: keep it dead simple</h2>
<p>We tried complex scoring matrices. They didn&rsquo;t survive contact with reality. What stuck was three dimensions and a formula you can do in your head:</p>
<ul>
<li><strong>Impact.</strong> How much does this slow us down or degrade the product?</li>
<li><strong>Risk.</strong> What happens if it gets worse or fails outright?</li>
<li><strong>Effort.</strong> How much work to fix it?</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-text" data-lang="text"><span style="display:flex;"><span>priority = (impact * 2) + risk - effort
</span></span></code></pre></div><p>Each on a 1-5 scale. Is it perfect? No. But it forces you to compare things that otherwise feel impossible to rank. &ldquo;The auth system is scary&rdquo; versus &ldquo;the build is slow&rdquo; suddenly becomes a conversation with numbers instead of gut feelings.</p>
<h2 id="when-to-act-now-versus-later">When to act now versus later</h2>
<p>Some things can&rsquo;t wait:</p>
<ul>
<li>Security exposure. Full stop.</li>
<li>The same root cause behind repeated incidents.</li>
<li>A dependency about to hit end-of-life.</li>
<li>Workarounds that tax every single release.</li>
</ul>
<p>Everything else goes into the prioritized backlog. High impact, low effort items get picked up opportunistically. Large refactors get scheduled as dedicated work with clear milestones.</p>
<h2 id="how-we-schedule-paydown">How we schedule paydown</h2>
<p>We tried three approaches before settling on a hybrid:</p>
<p><strong>Capacity allocation.</strong> We reserve roughly 20% of each sprint for maintenance and debt work. Non-negotiable. Product knows about it. This is the baseline.</p>
<p><strong>Debt sprints.</strong> Every six weeks, we run a focused sprint on the highest-priority debt items. Engineers pick from the top of the registry. These sprints have been some of the most satisfying work the team does.</p>
<p><strong>Opportunistic paydown.</strong> If you&rsquo;re already in the code, improve it. Boy scout rule. We just ask people to tag the cleanup commits so we can track the effort.</p>
<h2 id="incremental-migration-over-big-rewrites">Incremental migration over big rewrites</h2>
<p>We learned this the hard way. Big-bang rewrites fail. They just do. Every large debt item at the fintech startup now follows the same pattern:</p>
<ul>
<li>Run old and new paths in parallel.</li>
<li>Migrate the highest-traffic paths first.</li>
<li>Set a cutover date. Remove old code on that date. No exceptions.</li>
</ul>
<p>Feature flags make this safe:</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">if</span> flags<span style="color:#f92672">.</span>new_auth_flow:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> new_auth<span style="color:#f92672">.</span>authenticate(user)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">return</span> legacy_auth<span style="color:#f92672">.</span>authenticate(user)
</span></span></code></pre></div><p>The flag gives you a kill switch. Sleep better at night.</p>
<h2 id="selling-debt-work-to-non-engineers">Selling debt work to non-engineers</h2>
<p>This is where most teams fail. You can&rsquo;t walk into a planning meeting and say &ldquo;the auth code is messy.&rdquo; Nobody cares.</p>
<p>What works: translate debt into business language. &ldquo;Feature X takes twice as long to ship because every change requires manual regression testing in the auth module.&rdquo; Now it&rsquo;s a delivery speed conversation. Product managers understand delivery speed.</p>
<p>At the fintech startup, we started including debt impact in our sprint velocity reports. When the team could show that velocity dropped 15% quarter-over-quarter and tie it to specific debt items, getting time allocated stopped being a fight.</p>
<h2 id="prevention">Prevention</h2>
<p>A few guardrails go further than any cleanup sprint:</p>
<ul>
<li>Lightweight design reviews for anything touching critical paths.</li>
<li>Code review checklists that ask about maintainability, not just correctness.</li>
<li>Quarterly dependency upgrades so you never face a multi-year jump.</li>
<li>Documentation expectations for anything another team will touch.</li>
</ul>
<h2 id="closing">Closing</h2>
<p>Tech debt isn&rsquo;t a failure. It&rsquo;s an accounting problem. You took a loan, now you need a repayment plan.</p>
<p>The registry changed everything for us. Not because it was sophisticated &ndash; it was a handful of fields in Jira. But because it made invisible costs visible. And once everyone can see the cost, the conversation about paying it down gets a lot easier.</p>
]]></content:encoded></item><item><title>What I Learned Scaling an Engineering Team</title><link>https://lawzava.com/blog/2018-11-12-scaling-engineering-teams/</link><pubDate>Mon, 12 Nov 2018 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2018-11-12-scaling-engineering-teams/</guid><description>Lessons from growing an engineering org at the fintech startup &amp;amp;ndash; what breaks, what works, and why clarity beats process every time.</description><content:encoded><![CDATA[<p>When I joined the fintech startup as CTO, the engineering team was small enough that we could hash out every decision over lunch. Five of us in a room, whiteboarding architecture, shipping features the same week we specced them. It was fast, messy, and it worked.</p>
<p>Then we started growing.</p>
<p>By the time we hit fifteen engineers, things that used to happen naturally &ndash; knowledge sharing, aligning on priorities, knowing who owned what &ndash; stopped happening on their own. Nobody did anything wrong. The team just outgrew its own communication patterns. I remember one week where two engineers independently built overlapping caching layers because neither knew the other was working on it. That was the moment I realized: scaling a team is a fundamentally different problem than building a product.</p>
<h2 id="the-stages-nobody-warns-you-about">The stages nobody warns you about</h2>
<p>There&rsquo;s no smooth curve from small team to big team. It&rsquo;s a series of phase transitions, and each one breaks something that was working fine before.</p>
<p><strong>Around 10 engineers</strong>, everything runs on shared context. You all know the codebase, you all know each other, decisions are fast. Feels great. Savor it.</p>
<p><strong>At 25 to 30</strong>, you wake up one morning and realize you have actual teams now. Dependencies show up. Duplication shows up. That thing where you just shout across the room to coordinate? Gone. You need to be intentional about who talks to whom about what.</p>
<p><strong>At 40 to 60</strong>, you need real structure. Managers. Defined roles. Onboarding that goes beyond &ldquo;ask anyone.&rdquo; I watched this transition at the fintech startup and saw it at a startup accelerator in 2018 too &ndash; startup founders hitting this wall where the informal culture that made them fast was now making them slow. Every single one of them resisted adding structure. Every single one eventually had to.</p>
<p><strong>At 80 to 100</strong>, you&rsquo;re doing organizational design whether you like it or not. Cross-team alignment becomes someone&rsquo;s actual job. Architecture decisions need governance. Career paths matter because people will leave if they can&rsquo;t see a future.</p>
<h2 id="ownership-has-to-be-explicit">Ownership has to be explicit</h2>
<p>This was the hardest lesson for me. At a small startup, ownership is fluid. Whoever cares most picks it up. That&rsquo;s a feature, not a bug &ndash; until it stops working.</p>
<p>At the fintech startup we moved to small, mission-focused teams. Each team owned a clear slice of the product: the domain, the code, the on-call rotation, the technical direction. We wrote it down. A simple team charter &ndash; here&rsquo;s what we own, here are our interfaces, here&rsquo;s who&rsquo;s on-call. Nothing fancy.</p>
<p>The effect was immediate. Fewer &ldquo;who should I ask about this?&rdquo; questions. Faster decisions. Less duplicated work. Turns out people move quickly when they know their boundaries.</p>
<p>The classic team shapes held up well for us:</p>
<ul>
<li><strong>Stream-aligned teams</strong> delivering customer value end to end</li>
<li><strong>Platform teams</strong> providing shared infrastructure</li>
<li><strong>Enabling teams</strong> &ndash; temporary experts that unblock others</li>
<li><strong>Deep-speciality teams</strong> for the genuinely hard subsystems</li>
</ul>
<h2 id="communication-doesnt-scale-by-accident">Communication doesn&rsquo;t scale by accident</h2>
<p>At 10 people, context spreads by osmosis. You overhear things. You absorb the state of the project just by sitting in the same room.</p>
<p>At 50, that&rsquo;s completely gone. If you don&rsquo;t write it down, it didn&rsquo;t happen.</p>
<p>We learned this the hard way. The practices that actually helped:</p>
<ul>
<li>Short written updates after decisions. Not meeting notes. Actual &ldquo;here&rsquo;s what we decided and why.&rdquo;</li>
<li>Weekly demos. Not polished presentations. Just &ldquo;here&rsquo;s what shipped, here&rsquo;s what&rsquo;s stuck.&rdquo;</li>
<li>Lightweight architecture decision records for anything that changes a shared interface.</li>
<li>Clear escalation paths. When something is on fire, people need to know who to call without digging through a wiki.</li>
</ul>
<p>The goal is never more meetings. It&rsquo;s fewer surprises.</p>
<h2 id="process-should-follow-pain">Process should follow pain</h2>
<p>I&rsquo;ve seen teams add process preemptively &ndash; policies for problems they haven&rsquo;t hit yet. It rarely works. Process should show up when the pain of not having it outweighs the overhead of maintaining it.</p>
<p>At the fintech startup, our process grew roughly like this:</p>
<ul>
<li><strong>Early days</strong>: Code review. A deployment checklist. Basic on-call.</li>
<li><strong>Growing pains</strong>: Incident response runbooks. A planning cadence. Team charters.</li>
<li><strong>Later</strong>: Engineering levels. Hiring rubrics. Decision records for cross-cutting changes.</li>
</ul>
<p>One thing that helped a lot: tiered decision authority. Small decisions stay inside the team. Medium decisions involve peer teams. Big decisions get a written proposal and a review window. This kept us from bottlenecking everything through a single approval chain.</p>
<h2 id="hiring-is-the-long-game">Hiring is the long game</h2>
<p>Scaling headcount without scaling hiring quality will hurt you for years. We put rubrics in place before we ramped hiring. Trained interviewers. Calibrated scoring across panels. It slowed us down initially but the consistency paid off.</p>
<p>Onboarding was the other half. A 30/60/90 day plan with real milestones. A buddy for every new hire. A living checklist that we actually updated when things changed. The faster someone ships their first meaningful change, the faster they feel like they belong.</p>
<h2 id="invest-in-the-platform-early">Invest in the platform early</h2>
<p>Every team rebuilding the same deployment pipeline or logging setup is a team not shipping product features. We stood up a small platform team earlier than most people would recommend, and I&rsquo;d do it again.</p>
<p>Standardize the expensive stuff &ndash; languages, infrastructure patterns, observability tooling, security practices. Leave room for teams to make their own choices where it doesn&rsquo;t create systemic risk. That balance is never perfect. You just keep adjusting.</p>
<p>Technical debt compounds with team size. The more people writing code, the faster it accumulates. Give it an owner. Give it time. If nobody is responsible for paying it down, nobody will.</p>
<h2 id="culture-doesnt-transmit-itself">Culture doesn&rsquo;t transmit itself</h2>
<p>This one hit me at the accelerator. Talking to founders from all over Asia, the pattern was the same. They had a strong culture at 10 people. At 40, new hires didn&rsquo;t absorb it. The founders were frustrated because they thought culture was something you just had, not something you built and maintained.</p>
<p>Culture scales when values are written down, repeated constantly, and &ndash; most importantly &ndash; modeled by leaders. Recognize behaviors, not just outcomes. Document expectations in an engineering handbook instead of relying on tribal knowledge. Celebrate progress. Do blameless postmortems and actually mean it.</p>
<h2 id="what-it-comes-down-to">What it comes down to</h2>
<p>Scaling from 10 to 100 engineers isn&rsquo;t an engineering problem. It&rsquo;s an organizational design problem. The technical work is the easy part. The hard part is making ownership, decisions, and communication explicit enough that dozens of people can move fast without stepping on each other.</p>
<p>Get that right and growth is a multiplier. Get it wrong and every new hire makes you slower.</p>
]]></content:encoded></item><item><title>What I Learned About Code Reviews the Hard Way</title><link>https://lawzava.com/blog/2018-10-01-effective-code-reviews/</link><pubDate>Mon, 01 Oct 2018 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2018-10-01-effective-code-reviews/</guid><description>Most code reviews are theater. Here&amp;amp;rsquo;s how we fixed ours at the fintech startup and what actually made a difference.</description><content:encoded><![CDATA[<p>Most code reviews are theater. Someone opens a PR, someone else skims it, leaves an &ldquo;LGTM,&rdquo; and everyone moves on feeling productive. I&rsquo;ve been guilty of it. You probably have too.</p>
<p>At the fintech startup, we went through a painful period where our reviews were either rubber stamps or nitpick wars. Neither helped. We&rsquo;d ship bugs that a real review would have caught, then overcorrect into hour-long debates about variable naming. It took us a while to find the middle ground, and I want to share what actually worked.</p>
<h2 id="reviews-exist-to-manage-risk">Reviews exist to manage risk</h2>
<p>That&rsquo;s it. Not to prove you&rsquo;re clever. Not to enforce your preferred bracket style. Risk.</p>
<p>When I review code now, I&rsquo;m asking myself a few things: Will this break something in production? Will someone three months from now understand what this does? Are the tests actually testing the right behavior?</p>
<p>The highest-value areas to focus on:</p>
<ul>
<li><strong>Data correctness.</strong> Anything touching migrations or money gets extra scrutiny. Full stop.</li>
<li><strong>Security and access control.</strong> One missed auth check can undo months of good work.</li>
<li><strong>Error handling.</strong> The happy path is easy. The question is what happens when things go wrong.</li>
<li><strong>Performance.</strong> That innocent-looking query inside a loop? It will bite you.</li>
</ul>
<p>Everything else &ndash; formatting, import order, naming conventions &ndash; automate it. Seriously. Every minute a human spends arguing about tabs vs spaces is a minute not spent catching a real bug.</p>
<h2 id="how-i-actually-read-a-pr">How I actually read a PR</h2>
<p>I have a simple routine. Read the description first to understand what the change is supposed to do. Then jump to the tests &ndash; they tell you what the author thinks the behavior should be. Then trace the main code path. Finally, look at error handling and edge cases.</p>
<p>This order matters. If you start reading code line by line without context, you&rsquo;ll waste time on details that don&rsquo;t matter.</p>
<h2 id="giving-feedback-that-doesnt-suck">Giving feedback that doesn&rsquo;t suck</h2>
<p>We had a problem at the fintech startup where review comments were either too vague or too aggressive. &ldquo;This is wrong&rdquo; helps nobody. &ldquo;This function does three things, consider splitting parsing from validation so each has one job&rdquo; &ndash; that&rsquo;s actionable.</p>
<p>A few things that changed our review culture for the better:</p>
<p><strong>Label your comments.</strong> We started tagging things as <code>[blocker]</code>, <code>[suggestion]</code>, or <code>[nit]</code>. Sounds small. Made a huge difference. Suddenly people knew which comments were &ldquo;fix this or we don&rsquo;t merge&rdquo; versus &ldquo;take it or leave it.&rdquo; Removed so much unnecessary back-and-forth.</p>
<p><strong>Ask questions instead of making demands.</strong> &ldquo;Is there a reason we check this condition twice?&rdquo; lands differently than &ldquo;Remove the redundant check.&rdquo; Maybe there&rsquo;s a reason. Maybe the author knows something you don&rsquo;t.</p>
<p><strong>Explain the why.</strong> Don&rsquo;t just say &ldquo;use a map here.&rdquo; Say &ldquo;this lookup is O(n) inside the loop, a map makes it O(1), and this list will grow.&rdquo; Now the author learns something instead of just obeying.</p>
<h2 id="making-your-pr-easy-to-review">Making your PR easy to review</h2>
<p>This is the part most people skip, and it drives me nuts. If your PR is 800 lines with no description and no tests, you&rsquo;re not going to get a good review. You&rsquo;re going to get a tired reviewer clicking approve to clear their queue.</p>
<p>Keep changes small and focused. Self-review your own diff before asking someone else to look at it. You&rsquo;d be amazed how many issues you catch yourself. Write a description that explains what problem you&rsquo;re solving, what tradeoffs you made, and how you tested it.</p>
<p>At the fintech startup we started using a dead-simple PR template:</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-markdown" data-lang="markdown"><span style="display:flex;"><span><span style="color:#75715e">## What does this solve?
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">## Key decisions
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">## How I tested it
</span></span></span></code></pre></div><p>Three questions. Takes two minutes to fill out. Cuts review time significantly because the reviewer isn&rsquo;t guessing at intent.</p>
<h2 id="the-team-stuff">The team stuff</h2>
<p><strong>Review turnaround matters.</strong> We set a norm: small PRs get reviewed same day. Bigger ones within 48 hours. When reviews sit for days, people context-switch, branches diverge, and merging becomes painful. Fast reviews keep everyone moving.</p>
<p><strong>Let CI handle the boring stuff.</strong> Linting, formatting, type checks, security scanning &ndash; all automated. The reviewer&rsquo;s job is to think about things machines can&rsquo;t: design, intent, risk.</p>
<p><strong>Disagreements happen.</strong> When two people can&rsquo;t agree, we focus on the goal of the change, not personal taste. If we&rsquo;re still stuck, we pull in a third person, make a call, and write it down so we don&rsquo;t relitigate it next sprint.</p>
<h2 id="my-quick-mental-checklist">My quick mental checklist</h2>
<p>Before I approve anything, I run through this:</p>
<ul>
<li>Do I understand what this change is trying to do?</li>
<li>Does the code actually do that?</li>
<li>Are edge cases handled?</li>
<li>Do the tests cover the risky parts?</li>
<li>Will this be readable in six months?</li>
</ul>
<p>If I can answer yes to all five, I approve. If not, I comment. Simple as that.</p>
<p>Code reviews are a conversation about risk and clarity. When we stopped treating them as gatekeeping rituals and started treating them as collaborative problem-solving, everything got better. Fewer bugs, faster merges, less friction. That&rsquo;s the whole trick.</p>
]]></content:encoded></item><item><title>Making Go Services Fast: What Actually Matters</title><link>https://lawzava.com/blog/2018-06-25-building-high-performance-go-services/</link><pubDate>Mon, 25 Jun 2018 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2018-06-25-building-high-performance-go-services/</guid><description>Practical patterns for squeezing performance out of Go services — profiling, allocation control, bounded concurrency, and HTTP/DB tuning from real production work.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Profile first, fix allocations, bound your concurrency, tune your HTTP and DB layers. Everything else is noise.</p>
<hr>
<p>At the fintech startup we run a bunch of  <a href="/blog/2016-11-28-why-we-chose-go-for-backend-services/"
   
   >Go services</a>
 that handle financial data ingestion, NLP pipelines, and real-time news delivery. When I joined as CTO, some of these services were already showing strain under growing traffic. The Go runtime gives you great defaults, but defaults only take you so far. I spent a good chunk of 2018 hunting down latency spikes and memory bloat across our backend. Here is what I learned.</p>
<h2 id="measure-before-you-touch-anything">Measure Before You Touch Anything</h2>
<p>I can&rsquo;t stress this enough. I wasted a full day &ldquo;optimizing&rdquo; a JSON serialization path that turned out to account for 2% of our request latency. The actual bottleneck was connection pool exhaustion against Postgres. Embarrassing.</p>
<p>Before changing code, decide what &ldquo;fast&rdquo; means for your service:</p>
<ul>
<li><strong>Latency percentiles</strong> — p50, p95, p99. Averages lie.</li>
<li><strong>Requests per second</strong> at a fixed error rate</li>
<li><strong>Allocations per operation</strong> and total heap size</li>
<li><strong>GC pause time and frequency</strong></li>
<li><strong>Goroutine count</strong> — if this is climbing unbounded, you have a leak</li>
</ul>
<h2 id="profile-with-pprof-always">Profile With pprof. Always.</h2>
<p>Guessing is the enemy. <code>pprof</code> is built in and costs almost nothing to leave running on a debug port.</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:#f92672">import</span> <span style="color:#a6e22e">_</span> <span style="color:#e6db74">&#34;net/http/pprof&#34;</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">main</span>() {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">_</span> = <span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">ListenAndServe</span>(<span style="color:#e6db74">&#34;localhost:6060&#34;</span>, <span style="color:#66d9ef">nil</span>)
</span></span><span style="display:flex;"><span>    }()
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">// service startup</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><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-bash" data-lang="bash"><span style="display:flex;"><span>go tool pprof http://localhost:6060/debug/pprof/profile
</span></span><span style="display:flex;"><span>go tool pprof http://localhost:6060/debug/pprof/heap
</span></span></code></pre></div><p>For contention and scheduling issues, traces are invaluable:</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-bash" data-lang="bash"><span style="display:flex;"><span>go tool trace http://localhost:6060/debug/pprof/trace
</span></span></code></pre></div><p>I keep pprof enabled on every staging deployment. The one time I disabled it to &ldquo;reduce overhead&rdquo; was the one time I needed it most. Leave it on.</p>
<h2 id="allocations-are-the-performance-killer">Allocations Are the Performance Killer</h2>
<p>This was the single biggest lesson from our services. Allocation rate drives GC pressure, GC pressure drives latency spikes, latency spikes make your p99 look terrible. In one of our data ingestion services, cutting allocations by 40% dropped our p99 from 180ms to 45ms. Same hardware. Same traffic.</p>
<h3 id="preallocate-slices">Preallocate Slices</h3>
<p>If you know the size, tell Go. This is free performance.</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:#a6e22e">items</span> <span style="color:#f92672">:=</span> make([]<span style="color:#a6e22e">Item</span>, <span style="color:#ae81ff">0</span>, len(<span style="color:#a6e22e">input</span>))
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">v</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">input</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">items</span> = append(<span style="color:#a6e22e">items</span>, <span style="color:#a6e22e">transform</span>(<span style="color:#a6e22e">v</span>))
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Without the capacity hint, Go doubles the backing array every time it runs out of space. For a loop processing 10,000 items, that&rsquo;s a lot of unnecessary copying.</p>
<h3 id="use-syncpool-for-hot-paths">Use sync.Pool for Hot Paths</h3>
<p>Our news processing pipeline allocates byte buffers on every request. A <code>sync.Pool</code> cut per-request allocations roughly in half.</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">var</span> <span style="color:#a6e22e">bufPool</span> = <span style="color:#a6e22e">sync</span>.<span style="color:#a6e22e">Pool</span>{
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">New</span>: <span style="color:#66d9ef">func</span>() <span style="color:#66d9ef">interface</span>{} {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> make([]<span style="color:#66d9ef">byte</span>, <span style="color:#ae81ff">4096</span>)
</span></span><span style="display:flex;"><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">buf</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">bufPool</span>.<span style="color:#a6e22e">Get</span>().([]<span style="color:#66d9ef">byte</span>)
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">bufPool</span>.<span style="color:#a6e22e">Put</span>(<span style="color:#a6e22e">buf</span>)
</span></span></code></pre></div><p>One gotcha: pools get drained on every GC cycle. They smooth out allocation bursts but they&rsquo;re not a cache. Don&rsquo;t store anything you can&rsquo;t afford to recreate.</p>
<h3 id="watch-for-heap-escapes">Watch for Heap Escapes</h3>
<p>Go&rsquo;s escape analysis decides what lives on the stack versus the heap. Stack allocations are basically free. Heap allocations aren&rsquo;t.</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-bash" data-lang="bash"><span style="display:flex;"><span>go build -gcflags<span style="color:#f92672">=</span><span style="color:#e6db74">&#34;-m&#34;</span> ./...
</span></span></code></pre></div><p>Run that and read the output. Common things that force heap allocation:</p>
<ul>
<li>Returning a pointer to a local variable</li>
<li>Capturing locals in a closure</li>
<li>Storing a concrete value into an <code>interface{}</code></li>
</ul>
<p>That last one bit us. We had a logging middleware that accepted <code>interface{}</code> arguments for structured fields. Every log call was causing heap escapes. Switching to typed fields fixed it.</p>
<h3 id="strings">Strings</h3>
<p>Repeated concatenation with <code>+</code> allocates a new string every time. Use <code>strings.Builder</code>.</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">var</span> <span style="color:#a6e22e">b</span> <span style="color:#a6e22e">strings</span>.<span style="color:#a6e22e">Builder</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">for</span> <span style="color:#a6e22e">_</span>, <span style="color:#a6e22e">part</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">parts</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">b</span>.<span style="color:#a6e22e">WriteString</span>(<span style="color:#a6e22e">part</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">result</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">b</span>.<span style="color:#a6e22e">String</span>()
</span></span></code></pre></div><p>Seems obvious, but I&rsquo;ve found string concatenation in hot loops in production code more times than I want to admit.</p>
<h2 id="concurrency-goroutines-are-cheap-chaos-isnt">Concurrency: Goroutines Are Cheap, Chaos Isn&rsquo;t</h2>
<p>Goroutines cost about 2KB of stack. You can spin up millions. But should you? No. Unbounded goroutine creation is how you get cascading failures.</p>
<h3 id="worker-pools">Worker Pools</h3>
<p>We process incoming news articles through a pipeline of NLP stages. Early on, we spawned a goroutine per article. At 5,000 articles per minute, that was 5,000 goroutines competing for CPU and slamming downstream services. A fixed worker pool solved it immediately.</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">runWorkers</span>(<span style="color:#a6e22e">jobs</span> <span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#a6e22e">Job</span>, <span style="color:#a6e22e">results</span> <span style="color:#66d9ef">chan</span><span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">Result</span>, <span style="color:#a6e22e">n</span> <span style="color:#66d9ef">int</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> <span style="color:#a6e22e">wg</span> <span style="color:#a6e22e">sync</span>.<span style="color:#a6e22e">WaitGroup</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">i</span> &lt; <span style="color:#a6e22e">n</span>; <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">wg</span>.<span style="color:#a6e22e">Add</span>(<span style="color:#ae81ff">1</span>)
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>() {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">defer</span> <span style="color:#a6e22e">wg</span>.<span style="color:#a6e22e">Done</span>()
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">job</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">jobs</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">results</span> <span style="color:#f92672">&lt;-</span> <span style="color:#a6e22e">process</span>(<span style="color:#a6e22e">job</span>)
</span></span><span style="display:flex;"><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">wg</span>.<span style="color:#a6e22e">Wait</span>()
</span></span><span style="display:flex;"><span>    close(<span style="color:#a6e22e">results</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>We typically size <code>n</code> to 2x the number of CPU cores for CPU-bound work, and higher for I/O-bound work. But measure.</p>
<h3 id="buffered-channels-as-backpressure">Buffered Channels as Backpressure</h3>
<p>An unbounded queue hides overload. It grows silently until your process gets OOM-killed. Buffered channels give you explicit backpressure.</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:#a6e22e">jobs</span> <span style="color:#f92672">:=</span> make(<span style="color:#66d9ef">chan</span> <span style="color:#a6e22e">Job</span>, <span style="color:#ae81ff">1000</span>)
</span></span></code></pre></div><p>When the buffer fills, senders block. That&rsquo;s the signal that your consumers can&rsquo;t keep up. It&rsquo;s much better to slow down the producer than to let memory grow until the kernel kills you.</p>
<h3 id="always-use-context-for-cancellation">Always Use Context for Cancellation</h3>
<p>A goroutine without a cancellation mechanism is a goroutine that might run forever. Every outbound call, every slow operation needs a timeout.</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">handle</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">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">2</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:#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:#66d9ef">select</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#a6e22e">res</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&lt;-</span><span style="color:#a6e22e">slowWork</span>(<span style="color:#a6e22e">ctx</span>):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">use</span>(<span style="color:#a6e22e">res</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></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>We had a goroutine leak in one of our services that went unnoticed for weeks. A downstream API started timing out, but our goroutines just&hellip; waited. Forever. Adding context deadlines everywhere was tedious but it eliminated that entire class of problem.</p>
<h2 id="http-server-and-client-tuning">HTTP Server and Client Tuning</h2>
<p>The default <code>http.Server</code> has no timeouts. Read that again. <strong>No timeouts.</strong> A slow client can hold a connection open indefinitely.</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:#a6e22e">server</span> <span style="color:#f92672">:=</span> <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Server</span>{
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Addr</span>:           <span style="color:#e6db74">&#34;:8080&#34;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Handler</span>:        <span style="color:#a6e22e">handler</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">ReadTimeout</span>:    <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:#a6e22e">WriteTimeout</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 style="color:#a6e22e">IdleTimeout</span>:    <span style="color:#ae81ff">120</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">MaxHeaderBytes</span>: <span style="color:#ae81ff">1</span> <span style="color:#f92672">&lt;&lt;</span> <span style="color:#ae81ff">20</span>,
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Set all four. I&rsquo;ve seen production outages caused by nothing more than missing <code>ReadTimeout</code>.</p>
<p>For outbound HTTP, create <strong>one</strong> <code>http.Client</code> and reuse it. The default client has no connection pooling limits, which sounds fine until you&rsquo;re opening 10,000 connections to the same host.</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">var</span> <span style="color:#a6e22e">client</span> = <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Client</span>{
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">Transport</span>: <span style="color:#f92672">&amp;</span><span style="color:#a6e22e">http</span>.<span style="color:#a6e22e">Transport</span>{
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">MaxIdleConns</span>:        <span style="color:#ae81ff">100</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">MaxIdleConnsPerHost</span>: <span style="color:#ae81ff">100</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">IdleConnTimeout</span>:     <span style="color:#ae81ff">90</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">Timeout</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></code></pre></div><p>We were creating a new <code>http.Client</code> per request in one of our services. Swapping to a shared client with a tuned transport cut external API call latency by 30% just from connection reuse.</p>
<h2 id="database-access">Database Access</h2>
<p> <a href="/blog/2017-08-07-database-performance-tuning-systematic-approach/"
   
   >DB latency dominates most request paths</a>
. Our Postgres pools were the source of more production incidents than anything else in 2018.</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:#a6e22e">db</span>.<span style="color:#a6e22e">SetMaxOpenConns</span>(<span style="color:#ae81ff">25</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">SetMaxIdleConns</span>(<span style="color:#ae81ff">25</span>)
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">db</span>.<span style="color:#a6e22e">SetConnMaxLifetime</span>(<span style="color:#ae81ff">5</span> <span style="color:#f92672">*</span> <span style="color:#a6e22e">time</span>.<span style="color:#a6e22e">Minute</span>)
</span></span></code></pre></div><p>Key things I learned the hard way:</p>
<ul>
<li><code>MaxOpenConns</code> too high and you overwhelm the database. Too low and requests queue up waiting for a connection.</li>
<li><code>MaxIdleConns</code> should usually match <code>MaxOpenConns</code>. If idle connections are constantly being closed and reopened, you pay the TCP handshake cost on every query.</li>
<li><code>ConnMaxLifetime</code> prevents stale connections after a database failover. Without it, your app can hold connections to a node that&rsquo;s no longer primary.</li>
</ul>
<p>For write-heavy paths, batch inserts inside a transaction. The round-trip cost of individual inserts adds up fast. We had a service doing 500 individual inserts per batch. Wrapping them in a transaction with a multi-value INSERT cut that path from 2 seconds to 80 milliseconds.</p>
<h2 id="runtime-knobs">Runtime Knobs</h2>
<p>A few runtime settings worth knowing:</p>
<ul>
<li><strong>GOMAXPROCS</strong> — should match available CPU cores. In containers, this defaults to the host CPU count, not your cgroup limit. Use <code>uber-go/automaxprocs</code> to fix it automatically.</li>
<li><strong>GOGC</strong> — controls GC aggressiveness. Default is 100 (GC triggers when heap doubles). For latency-sensitive services with enough memory, bumping this to 200 or higher reduces GC frequency at the cost of higher memory use.</li>
<li><strong>runtime/pprof and expvar</strong> — lightweight runtime visibility. Expose goroutine count, heap size, and request latency. Trends matter more than snapshots.</li>
</ul>
<h2 id="benchmarks-trust-numbers-not-feelings">Benchmarks: Trust Numbers, Not Feelings</h2>
<p>Go has built-in benchmarking. Use 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-go" data-lang="go"><span style="display:flex;"><span><span style="color:#66d9ef">func</span> <span style="color:#a6e22e">BenchmarkProcess</span>(<span style="color:#a6e22e">b</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">testing</span>.<span style="color:#a6e22e">B</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">input</span> <span style="color:#f92672">:=</span> <span style="color:#a6e22e">generateInput</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">b</span>.<span style="color:#a6e22e">ResetTimer</span>()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">for</span> <span style="color:#a6e22e">i</span> <span style="color:#f92672">:=</span> <span style="color:#ae81ff">0</span>; <span style="color:#a6e22e">i</span> &lt; <span style="color:#a6e22e">b</span>.<span style="color:#a6e22e">N</span>; <span style="color:#a6e22e">i</span><span style="color:#f92672">++</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">process</span>(<span style="color:#a6e22e">input</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><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-bash" data-lang="bash"><span style="display:flex;"><span>go test -bench<span style="color:#f92672">=</span>. -benchmem -count<span style="color:#f92672">=</span><span style="color:#ae81ff">5</span> ./...
</span></span></code></pre></div><p>The <code>-benchmem</code> flag is crucial. It shows allocations per operation. And <code>-count=5</code> gives you enough samples for <code>benchstat</code> to tell you whether a change is real or noise.</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-bash" data-lang="bash"><span style="display:flex;"><span>go get golang.org/x/perf/cmd/benchstat
</span></span><span style="display:flex;"><span>benchstat old.txt new.txt
</span></span></code></pre></div><p>I&rsquo;ve been fooled by benchmark improvements that turned out to be within noise. <code>benchstat</code> keeps you honest.</p>
<h2 id="what-it-comes-down-to">What It Comes Down To</h2>
<p>Performance work in Go isn&rsquo;t glamorous. It&rsquo;s running pprof, staring at flame graphs, moving allocations to the stack, tuning pool sizes, and setting timeouts that should have been set from the start. But the payoff is real. We run services at the fintech startup that handle significant traffic on modest infrastructure, and most of the wins came from the patterns above. Not clever algorithms. Not exotic data structures. Just the basics, applied consistently.</p>
]]></content:encoded></item><item><title>Stop Wasting Everyone's Time in Technical Interviews</title><link>https://lawzava.com/blog/2018-04-16-technical-interviewing-what-actually-works/</link><pubDate>Mon, 16 Apr 2018 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2018-04-16-technical-interviewing-what-actually-works/</guid><description>Most technical interviews test the wrong things. After hiring engineers at the fintech startup, here&amp;amp;rsquo;s what I&amp;amp;rsquo;ve learned actually predicts job performance.</description><content:encoded><![CDATA[<p>Last month I sat across from a candidate who&rsquo;d built distributed systems handling millions of financial data points. Impressive resume. Real production experience. Then we asked him to reverse a binary tree on a whiteboard, and he froze. We almost passed on him. That would have been a terrible mistake.</p>
<p>He&rsquo;s one of our best hires.</p>
<p>That experience broke something in how I thought about interviewing. At the fintech startup we were growing the engineering team fast, and I kept seeing the same pattern: candidates who crushed whiteboard problems but couldn&rsquo;t debug a real issue, and candidates who struggled with algorithmic puzzles but wrote clean, thoughtful code every day.</p>
<p>Something was deeply wrong with how our industry interviews people.</p>
<h2 id="the-stuff-that-doesnt-work">The stuff that doesn&rsquo;t work</h2>
<p><strong>Whiteboard algorithms.</strong> I get why they&rsquo;re popular. They feel rigorous. But think about what you&rsquo;re actually testing. You&rsquo;re testing whether someone recently crammed LeetCode. You&rsquo;ve stripped away their IDE, their docs, their ability to Google something. You&rsquo;ve removed every tool they&rsquo;d use on the job and then judged them on the result. That&rsquo;s not an interview. That&rsquo;s hazing.</p>
<p><strong>Brain teasers.</strong> &ldquo;How many golf balls fit in a school bus?&rdquo; If you&rsquo;re still asking these in 2018, please stop. Google dropped them years ago after their own data showed zero correlation with job performance. Zero.</p>
<p><strong>Trivia questions.</strong> &ldquo;What&rsquo;s the time complexity of HashMap.get() in Java?&rdquo; Cool, they memorized it. Or they didn&rsquo;t. Either way you&rsquo;ve learned nothing about whether they can build software.</p>
<p><strong>Massive take-home projects.</strong> I&rsquo;ve seen companies send 8-hour assignments and call it &ldquo;a small exercise.&rdquo; You&rsquo;re not testing skill. You&rsquo;re filtering for people with no life outside work. Single parents, people with side commitments, anyone who values their free time &ndash; they just opt out. And you never even know what you missed.</p>
<h2 id="what-actually-predicts-performance">What actually predicts performance</h2>
<p>I&rsquo;ve been iterating on our process at the fintech startup for a while now. Here&rsquo;s what&rsquo;s worked.</p>
<h3 id="give-them-a-laptop-and-a-real-problem">Give them a laptop and a real problem</h3>
<p>This is the single biggest improvement we made. Instead of a whiteboard, we hand candidates a laptop with an IDE, internet access, and a small codebase. The task is something close to what they&rsquo;d actually do on the job. Add a feature. Fix a bug. Review a pull request.</p>
<p>You learn so much more this way. How do they read unfamiliar code? Do they check edge cases? How do they use their tools? Do they ask good questions when stuck?</p>
<p>One of our best interview tasks is a broken API endpoint with a failing test. The candidate gets logs, the codebase, and 45 minutes. We&rsquo;re not looking for a perfect fix. We&rsquo;re watching how they think.</p>
<h3 id="pair-with-them">Pair with them</h3>
<p>Pair programming sessions are gold. You pick a small problem, sit next to the candidate, and work on it together. Not as an examiner &ndash; as a collaborator.</p>
<p>This tells you things no other format can. How do they handle suggestions? Do they explain their thinking? Can they take feedback without getting defensive? These are the things that actually matter when you&rsquo;re shipping code with a team.</p>
<h3 id="talk-about-what-theyve-already-built">Talk about what they&rsquo;ve already built</h3>
<p>For senior roles especially, I&rsquo;ve started spending more time on deep dives into past work. Pick a system they built. Ask them to walk you through the architecture. Then start pulling threads. Why this database? What would you change? What broke in production?</p>
<p>Good engineers light up during these conversations. They remember the tradeoffs. They&rsquo;ll tell you about the decision they regret. The ones who padded their resume get vague fast.</p>
<h3 id="use-rubrics-not-vibes">Use rubrics, not vibes</h3>
<p>Early on at the fintech startup, our debriefs were a mess. &ldquo;I liked her.&rdquo; &ldquo;He seemed smart.&rdquo; &ldquo;I don&rsquo;t know, something felt off.&rdquo; That&rsquo;s not a hiring decision. That&rsquo;s a gut check, and guts are biased.</p>
<p>Now every interviewer fills out a rubric before the debrief. Same questions, same scale. You write down specific evidence, not impressions. It&rsquo;s not glamorous, but it&rsquo;s the difference between a process and a coin flip.</p>
<h2 id="designing-the-loop">Designing the loop</h2>
<p>Keep it tight. Nobody needs a six-round interview for a backend role.</p>
<p>Here&rsquo;s roughly what we do:</p>
<ul>
<li><strong>Phone screen</strong> (30-45 min): Can this person communicate? Do they have the baseline?</li>
<li><strong>Work sample</strong> (60-90 min): Laptop, real problem, real tools.</li>
<li><strong>Deep dive</strong> (45 min): Walk me through something you built.</li>
<li><strong>Team fit</strong> (45 min): Values, collaboration style, how they handle disagreement.</li>
</ul>
<p>For senior roles, swap the deep dive for a system design conversation. Let them drive. Ask about tradeoffs, not trivia.</p>
<h2 id="train-your-interviewers">Train your interviewers</h2>
<p>This one&rsquo;s overlooked constantly. Most engineers are terrible interviewers. Not because they&rsquo;re bad people, but because nobody taught them. They default to whatever they experienced as a candidate, which was probably also bad.</p>
<p>At the fintech startup we started having new interviewers shadow experienced ones. We review rubrics together. We debrief on the debrief. It takes time, but the signal quality goes way up.</p>
<h2 id="candidate-experience-is-your-reputation">Candidate experience is your reputation</h2>
<p>I can&rsquo;t count how many times a candidate told me about a horrible interview experience at some other company. Ghosted after three rounds. Left waiting in a lobby for 40 minutes. Given a problem that had nothing to do with the role.</p>
<p>Every candidate who walks out of your office talks about it. To their friends, to their colleagues, sometimes on Glassdoor. At a startup where we&rsquo;re competing with bigger names for talent, that reputation matters enormously.</p>
<p>Be clear about the process upfront. Respect their time. If you&rsquo;re not making an offer, tell them why. It costs you nothing and it&rsquo;s the right thing to do.</p>
<h2 id="watch-out-for-these-traps">Watch out for these traps</h2>
<p><strong>Hiring for similarity.</strong> If your whole team went to the same three schools and thinks the same way, you don&rsquo;t have a team. You have an echo chamber. Diverse panels, evidence-based decisions, focus on complementary strengths.</p>
<p><strong>Credential worship.</strong> A degree from a top university tells you someone got into a top university. It doesn&rsquo;t tell you they can debug a production outage at 2am or write code their teammates can actually read.</p>
<p><strong>Letting one bad interview tank a candidate.</strong> Single data points are noisy. If four interviews went great and one was mediocre, look at the full picture. Maybe the interviewer had an off day. Maybe the question was bad. Aggregate the signal.</p>
<h2 id="the-actual-point">The actual point</h2>
<p>Technical interviewing isn&rsquo;t some unsolvable problem. It&rsquo;s just that most companies copy what everyone else does without asking whether it works. Test real work. Use consistent evaluation. Treat candidates like humans. The bar isn&rsquo;t that high, and yet most of our industry still can&rsquo;t clear it.</p>
]]></content:encoded></item><item><title>A Go Developer Looks at Rust for Backend Work</title><link>https://lawzava.com/blog/2018-03-05-rust-for-backend-services/</link><pubDate>Mon, 05 Mar 2018 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2018-03-05-rust-for-backend-services/</guid><description>I write Go every day at the fintech startup. Here&amp;amp;rsquo;s why I&amp;amp;rsquo;ve been spending evenings with Rust, what impressed me, and where it still hurts.</description><content:encoded><![CDATA[<p>I write Go. Basically all day. Our backend services at the fintech startup are Go, and I genuinely like the language. Simple, fast to compile, easy to deploy, boring in the best way.</p>
<p>So why am I spending evenings reading the Rust book?</p>
<p>It started with a parsing service. We ingest a lot of financial data &ndash; news feeds, filings, scraped content &ndash; and one of our pipeline stages was chewing through memory in ways that made me nervous. Go&rsquo;s garbage collector is good, but &ldquo;good&rdquo; still means occasional latency spikes when you&rsquo;re allocating like crazy. I started wondering: what if we didn&rsquo;t have a garbage collector at all?</p>
<p>That&rsquo;s the pitch with Rust, basically. Memory safety without the GC. You get compile-time guarantees that you won&rsquo;t hit null dereferences, use-after-free, or data races. The compiler yells at you instead of production.</p>
<h2 id="what-actually-impressed-me">What actually impressed me</h2>
<p>The ownership model is the thing everyone talks about, and honestly, it deserves the hype. In Go, I&rsquo;m used to reasoning about who&rsquo;s holding a reference to what, but it&rsquo;s all in my head. Rust puts it in the type system. The compiler enforces it. You literally can&rsquo;t compile code that has a data race.</p>
<p>That&rsquo;s wild to me.</p>
<p>Error handling is another win. Go&rsquo;s <code>if err != nil</code> pattern is fine, I&rsquo;ve defended it plenty of times. But Rust&rsquo;s <code>Result</code> type with the <code>?</code> operator is genuinely more ergonomic:</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-rust" data-lang="rust"><span style="display:flex;"><span><span style="color:#66d9ef">fn</span> <span style="color:#a6e22e">read_user</span>(id: <span style="color:#a6e22e">UserId</span>, db: <span style="color:#66d9ef">&amp;</span><span style="color:#a6e22e">Db</span>) -&gt; Result<span style="color:#f92672">&lt;</span>User, DbError<span style="color:#f92672">&gt;</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">let</span> row <span style="color:#f92672">=</span> db.get(id)<span style="color:#f92672">?</span>;
</span></span><span style="display:flex;"><span>    Ok(User::from_row(row)<span style="color:#f92672">?</span>)
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Compact. Every failure path is explicit and visible. No hidden exceptions, no panics you didn&rsquo;t expect. I appreciate that.</p>
<p>And the performance characteristics are real. No GC pauses means your tail latency stays flat. For a service that needs to respond in single-digit milliseconds under load, that matters. I&rsquo;ve been benchmarking a small parser in Rust vs our Go equivalent, and the Rust version uses about a third of the memory with more predictable response times.</p>
<h2 id="where-it-hurts">Where it hurts</h2>
<p>I&rsquo;m not going to pretend this has been smooth.</p>
<p>The learning curve is steep. Ownership and borrowing make sense conceptually, but fighting the borrow checker for the first couple weeks is genuinely frustrating. There were moments where I knew the code was correct but couldn&rsquo;t convince the compiler. That&rsquo;s a different kind of pain than Go, where the language mostly gets out of your way.</p>
<p>Compile times. Oh, the compile times. Coming from Go, where a full rebuild takes seconds, waiting for Rust to chew through dependencies feels like going back in time. Incremental builds help, but the first build of a project with a handful of crates? Go make coffee.</p>
<p>The ecosystem in early 2018 is&hellip; young. <code>serde</code> is great, <code>hyper</code> is solid, <code>tokio</code> works. But the moment you need something less common &ndash; a specific database driver, an integration with a particular message queue &ndash; you might be writing it yourself or depending on a crate with 12 stars and one contributor. In Go, I take the standard library and rich ecosystem for granted.</p>
<p>And async is still rough. Futures exist, but ergonomic async/await syntax isn&rsquo;t stable yet. Writing async Rust today involves a lot of ceremony that Go handles with goroutines and channels almost effortlessly.</p>
<h2 id="would-i-use-it-at-the-fintech-startup">Would I use it at the fintech startup?</h2>
<p>I&rsquo;ve been thinking about this specifically. Not &ldquo;is Rust good&rdquo; &ndash; it clearly is &ndash; but &ldquo;should we adopt it here, now, for our stuff.&rdquo;</p>
<p>The honest answer: selectively, maybe.</p>
<p>Our data ingestion pipeline is the obvious candidate. It&rsquo;s CPU-bound, memory-intensive, processes untrusted input, and needs predictable latency. Rust fits that profile perfectly. I could see rewriting the parsing stage as a standalone Rust service behind a clear API boundary. Keep everything else in Go. No big migration, no retraining the whole team. Just one focused service where the tradeoffs make sense.</p>
<p>What I wouldn&rsquo;t do is rewrite our CRUD APIs in Rust. The database is the bottleneck there, not the language. Go is plenty fast for request routing and JSON shuffling, and the team can iterate on those services without learning a new language.</p>
<p>The staffing question is real too. I can&rsquo;t hire Rust developers easily right now. If I put Rust into production, I need to be honest that I&rsquo;m also signing up to teach it. That&rsquo;s a cost.</p>
<h2 id="where-ive-landed">Where I&rsquo;ve landed</h2>
<p>Rust is the real deal for specific problems. Memory safety without a GC, predictable performance, genuinely safe concurrency. These aren&rsquo;t marketing claims &ndash; they hold up in practice.</p>
<p>But it&rsquo;s not a replacement for Go. Not for us, not right now. It&rsquo;s a complement. A sharp tool for the places where Go&rsquo;s tradeoffs start to pinch.</p>
<p>I&rsquo;m going to keep exploring. Probably going to prototype that parser service over the next few weeks and see how it holds up under our actual production load. If the results are as good as my benchmarks suggest, we might have our first Rust service in production by summer.</p>
<p>That feels like the right way to do it. Small, measured, honest about the costs.</p>
]]></content:encoded></item><item><title>Machine Learning for Backend Engineers: What Actually Matters</title><link>https://lawzava.com/blog/2018-02-05-machine-learning-for-backend-engineers/</link><pubDate>Mon, 05 Feb 2018 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2018-02-05-machine-learning-for-backend-engineers/</guid><description>What backend engineers actually need to know about ML in production &amp;amp;ndash; from someone who builds NLP pipelines for financial news.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Most ML in production is data plumbing and ops. Master those and the model is the easy part.</p>
<p>I&rsquo;ve been building NLP and sentiment analysis systems at the fintech startup for a while now. We process financial news at scale &ndash; classifying articles, extracting sentiment, figuring out which stories actually move markets. And the thing that surprises most backend engineers when they first touch ML? The model is maybe 10% of the work. The rest is everything you already know how to do, just with new failure modes.</p>
<p>This post is for backend engineers getting pulled into ML projects. Not data scientists. Not researchers. People who build services, own uptime, and wonder why the model team keeps asking for &ldquo;just one more pipeline.&rdquo;</p>
<h2 id="the-three-kinds-of-ml-youll-actually-see">The Three Kinds of ML You&rsquo;ll Actually See</h2>
<p>Supervised learning dominates production. Labeled data in, predictions out. Classification or regression. At the fintech startup, most of our models are supervised &ndash; we feed in financial articles with known sentiment labels, and the model learns to score new ones. Simple concept, messy execution.</p>
<p>Unsupervised learning shows up for clustering and anomaly detection. We use it for grouping related news stories. Useful, but less common in typical backend work.</p>
<p>Reinforcement learning? You&rsquo;ll probably never touch it. Skip it.</p>
<h2 id="the-lifecycle-is-boring-thats-the-point">The Lifecycle Is Boring (That&rsquo;s the Point)</h2>
<p>Collect data. Clean it. Build features. Train a model. Evaluate. Deploy. Monitor. Repeat forever.</p>
<p>Backend engineers own the first step, the last two, and large chunks of everything in between. If you think your job ends at &ldquo;expose the model behind an API,&rdquo; you&rsquo;re going to have a bad time. Training-serving skew alone has cost us weeks of debugging at the fintech startup. The model works perfectly in the notebook. Performs terribly in production. Every. Single. Time. Until you get disciplined about it.</p>
<h2 id="data-is-the-whole-game">Data Is the Whole Game</h2>
<p>I can&rsquo;t stress this enough. The model is a commodity. The data is the product.</p>
<p>At the fintech startup we ingest thousands of financial articles per day. Each one needs to be cleaned, normalized, deduped, and tagged before a model ever sees it. A sentiment model trained on messy data doesn&rsquo;t produce &ldquo;noisy predictions.&rdquo; It produces wrong predictions that look confident. That&rsquo;s worse.</p>
<p>Here&rsquo;s what a basic feature builder looks like:</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:#75715e"># Example: build features for a single user</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">build_user_features</span>(user_id, db, now):
</span></span><span style="display:flex;"><span>    user <span style="color:#f92672">=</span> db<span style="color:#f92672">.</span>query(User)<span style="color:#f92672">.</span>get(user_id)
</span></span><span style="display:flex;"><span>    orders <span style="color:#f92672">=</span> db<span style="color:#f92672">.</span>query(Order)<span style="color:#f92672">.</span>filter_by(user_id<span style="color:#f92672">=</span>user_id)<span style="color:#f92672">.</span>all()
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> user:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    total_orders <span style="color:#f92672">=</span> len(orders)
</span></span><span style="display:flex;"><span>    total_spent <span style="color:#f92672">=</span> sum(o<span style="color:#f92672">.</span>total <span style="color:#66d9ef">for</span> o <span style="color:#f92672">in</span> orders)
</span></span><span style="display:flex;"><span>    last_order_at <span style="color:#f92672">=</span> max((o<span style="color:#f92672">.</span>created_at <span style="color:#66d9ef">for</span> o <span style="color:#f92672">in</span> orders), default<span style="color:#f92672">=</span><span style="color:#66d9ef">None</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;user_id&#34;</span>: user_id,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;account_age_days&#34;</span>: (now <span style="color:#f92672">-</span> user<span style="color:#f92672">.</span>created_at)<span style="color:#f92672">.</span>days,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;total_orders&#34;</span>: total_orders,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;total_spent&#34;</span>: total_spent,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;avg_order_value&#34;</span>: total_spent <span style="color:#f92672">/</span> total_orders <span style="color:#66d9ef">if</span> total_orders <span style="color:#66d9ef">else</span> <span style="color:#ae81ff">0</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#34;days_since_last_order&#34;</span>: (now <span style="color:#f92672">-</span> last_order_at)<span style="color:#f92672">.</span>days <span style="color:#66d9ef">if</span> last_order_at <span style="color:#66d9ef">else</span> <span style="color:#66d9ef">None</span>,
</span></span><span style="display:flex;"><span>    }
</span></span></code></pre></div><p>Straightforward. But now imagine this runs during training with one set of data transformations and during inference with a slightly different one. Your model is now subtly broken and nobody knows. We learned this the hard way &ndash; our sentiment scores drifted for two weeks before anyone noticed the feature computation differed between batch training and real-time serving.</p>
<p>Feature stores exist to solve exactly this. Compute features once, store them with point-in-time correctness, serve them identically to training and inference. Whether you build your own or use an off-the-shelf solution doesn&rsquo;t matter. What matters is that both paths see the same numbers.</p>
<h2 id="serving-pick-your-pain">Serving: Pick Your Pain</h2>
<p>Two patterns. Batch and real-time.</p>
<p>Batch works when latency doesn&rsquo;t matter. Pre-compute predictions nightly, store them, serve from a cache. We batch-score article relevance this way at the fintech startup &ndash; run it overnight, have results ready for morning markets. Simple and reliable.</p>
<p>Real-time is for interactive features. User types a query, you need a prediction in 50ms. This is where things get operationally interesting. You&rsquo;re now running model inference on the hot path. Latency budgets are real. The model is large. Feature computation has external dependencies. Any of those can spike your p99 and ruin someone&rsquo;s day.</p>
<p>Embedding the model in your application process is the fastest path to production and the fastest path to regret. Model updates now require app deploys. Memory usage goes up. You can&rsquo;t scale them independently.</p>
<p>A dedicated model service is more work upfront but lets you version, deploy, and scale the model separately. We went this route for our real-time NLP scoring and haven&rsquo;t looked back.</p>
<p>Managed services? Fine if you accept the vendor lock-in and latency constraints. Just go in with your eyes open.</p>
<p>For latency, the playbook is boring but effective: cache aggressively, precompute what you can, use lighter models for hot paths, and always have a fallback. If the sentiment model is down, we return a neutral score and flag it. Users get a degraded experience instead of an error.</p>
<h2 id="deploy-like-its-a-service-because-its">Deploy Like It&rsquo;s a Service, Because It&rsquo;s</h2>
<p>Version your models. <code>models/sentiment/v7</code> is fine. What&rsquo;s not fine is deploying a model without knowing what data it was trained on or what its evaluation metrics looked like.</p>
<p>Treat model deploys like service deploys. Canary first. A/B test if you can. Feature flags if you can&rsquo;t. We roll out new sentiment models to 5% of traffic, compare prediction distributions against the old model, and only promote if the numbers look sane. No heroics.</p>
<p>Rollback needs to be trivial. If v7 starts producing garbage, switching back to v6 should take seconds, not a meeting.</p>
<h2 id="monitoring-is-where-backend-engineers-shine">Monitoring Is Where Backend Engineers Shine</h2>
<p>This is your wheelhouse. Latency, error rates, throughput &ndash; you already track these. For ML you add a few more:</p>
<p>Prediction distribution. If your sentiment model suddenly thinks everything is positive, something broke. Track the histogram.</p>
<p>Confidence scores. If average confidence drops, the model is seeing data it wasn&rsquo;t trained for. At the fintech startup, a confidence drop on our article classifier was our first signal that a new type of financial instrument was showing up in the news. The model hadn&rsquo;t seen crypto coverage before. Neither had we, honestly.</p>
<p>Feature drift. If the input distributions shift from what the model saw during training, predictions will degrade. You won&rsquo;t always have ground truth labels right away &ndash; in financial sentiment, the &ldquo;correct&rdquo; label might not be clear for days or weeks. So you need proxy signals.</p>
<h2 id="working-with-data-scientists">Working With Data Scientists</h2>
<p>I&rsquo;ll be direct. The handoff between data scientists and backend engineers is where most ML projects die.</p>
<p>Data scientists need clean, documented data and a clear path to production. Backend engineers need a contract: what goes in, what comes out, how fast, and what &ldquo;wrong&rdquo; looks like. If you don&rsquo;t agree on these things explicitly, you&rsquo;ll agree on them implicitly through production incidents.</p>
<p>The best setup I&rsquo;ve seen is shared ownership. The data scientist owns the model logic. The backend engineer owns the serving infrastructure. Both own the pipeline. Nobody gets to throw something over a wall and walk away.</p>
<h2 id="in-short">In Short</h2>
<p>ML in production is mostly engineering. Data pipelines, feature consistency, serving infrastructure, monitoring, and deployment discipline. The model itself is usually the part that changes least. If you get the plumbing right &ndash; consistent features, clear latency budgets, solid monitoring for drift &ndash; you&rsquo;ll ship more reliable ML than most teams running exotic architectures on shaky foundations.</p>
<p>That&rsquo;s the job. It&rsquo;s not glamorous. It&rsquo;s useful.</p>
]]></content:encoded></item><item><title>What I Learned Building Our Platform Team This Year</title><link>https://lawzava.com/blog/2017-12-28-building-platform-teams/</link><pubDate>Thu, 28 Dec 2017 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2017-12-28-building-platform-teams/</guid><description>Reflections on standing up the fintech startup&amp;amp;rsquo;s platform team in 2017 — what worked, what didn&amp;amp;rsquo;t, and why treating infra like a product changed everything.</description><content:encoded><![CDATA[<p>Looking back at 2017, the single best engineering decision we made at the fintech startup was pulling infrastructure work out of the product teams and giving it a home. Not a big, formal reorg. Just two of us, initially, saying: we&rsquo;re going to own CI/CD, deployment, and monitoring so the rest of the team can stop reinventing it every sprint.</p>
<p>That was March. By December, we had a small platform team that actually worked. Getting there was messy.</p>
<h3 id="the-problem-was-obvious">The problem was obvious</h3>
<p>We had product engineers writing their own deployment scripts. Plural. Each service had its own way of getting to production. Some used shell scripts checked into the repo. One team had a guy who just SSH&rsquo;d into the box. Logging was inconsistent. Monitoring was whatever someone remembered to set up. When something broke at 2 AM, diagnosing it meant guessing which service did what and where its logs lived.</p>
<p>This is fine when you have three services. We had more than three services.</p>
<h3 id="what-we-actually-built">What we actually built</h3>
<p>I kept the scope ruthless. Four areas, nothing else.</p>
<p><strong>Developer experience.</strong> CI/CD pipelines, a shared staging environment, deployment tooling that didn&rsquo;t require tribal knowledge. This was the first thing we tackled because it was the loudest pain point.</p>
<p><strong>Runtime infrastructure.</strong> Databases, caches, message queues. Standardized. Documented. Not six different Postgres configurations floating around.</p>
<p><strong>Observability.</strong> Centralized logging and monitoring. One place to look when things go wrong. This alone saved us hours every incident.</p>
<p><strong>Security defaults.</strong> Secrets management that wasn&rsquo;t &ldquo;put it in an environment variable and hope.&rdquo; Auth tooling that product teams could plug into without reading a novel.</p>
<h3 id="treat-it-like-a-product-or-it-dies">Treat it like a product or it dies</h3>
<p>Here is the thing I didn&rsquo;t expect. Building the platform was the easy part. Getting people to use it? That was the actual job.</p>
<p>We had product engineers with muscle memory. They knew their janky deploy scripts. They didn&rsquo;t trust our new pipeline. Fair enough. So I stopped thinking of the platform as infrastructure and started thinking of it as a product. Our users were the product teams. If they didn&rsquo;t adopt what we built, we failed. Full stop.</p>
<p>This meant sitting with teams. Watching them work. Asking dumb questions like &ldquo;why did you just do that manually?&rdquo; and then going back and automating whatever that was. It meant writing docs that were actually useful, not docs that checked a box. It meant having a Slack channel where people got answers fast.</p>
<p>Self-service was non-negotiable. If a team needed a new database, they should get it in minutes. Not file a ticket. Not wait for me to wake up. The moment we became a ticket queue, we became the bottleneck we were trying to eliminate.</p>
<h3 id="how-we-built-the-team">How we built the team</h3>
<p>I started alone, then pulled in one more person with strong ops background. That was the right call. You need someone who has been paged at 3 AM and someone who knows how to build tooling that doesn&rsquo;t feel like punishment to use. Pure infra people build things that work but nobody can figure out. Pure app developers build things that look nice but fall over under load.</p>
<p>We stayed small. Two people, then three by Q4. Attached to engineering leadership, sitting next to the product teams. Physically close. That proximity matters more than any process. When you overhear someone complaining about deploys, you fix deploys. When you&rsquo;re in a different building, you build things nobody asked for.</p>
<h3 id="golden-paths-over-mandates">Golden paths over mandates</h3>
<p>We never mandated anything. I hate mandates. Instead, we built golden paths — a service template, a default pipeline config, a monitoring setup that worked out of the box. You could ignore it all and do your own thing. But the golden path was so much easier that nobody bothered.</p>
<p>That&rsquo;s the trick. Make the right thing the easy thing. Don&rsquo;t write policies. Write code that makes the policy unnecessary.</p>
<p>We exposed platform capabilities through simple APIs. Want a database? Here&rsquo;s a YAML spec, submit it, done. Want to deploy? Push to main. Want logs? They are already where you expect 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-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">apiVersion</span>: <span style="color:#ae81ff">platform.example.com/v1</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">kind</span>: <span style="color:#ae81ff">Database</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">metadata</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">name</span>: <span style="color:#ae81ff">billing-db</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">spec</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">engine</span>: <span style="color:#ae81ff">postgresql</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">size</span>: <span style="color:#ae81ff">small</span>
</span></span></code></pre></div><p>Nothing fancy. Deliberately boring. Boring infrastructure is good infrastructure.</p>
<h3 id="what-i-would-do-differently">What I would do differently</h3>
<p>I waited too long to write documentation. The first three months, everything lived in my head and in Slack threads. That doesn&rsquo;t scale. Even with three product teams, it doesn&rsquo;t scale. Write the docs early, even if they are rough.</p>
<p>I also underestimated how much time support would eat. By September, I was spending most of my week answering questions and helping teams migrate. Almost no time left to build new capabilities. We fixed this by setting explicit office hours and protecting two full days per week for building. Should have done that from day one.</p>
<h3 id="where-this-goes-next">Where this goes next</h3>
<p>We&rsquo;re a small company. This setup works for our size. If the fintech startup doubles its engineering team, the platform group will need to split into focused sub-teams — developer experience, infrastructure, security. But not yet. Right now the right move is staying lean and staying close to the people we serve.</p>
<p>A platform team is a product team that happens to build infrastructure. The moment you forget the product part, you&rsquo;re just an ops team with a fancier name. Keep your users close, build for self-service, and make the right path the easy path. That&rsquo;s the whole playbook.</p>
]]></content:encoded></item><item><title>Stop Trying to Fix All Your Tech Debt</title><link>https://lawzava.com/blog/2017-12-18-technical-debt-triage-framework-for-prioritization/</link><pubDate>Mon, 18 Dec 2017 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2017-12-18-technical-debt-triage-framework-for-prioritization/</guid><description>A two-number scoring system for tech debt that tells you what to fix now, what to schedule, and what to quietly accept.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Multiply pain by how often you touch it. Fix the top. Schedule the middle. Ignore the rest guilt-free.</p>
<h3 id="the-problem-with-we-should-really-fix-that">The Problem With &ldquo;We Should Really Fix That&rdquo;</h3>
<p>At the fintech startup we had a spreadsheet. Forty-seven items on it. Stuff ranging from &ldquo;our deploy script is held together with duct tape&rdquo; to &ldquo;that one table column named <code>data2</code>.&rdquo; Every retro, someone added more. Nothing came off.</p>
<p>The list was useless because everything on it felt important to whoever wrote it down. No ranking. No shared language for severity. Just a growing monument to good intentions.</p>
<p>So I built a framework. Dead simple. Two numbers.</p>
<h3 id="two-numbers-one-multiplication">Two Numbers, One Multiplication</h3>
<p>For every piece of tech debt, score two things on a 1-to-5 scale:</p>
<p><strong>Impact</strong> &ndash; how much does this actually hurt? A 5 means outages, data bugs, or features you literally can&rsquo;t ship. A 1 means it annoys you when you see it but has zero customer-facing consequence.</p>
<p><strong>Change frequency</strong> &ndash; how often does someone touch this code? A 5 means daily. A 1 means you forgot the directory existed.</p>
<p>Multiply them. That&rsquo;s your priority score.</p>
<p><code>priority = impact x change frequency</code></p>
<ul>
<li><strong>15-25</strong>: Fix this soon. Like, put it in the next sprint soon.</li>
<li><strong>8-14</strong>: Schedule it. Quarter planning, debt sprints, whatever your cadence is.</li>
<li><strong>1-7</strong>: Accept it. Seriously. Move on.</li>
</ul>
<h3 id="running-the-numbers-on-real-stuff">Running the Numbers on Real Stuff</h3>
<p>I&rsquo;ll use examples close to what we actually triaged at the fintech startup.</p>
<p>Our user service had no tests and we were shipping changes to it three times a week. Impact 4, frequency 5. Score: <strong>20</strong>. Obviously top of the list.</p>
<p>The deploy process was painful and slow. Impact 4, frequency 4. Score: <strong>16</strong>. Right behind it.</p>
<p>We had a gnarly payment integration that scared everyone, but we only touched it once a quarter. Impact 5, frequency 2. Score: <strong>10</strong>. Scary, but not urgent.</p>
<p>An admin dashboard that loaded slowly? Impact 2, frequency 2. Score: <strong>4</strong>. Nobody cared enough. And that was the correct call.</p>
<p>The math removes the emotion. That&rsquo;s the point.</p>
<h3 id="not-all-debt-ages-the-same">Not All Debt Ages the Same</h3>
<p>This matters and most people miss it. Some debt compounds. Every time you work around the problem, the workaround becomes the new baseline. The next person works around the workaround. You know exactly what I&rsquo;m talking about.</p>
<p>Other debt just sits there. Ugly, stable, inert. The weird naming convention in a module nobody touches? It&rsquo;ll be weird next year too, but it won&rsquo;t be worse.</p>
<p>And some debt evaporates. We had a messy integration with a third-party API we were planning to drop. Spending time cleaning it up would have been pure waste.</p>
<p>When you&rsquo;re prioritizing, ask: is this getting worse? Compounding debt should jump the queue even if the current score is middling.</p>
<h3 id="four-ways-to-actually-pay-it-down">Four Ways to Actually Pay It Down</h3>
<p><strong>Boy Scout rule.</strong> You&rsquo;re already in the file. Leave it slightly better. Rename that variable. Extract that function. This handles the small stuff and keeps entropy from winning.</p>
<p><strong>Protected time.</strong> Block real capacity for debt work. We did one day a week. Some teams do a cooldown sprint after each release. Doesn&rsquo;t matter what rhythm you pick, what matters is that the time is sacred and not the first thing cut when a deadline looms.</p>
<p><strong>Bundle it with features.</strong> If a feature touches a debt-heavy area, pad the estimate to include cleanup. Product managers accept this more easily than standalone debt tickets because the work is tied to something they already want.</p>
<p><strong>Bite the bullet.</strong> Some debt needs a dedicated rewrite. A subsystem replacement. A migration. These need a business case, an owner, and a timeline. Not a Jira ticket that says &ldquo;refactor payments&rdquo; with no assignee sitting in the backlog for eight months.</p>
<h3 id="selling-debt-work-upward">Selling Debt Work Upward</h3>
<p>Engineers talk about debt in terms of code quality. Leadership doesn&rsquo;t care about code quality. They care about shipping speed, incident frequency, and hiring retention.</p>
<p>So translate. &ldquo;This area has no tests and we ship to it constantly, which is why we&rsquo;ve had three production incidents this quarter&rdquo; lands differently than &ldquo;we need to improve test coverage.&rdquo; Same problem. Different framing.</p>
<p>At the fintech startup I started tying debt items to incident reports. When leadership could see a direct line between a piece of debt and a customer-facing problem, the prioritization conversation got a lot shorter.</p>
<h3 id="accepting-debt-is-a-decision-not-a-failure">Accepting Debt Is a Decision, Not a Failure</h3>
<p>Here&rsquo;s what changed once we had the scoring system: we stopped feeling guilty about the bottom of the list. A score of 4 means you&rsquo;ve looked at it, evaluated it, and decided your time is better spent elsewhere. That&rsquo;s not neglect. That&rsquo;s judgment.</p>
<p>Low-score debt in a system you&rsquo;re replacing? Ignore it. Cosmetic issues in stable code? Ignore them. Theoretical problems with no evidence of real pain? Keep an eye on them, but don&rsquo;t burn a sprint.</p>
<h3 id="the-takeaway">The Takeaway</h3>
<p>Forty-seven items became six that mattered. The rest we either accepted or scheduled for later with clear triggers for when to revisit. The team stopped arguing about what to fix because the math settled it.</p>
<p>Two numbers. One multiplication. That&rsquo;s the whole framework.</p>
]]></content:encoded></item><item><title>Stop Counting Code Reviews and Start Reading Them</title><link>https://lawzava.com/blog/2017-11-13-why-code-review-quality-matters-more-than-quantity/</link><pubDate>Mon, 13 Nov 2017 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2017-11-13-why-code-review-quality-matters-more-than-quantity/</guid><description>Most code reviews are theater. Here&amp;amp;rsquo;s what actually makes them worth the time.</description><content:encoded><![CDATA[<p>Most code reviews are a waste of everyone&rsquo;s time.</p>
<p>I know that sounds harsh. But I&rsquo;ve watched it happen over and over at the fintech startup and at every team I&rsquo;ve worked with before. Someone opens a PR, someone else clicks approve within three minutes, and we all pretend the process worked. It didn&rsquo;t. Nobody read anything. The review was theater.</p>
<p>The problem isn&rsquo;t that teams skip reviews. Almost nobody skips them anymore. The problem is that teams confuse <em>doing</em> reviews with <em>doing them well</em>. You can have a 100% review rate and still ship garbage if nobody&rsquo;s actually thinking about the code they&rsquo;re approving.</p>
<h2 id="actually-read-the-damn-code">Actually read the damn code</h2>
<p>This shouldn&rsquo;t need saying, but here we are. Before you leave a single comment on a PR, you need to understand what the change does and why it exists. If the PR description doesn&rsquo;t explain that, send it back. Don&rsquo;t guess. Don&rsquo;t fill in the blanks yourself and hope you got it right.</p>
<p>At the fintech startup we made it a rule: if you can&rsquo;t explain the change to someone else in plain language, you haven&rsquo;t reviewed it. Simple as that. I&rsquo;ve rejected &ldquo;LGTM&rdquo; approvals that came in ninety seconds after a PR was opened. There&rsquo;s no way you read 400 lines of code in ninety seconds. You just didn&rsquo;t.</p>
<p>Follow the data through the code. Step through the logic path. Check what happens when things go wrong, not just when they go right. That&rsquo;s the work.</p>
<h2 id="stop-obsessing-over-style">Stop obsessing over style</h2>
<p>I see this constantly. A reviewer leaves fifteen comments about bracket placement and variable naming while completely missing a SQL injection vulnerability three lines down. Priorities, people.</p>
<p>Correctness. Security. Error handling. Edge cases. Maintainability. That&rsquo;s what humans should focus on. Everything else should be automated. We set up linters and formatters specifically so we could stop arguing about tabs versus spaces and start catching actual bugs.</p>
<p>If your review comments are mostly about style, you&rsquo;re not reviewing. You&rsquo;re copyediting.</p>
<h2 id="say-something-useful">Say something useful</h2>
<p>&ldquo;This is confusing&rdquo; isn&rsquo;t feedback. It&rsquo;s a feeling. Tell the author <em>what</em> is confusing and <em>why</em>. Better yet, suggest a fix.</p>
<p>If a function called <code>processUserData</code> is doing both validation and transformation, don&rsquo;t just say &ldquo;this does too much.&rdquo; Say &ldquo;split this into <code>validateUserData</code> and <code>transformUserData</code> so we can test them independently and the control flow is obvious.&rdquo; That&rsquo;s something someone can act on in five minutes instead of spending twenty minutes guessing what you meant.</p>
<p>Also, label your comments. At the fintech startup we started prefixing with <code>blocker:</code>, <code>suggestion:</code>, or <code>question:</code>. It sounds bureaucratic, but it completely changed the dynamic. Authors stopped treating every comment as a demand and reviewers stopped softening real concerns to avoid conflict. Blockers block the merge. Suggestions are optional. Questions are just questions.</p>
<h2 id="make-reviews-possible-in-the-first-place">Make reviews possible in the first place</h2>
<p>Giant PRs get bad reviews. Always. Nobody can hold 2000 lines of diff in their head and reason about correctness. The code might be fine. The reviewer will never know because they glazed over at line 300.</p>
<p>Keep PRs small. One concern per PR. Write a description that explains the problem, the approach, and how to test it. I&rsquo;ve seen PRs at the fintech startup where the description was better than the code, and that&rsquo;s not an insult. Good context makes review ten times faster and ten times more useful.</p>
<h2 id="automate-everything-that-isnt-judgment">Automate everything that isn&rsquo;t judgment</h2>
<p>Formatting, linting, type checking, test gates, dependency scanning. All of it should run before a human ever looks at the PR. Every minute a reviewer spends on something a machine could catch is a minute wasted.</p>
<p>Your reviewers&rsquo; attention is finite and expensive. Protect it.</p>
<h2 id="the-honest-thing-to-do">The honest thing to do</h2>
<p>If you don&rsquo;t have time to review something properly, say so. &ldquo;I can&rsquo;t get to this until tomorrow&rdquo; is infinitely more useful than a rubber-stamp approval today. A fake review is worse than no review because it gives false confidence. At least with no review, everyone knows the risk.</p>
<p>Code review works when people take it seriously. Not as a gate to check off, but as the last chance to catch something before it hits production. That&rsquo;s the whole point. Everything else is just counting approvals.</p>
]]></content:encoded></item><item><title>Engineering Manager vs Tech Lead: What's Actually Different</title><link>https://lawzava.com/blog/2017-10-09-engineering-manager-vs-tech-lead/</link><pubDate>Mon, 09 Oct 2017 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2017-10-09-engineering-manager-vs-tech-lead/</guid><description>Two leadership tracks, one fork in the road. What engineering managers and tech leads actually do day-to-day, from how we structured it at the fintech startup.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Managers multiply through people. Tech leads multiply through systems. Pick based on what drains you least, not what sounds prestigious.</p>
<hr>
<h3 id="the-fork-nobody-prepares-you-for">The fork nobody prepares you for</h3>
<p>Every strong engineer eventually faces this question. Do you go into management, or do you stay technical and lead from there?</p>
<p>I&rsquo;ve been on both sides of this at the fintech startup. When we were small, I did both. Ran architecture decisions, hired engineers, handled 1:1s, reviewed code. It was unsustainable. Once we grew past a handful of engineers, I had to pick. And more importantly, I had to figure out what each role <em>actually</em> demanded so we could hire and promote into them properly.</p>
<p>Here&rsquo;s what I learned.</p>
<h3 id="side-by-side-breakdown">Side-by-side breakdown</h3>
<table>
  <thead>
      <tr>
          <th></th>
          <th><strong>Engineering Manager</strong></th>
          <th><strong>Tech Lead</strong></th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Primary focus</strong></td>
          <td>People, process, delivery</td>
          <td>Architecture, code quality, technical direction</td>
      </tr>
      <tr>
          <td><strong>Typical day</strong></td>
          <td>1:1s, standups, cross-team syncs, hiring calls</td>
          <td>Code reviews, design docs, debugging hard problems, prototyping</td>
      </tr>
      <tr>
          <td><strong>Authority</strong></td>
          <td>Direct — owns performance reviews, promotions, team composition</td>
          <td>Indirect — influences through proposals, standards, and example</td>
      </tr>
      <tr>
          <td><strong>Codes?</strong></td>
          <td>Sometimes, in small teams. Shouldn&rsquo;t be on the critical path.</td>
          <td>Yes. Expected to ship meaningful work.</td>
      </tr>
      <tr>
          <td><strong>Accountable for</strong></td>
          <td>Team health, velocity, retention, stakeholder alignment</td>
          <td>System quality, technical debt, architectural coherence</td>
      </tr>
      <tr>
          <td><strong>Hardest part</strong></td>
          <td>Difficult conversations. Firing someone. Navigating politics.</td>
          <td>Being responsible for outcomes you don&rsquo;t directly control.</td>
      </tr>
      <tr>
          <td><strong>Reward</strong></td>
          <td>Watching people grow. Team shipping without drama.</td>
          <td>Shaping a system. Solving the problem nobody else could crack.</td>
      </tr>
  </tbody>
</table>
<p>That table is the short version. Let me expand on the parts that aren&rsquo;t obvious.</p>
<h3 id="what-an-engineering-manager-actually-does">What an engineering manager actually does</h3>
<p>Most of the job is invisible. You&rsquo;re clearing the path so your team can build. Hiring the right people, onboarding them fast, giving honest feedback, shielding the team from organizational noise. None of that shows up in a commit log.</p>
<p>At the fintech startup, I made a rule for myself: if I was writing production code as a manager, something was wrong. Either I hadn&rsquo;t hired well enough, or I was clinging to the IC identity. Both are problems.</p>
<p>The trap is thinking management is a promotion from engineering. It&rsquo;s not. It&rsquo;s a career change. You stop building things and start building an environment. Completely different skill set. If you hate ambiguity, dislike repetitive conversations, or need the dopamine of shipping code daily — you will be miserable. I&rsquo;ve seen good engineers become terrible managers because nobody told them this upfront.</p>
<p>What makes a great EM: you actually care about people&rsquo;s careers. Not performatively. You enjoy the puzzle of team dynamics. You can sit in a room full of competing priorities and walk out with a plan everyone tolerates.</p>
<h3 id="what-a-tech-lead-actually-does">What a tech lead actually does</h3>
<p>The tech lead owns the &ldquo;how.&rdquo; You decide the architecture, set coding standards, break ties on technical debates, and make sure the system doesn&rsquo;t rot while the team ships features.</p>
<p>You still write code. That&rsquo;s non-negotiable. A tech lead who stops coding loses credibility fast. But you also spend a surprising amount of time communicating. Explaining tradeoffs to product managers. Writing design documents. Mentoring junior engineers through code review. The ratio shifts — maybe 50% coding, 50% everything else — but the code stays.</p>
<p>The frustrating part? You&rsquo;re accountable for the system but you don&rsquo;t control the people building it. You can&rsquo;t assign tasks or run performance reviews. You lead by influence, by being right often enough that people trust your judgment. If your ideas are bad, or if you can&rsquo;t communicate why they&rsquo;re good, you have nothing.</p>
<p>At the fintech startup, our tech leads owned specific domains. One person owned the data pipeline architecture. Another owned the API layer. Clear ownership, clear accountability. No committees.</p>
<h3 id="how-to-choose">How to choose</h3>
<p>Forget the title. Look at how you spend your energy.</p>
<p>After a day of back-to-back 1:1s, are you energized or drained? After eight hours deep in a codebase, do you feel alive or lonely? The honest answer points you in the right direction.</p>
<p>Better yet — test it. Before you commit to either track, volunteer for the work. Mentor a junior engineer for a quarter. Run a sprint planning cycle. On the tech side, drive an RFC through to implementation. Own a migration. Lead a postmortem.</p>
<p>The title matters less than the work. If you&rsquo;re doing the work and it fits, the title will follow.</p>
<h3 id="the-hybrid-trap">The hybrid trap</h3>
<p>We tried the &ldquo;tech lead manager&rdquo; thing at the fintech startup when the team was four people. I was doing both. It sort of worked until it didn&rsquo;t. You end up being mediocre at two jobs instead of good at one. Skipping 1:1s because you&rsquo;re deep in a debugging session. Skimping on code review because you have a hiring pipeline to manage.</p>
<p>If your company is small enough that one person must do both — fine, just know it&rsquo;s temporary. Plan the split before you need it.</p>
<h3 id="one-more-thing">One more thing</h3>
<p>Management isn&rsquo;t a promotion. Tech lead isn&rsquo;t a consolation prize. I&rsquo;ve met directors who would&rsquo;ve been happier as principal engineers and staff engineers who secretly wanted to run a team. The industry does a bad job of framing these as equal. They are.</p>
<p>You can switch tracks. People do it all the time. But switching costs something — you lose momentum, you rebuild credibility, and skills you don&rsquo;t use will fade. So pick deliberately, try the work before you commit, and don&rsquo;t let someone else&rsquo;s career ladder define yours.</p>
]]></content:encoded></item><item><title>Your Startup Doesn't Need a Security Team. It Needs a Security Champion.</title><link>https://lawzava.com/blog/2017-09-18-why-every-startup-needs-security-champion/</link><pubDate>Mon, 18 Sep 2017 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2017-09-18-why-every-startup-needs-security-champion/</guid><description>You can&amp;amp;rsquo;t afford a security team at a startup. But you can turn one motivated engineer per squad into a security champion — and that changes everything.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Pick the engineer who already asks, &ldquo;but what if someone sends a malformed token?&rdquo; in code review. Give them time, training, and air cover. That&rsquo;s your security champion. We did this at our fintech startup with a team of eight, and it caught real problems that would have cost us users and money.</p>
<hr>
<h3 id="we-had-no-security-team">We Had No Security Team</h3>
<p>In 2017, our fintech startup processed financial news for thousands of users. Eight engineers. No dedicated security person. No budget for one either.</p>
<p>But we were handling user data, API keys, third-party integrations, and payment flows. The attack surface wasn&rsquo;t small. And everything I know from national cyber-defense exercises confirms that &ldquo;we&rsquo;ll deal with security later&rdquo; is how you end up in an incident report.</p>
<p>So I borrowed an idea from military operations: you don&rsquo;t need every soldier to be a specialist. You need embedded people who know enough to spot danger and escalate fast.</p>
<p>That&rsquo;s a security champion.</p>
<h3 id="what-a-security-champion-actually-is">What a Security Champion Actually Is</h3>
<p>Not a security expert. Not a penetration tester. Just an engineer on the team who volunteers to keep security visible.</p>
<p>At the fintech startup, our champion was a backend developer with a natural paranoia about input validation. Perfect. I didn&rsquo;t need him to write a threat model from scratch. I needed him to ask &ldquo;where does this data come from?&rdquo; during design discussions. To flag hardcoded credentials in PRs. To be the person other engineers felt comfortable asking basic security questions.</p>
<p>The role boils down to three things:</p>
<ol>
<li><strong>Ask the uncomfortable questions during design and code review.</strong> Not after. During.</li>
<li><strong>Keep the basics visible.</strong> Auth, data handling, dependency updates, logging.</li>
<li><strong>Escalate what&rsquo;s beyond their depth.</strong> In our case, that meant escalating to me or to an external advisor we had on retainer for a few hours a month.</li>
</ol>
<p>That&rsquo;s it. Lightweight. High leverage.</p>
<h3 id="why-this-works-better-than-youd-expect">Why This Works Better Than You&rsquo;d Expect</h3>
<p><strong>Single points of failure kill you.</strong> This is a core lesson from national cyber-defense exercises. If one person holds all the security knowledge, they become a bottleneck, and when they&rsquo;re on vacation or quit, you&rsquo;re blind. Champions spread awareness across the org. Security stops being one person&rsquo;s job and becomes part of how the team thinks.</p>
<p><strong>Context beats checklists.</strong> An external auditor can scan your code. But your champion knows that the team just shipped a new webhook endpoint last Thursday, that it accepts user-supplied URLs, and that nobody discussed SSRF. That contextual knowledge is worth more than any automated scan.</p>
<p><strong>Culture shift happens from the inside.</strong> When the security voice comes from within the team, not from some outside compliance function, people listen differently. It stops feeling like an audit and starts feeling like engineering discipline. At the fintech startup, after a few months, engineers who weren&rsquo;t champions started raising security concerns on their own. That&rsquo;s when you know it&rsquo;s working.</p>
<h3 id="how-to-set-it-up">How to Set It Up</h3>
<p><strong>Let people volunteer.</strong> Draft picks don&rsquo;t work here. The engineer who wants the role will actually do the work. The one who gets voluntold will treat it as overhead.</p>
<p><strong>Give them real time.</strong> This is where most programs die. If security work sits on top of a full sprint, it won&rsquo;t happen. We carved out a few hours per week. Not a lot. Enough to signal that leadership actually cares.</p>
<p><strong>Train them, but practically.</strong> Skip the theory-heavy workshops. We ran short sessions: here&rsquo;s how XSS works, here&rsquo;s how to spot an insecure deserialization pattern in our stack, here&rsquo;s what our threat model looks like for this feature. Hands-on, specific to our codebase, thirty minutes at a time.</p>
<p><strong>Give them air cover.</strong> This is the one that matters most. If a champion pushes back on shipping an unsafe feature and gets overruled by a PM every time, the program is dead. Leadership has to back them up. I made it clear at the fintech startup: if the champion says &ldquo;this needs a security review before it ships,&rdquo; that&rsquo;s what happens. No exceptions.</p>
<p><strong>Make the work visible.</strong> If nobody sees the security work, nobody values it. Mention it in standups. Bring it up in performance reviews. Treat it as a real engineering contribution, because it is.</p>
<h3 id="the-pushback-youll-get">The Pushback You&rsquo;ll Get</h3>
<p>Engineers will say they&rsquo;re not qualified. Good. That means they understand the limits of the role. Champions aren&rsquo;t supposed to be experts. They&rsquo;re supposed to spot obvious risks, ask questions, and know when to escalate. That&rsquo;s a much lower bar than people think.</p>
<p>Engineers will say they don&rsquo;t have time. They&rsquo;re right, unless you give them time. Protect those hours.</p>
<p>Some people will question whether it actually helps. Point to the PR where a champion caught a SQL injection before it hit staging. Or the design review where someone asked &ldquo;what happens if this JWT is expired but the signature is still valid?&rdquo; and saved the team a week of debugging in production.</p>
<h3 id="scaling-it">Scaling It</h3>
<p>With eight engineers, one champion was enough. As the fintech startup grew, the model scaled naturally: one champion per team, a shared Slack channel for cross-team security questions, and a monthly sync where champions compared notes. Eventually, if you&rsquo;re lucky enough to grow into needing a real security team, your champions become the connective tissue between that team and the rest of engineering. They already know the code, the people, and the history.</p>
<h3 id="the-honest-truth">The Honest Truth</h3>
<p>A security champion program isn&rsquo;t a substitute for real security investment. It&rsquo;s a bridge. It gets a startup from &ldquo;we have nothing&rdquo; to &ldquo;we have something real&rdquo; without blowing the budget. It builds habits that stick. And it creates a culture where security is part of building software, not a panic that happens after a breach.</p>
<p>We ran this at the fintech startup. It worked. Not because the process was perfect, but because we had one engineer who gave a damn and leadership that backed him up. Start there.</p>
]]></content:encoded></item><item><title>Leading Without a Title — What Actually Works</title><link>https://lawzava.com/blog/2017-06-26-technical-leadership-without-authority/</link><pubDate>Mon, 26 Jun 2017 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2017-06-26-technical-leadership-without-authority/</guid><description>Nobody handed me a leadership mandate at the fintech startup. I earned it through credibility, clear communication, and the unglamorous work.</description><content:encoded><![CDATA[<p>Three months into my role as CTO at the fintech startup, I walked into a meeting where the product team had already decided on an architecture for a new feature. Nobody had consulted me. Nobody was required to. The company was small and scrappy, roles bled into each other, and the fact that I had &ldquo;CTO&rdquo; on paper meant very little when the people in the room had been shipping the product longer than I had. I sat down, listened to the plan, and realized it had a serious scalability problem. But I also realized that saying &ldquo;this is wrong, I&rsquo;m the CTO&rdquo; would be the fastest way to get ignored forever.</p>
<p>So I shut up, asked a few questions, and later that afternoon wrote a one-page doc outlining the bottleneck I saw. I included numbers from our existing traffic patterns. I proposed an alternative that reused most of their work. Two days later the team pivoted — not because I pulled rank, but because the problem was clear once I put it on paper.</p>
<p>That was the moment I understood what technical leadership without authority actually looks like. And honestly, it&rsquo;s the only kind of leadership that sticks.</p>
<h3 id="credibility-is-earned-in-the-boring-moments">Credibility is earned in the boring moments</h3>
<p>You don&rsquo;t become the person people listen to by giving brilliant speeches. You become that person by being right about the small stuff consistently. Fixing the flaky test nobody wants to touch. Jumping on an incident at 2am and writing up the postmortem the next morning. Reviewing PRs with actual thought instead of rubber-stamping them.</p>
<p>At the fintech startup, I had to build credibility from zero. The team was sharp. They didn&rsquo;t care about titles. What they cared about was whether you could help them solve hard problems without wasting their time. Fair enough.</p>
<p>I focused on a few areas where I knew my experience was strong — system design, infrastructure, and debugging production issues under pressure. I didn&rsquo;t pretend to know the financial domain better than people who had been in fintech for years. When I was wrong, I said so. When I was unsure, I said that too. Turns out, admitting uncertainty is one of the fastest ways to build trust. People can smell posturing.</p>
<h3 id="the-real-work-is-communication">The real work is communication</h3>
<p>Here is what nobody tells you about influence without authority: it&rsquo;s 80% communication and 20% technical skill. You can be the best engineer in the room and still get nothing done if you can&rsquo;t frame a problem in a way that makes people care.</p>
<p>I learned to always lead with the problem. Not my solution. The problem. If I walked into a discussion and said &ldquo;we should switch to Kafka,&rdquo; people pushed back instinctively. But if I said &ldquo;we&rsquo;re dropping 12% of events during peak load because our queue can&rsquo;t keep up — here are the numbers from last Tuesday,&rdquo; suddenly everyone wanted to talk about solutions. Same destination, completely different path.</p>
<p>Writing things down changed everything for me. At the fintech startup we were moving fast, and decisions got lost in Slack threads and hallway conversations. I started writing short proposals — a page, maybe two. Problem, context, recommendation, alternatives, tradeoffs. Nothing fancy. But it forced me to think clearly, and it gave people something to react to on their own time instead of in a high-pressure meeting. Half the arguments I avoided were arguments that never needed to happen in the first place.</p>
<h3 id="make-other-people-better">Make other people better</h3>
<p>The biggest unlock was realizing that my job wasn&rsquo;t to be the smartest person on the team. My job was to make the team smarter. Pair with someone on a tricky migration. Share the context behind a design decision so the next person doesn&rsquo;t have to reverse-engineer it. Fix the CI pipeline that wastes 20 minutes of everyone&rsquo;s day.</p>
<p>These things aren&rsquo;t glamorous. Nobody gives you a promotion for speeding up the build. But they compound. Every small improvement makes the team a little faster, a little less frustrated. And the people you helped? They remember. When you propose something ambitious later, they are the ones who back you up.</p>
<h3 id="start-small-find-allies">Start small, find allies</h3>
<p>When I wanted to push a bigger initiative — say, rearchitecting a service that had become a bottleneck — I never started with a grand plan. I started with a focused prototype. Something I could build in a week that demonstrated the idea was viable. Early wins create momentum. A working demo is worth ten slide decks.</p>
<p>Then I found allies. One person pushing for change is easy to dismiss. Three people aligned on the same problem? That&rsquo;s a movement. I would share early results with engineers I trusted, get their input, fold in their ideas. By the time the proposal reached a wider audience, it wasn&rsquo;t just my thing. It was our thing.</p>
<h3 id="the-traps-that-get-smart-engineers">The traps that get smart engineers</h3>
<p>Being right and being effective are two different skills. I&rsquo;ve watched brilliant engineers lose every argument because they treated disagreements as contests to win instead of problems to solve together. If you burn a relationship to win a technical debate, congratulations — you won the battle and lost the war. That person won&rsquo;t support your next idea. Or the one after that.</p>
<p>Pick your battles. Not everything is worth fighting over. Some decisions are reversible and low-stakes, and spending political capital on them is a waste. Save your energy for the things that genuinely matter.</p>
<h3 id="this-is-the-job">This is the job</h3>
<p>I used to think leadership without authority was a stepping stone — something you did while waiting for the &ldquo;real&rdquo; leadership role. I was wrong. This is the real thing. The credibility you build, the communication habits you develop, the trust you earn — these are the foundations. If a title shows up later, great. You will be ready. If it doesn&rsquo;t, you&rsquo;re still the person who makes things happen. And every team needs that person more than they need another manager.</p>
]]></content:encoded></item><item><title>2016: The Year I Stopped Fighting Infrastructure</title><link>https://lawzava.com/blog/2016-12-28-year-in-review-technology-trends-2016/</link><pubDate>Wed, 28 Dec 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-12-28-year-in-review-technology-trends-2016/</guid><description>A personal look back at 2016 &amp;amp;ndash; Docker going mainstream, Kubernetes momentum, Go adoption, and lessons from building at a mobility startup and a fintech startup.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Docker won. Kubernetes is next. Go keeps earning its place. I was wrong about serverless timing and right about containers. Building two startups simultaneously taught me more about operational discipline than any conference talk ever could.</p>
<p>2016 felt like the year the industry collectively decided to stop debating containers and start running them. For me it was more specific than that. It was the year I stopped treating infrastructure as a thing to fight and started treating it as a thing to design.</p>
<h3 id="docker-finally-stopped-being-a-toy">Docker finally stopped being a toy</h3>
<p>I spent most of 2015 defending Docker to skeptics. &ldquo;It works on my machine&rdquo; jokes were still the dominant reaction. By mid-2016, that conversation was over. Docker went mainstream. Not just for dev environments &ndash; for production workloads, across teams that had never touched a Dockerfile before.</p>
<p>At a mobility startup, we containerized everything. Location tracking, payment processing, fleet management. Docker Compose got us off the ground fast, and when we outgrew single-host scheduling, the migration path was clear. The tooling matured enough in 2016 that containers stopped being a bet and started being a default.</p>
<p>What changed wasn&rsquo;t Docker itself &ndash; it was the ecosystem around it. Registries got better. Logging and monitoring caught up. CI pipelines learned to build and push images without a week of YAML wrestling. The boring parts got boring, which is when you know adoption is real.</p>
<h3 id="kubernetes-momentum-was-undeniable">Kubernetes momentum was undeniable</h3>
<p>I wrote about this earlier this year after evaluating Swarm, Kubernetes, and Mesos side by side. My conclusion then: Kubernetes is going to win, but the operational tax in late 2016 is genuinely painful.</p>
<p>That assessment held up. We ran Swarm at the mobility startup for three months before migrating to Kubernetes, and both decisions were correct. Swarm gave us speed. Kubernetes gave us a real platform. The CNCF formation, Google&rsquo;s backing, and the velocity of community tooling made the trajectory obvious.</p>
<p>What I underestimated was how fast managed Kubernetes would become available. GKE was already solid. By early 2017, the &ldquo;Kubernetes is too hard to operate&rdquo; argument will start losing force. That&rsquo;s a good thing.</p>
<h3 id="go-kept-earning-its-place">Go kept earning its place</h3>
<p>We chose Go for our backend services at both the mobility startup and the fintech startup, and 2016 validated that decision repeatedly. The language grew in adoption. The tooling stayed excellent. The deployment story &ndash; single static binary, no runtime dependencies &ndash; kept saving us time in production.</p>
<p>Go 1.7 shipped with context in the standard library, which was a practical win for anyone building HTTP services with cancellation and timeouts. The ecosystem filled in noticeably. More battle-tested libraries for common patterns. Better database drivers. gRPC support matured.</p>
<p>The trade-offs I wrote about earlier remain real. No generics. Verbose error handling. A younger ecosystem than Java or Python. But for the kind of services we build &ndash; concurrent, networked, performance-sensitive &ndash; Go in 2016 was the right tool. Not the trendiest. The right one.</p>
<h3 id="what-i-got-wrong">What I got wrong</h3>
<p>I thought serverless would matter more by now. Lambda launched in 2014, and by early 2016 I expected it to be a serious architecture option for startups. It wasn&rsquo;t. The tooling was immature, cold starts were painful, and the debugging experience was bad enough to kill productivity. Serverless in 2016 was interesting to watch but not interesting to use for anything beyond simple event handlers.</p>
<p>I also underestimated how long microservices confusion would persist. I assumed the &ldquo;monolith first&rdquo; message from DHH and others would settle things. It didn&rsquo;t. Teams kept splitting services prematurely, then drowning in operational complexity they weren&rsquo;t staffed to handle. This is still happening as I write this.</p>
<h3 id="what-i-got-right">What I got right</h3>
<p>Containers as the default deployment unit. Called it in 2015, and 2016 delivered. Kubernetes as the orchestration winner. Called it in October, and the trajectory only accelerated. Go as a serious backend language for startups. We bet on it early and it paid off consistently.</p>
<p>The one I&rsquo;m most satisfied about: investing in operational discipline over feature velocity. At both the mobility startup and the fintech startup, I pushed hard for automated deploys, rollback capability, and monitoring before we added features. That discipline saved us multiple times this year. One outage at the mobility startup that would have been a multi-hour firefight turned into a three-minute rollback because we had the infrastructure in place.</p>
<h3 id="the-fintech-angle">The fintech angle</h3>
<p>At the fintech startup, the ML side of the business grew significantly in 2016. TensorFlow matured, and our team started using it for financial content relevance scoring. The accessibility of ML tooling improved noticeably &ndash; pre-trained models, cloud GPUs, better documentation. But the gap between &ldquo;can run a model&rdquo; and &ldquo;understands what the model is doing&rdquo; remained wide. We hired for ML literacy, not just ML capability, and that distinction mattered.</p>
<h3 id="looking-forward">Looking forward</h3>
<p>2017 will be about consolidation. Kubernetes will keep winning and the operational burden will drop as managed offerings improve. Go will gain traction in infrastructure tooling &ndash; I expect to see more CLIs, more Kubernetes operators, more network services written in Go. Serverless will get better but will remain niche for another year at least.</p>
<p>The thing I care about most heading into 2017: security as a first-class engineering concern, not a compliance checkbox. GDPR is coming. The breach headlines keep getting worse. At both companies I pushed for security review in the development workflow, not as a gate at the end. That investment will compound.</p>
<p>Discipline over heroics. That was the lesson of 2016. The teams that shipped reliably weren&rsquo;t the ones with the best engineers. They were the ones with the best habits.</p>
]]></content:encoded></item><item><title>Building Effective Engineering Teams</title><link>https://lawzava.com/blog/2016-12-05-building-effective-engineering-teams/</link><pubDate>Mon, 05 Dec 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-12-05-building-effective-engineering-teams/</guid><description>What a year of building an engineering team at a mobility startup taught me about hiring, trust, and the habits that actually matter.</description><content:encoded><![CDATA[<p>2016 is almost over, and I&rsquo;ve been running the engineering team at a mobility startup for about a year now. We started as three people with a monolith and a lot of ambition. We&rsquo;re ending the year as seven, with a handful of services, a real deployment pipeline, and a much better understanding of what makes a small team work.</p>
<p>Some of that understanding came from reading. Most of it came from making mistakes and then paying attention. The short version: small teams with clear ownership, written context, and permission to disagree will outperform larger teams that rely on heroics and tribal knowledge. I learned this by getting it wrong first.</p>
<h3 id="the-hire-that-changed-how-i-think-about-hiring">The hire that changed how I think about hiring</h3>
<p>Early on I hired someone brilliant. Strong technical skills, great in interviews, impressive background. Within two months the team was slower than before. Not because the person was bad at code. Because they were bad at collaboration. They dismissed questions in reviews. They rewrote other people&rsquo;s work without discussion. They treated every technical decision as a competition.</p>
<p>I waited too long to address it. I kept telling myself the technical output justified the friction. It didn&rsquo;t. By the time we parted ways, I had lost one engineer who quit quietly and another who stopped contributing ideas in meetings.</p>
<p>The lesson was simple and painful: a good engineer who makes the people around them worse is a net negative. I don&rsquo;t care how fast you can ship if the team ships slower because of you.</p>
<p>After that, every interview included a pairing session where I watched how candidates communicated under ambiguity. Not whether they got the answer, but how they handled not knowing it.</p>
<h3 id="writing-things-down-saved-us">Writing things down saved us</h3>
<p>The single highest-leverage practice we adopted was writing things down. Architecture decisions, deployment procedures, incident postmortems, onboarding steps. All of it.</p>
<p>This wasn&rsquo;t natural for us. We were a small startup. Everybody sat in the same room. Writing felt like bureaucracy. Then someone went on holiday for a week and we realized that half of our operational knowledge lived in one person&rsquo;s head. That isn&rsquo;t a team. That&rsquo;s a single point of failure with extra chairs.</p>
<p>We started with a rule: if you make a decision that affects how something works, write a short paragraph explaining what you decided and why. Not a formal document. Not a template. Just the decision and the reasoning. We kept these in a shared repo.</p>
<p>Within a month, arguments about past decisions dropped to near zero. New engineers could read the context instead of guessing. And when we revisited old decisions, we could evaluate them against the original reasoning instead of relying on memory.</p>
<p>Clarity scales better than talent. I believe that more every month.</p>
<h3 id="small-and-scoped-beats-big-and-versatile">Small and scoped beats big and versatile</h3>
<p>We tried having one team that did everything. Backend, frontend, infrastructure, mobile API work. It worked at three people. At five, it started to crack. Not because people were less capable, but because context switching became the dominant activity. Everybody was halfway through everything and fully done with nothing.</p>
<p>We split into two groups. One owned the rider-facing product and mobile API. The other owned fleet operations, the backend services, and infrastructure. Each group had enough skills to ship without waiting for the other.</p>
<p>The improvement was immediate. Cycle time dropped. Ownership became clear. People stopped asking &ldquo;who is working on that?&rdquo; because the answer was obvious from the structure.</p>
<p>I don&rsquo;t think there&rsquo;s a magic team size. But I do think the moment coordination overhead starts eating into building time, you have outgrown your current structure. Pay attention to that signal.</p>
<h3 id="psychological-safety-isnt-a-slogan">Psychological safety isn&rsquo;t a slogan</h3>
<p>I used to think psychological safety meant being nice. It doesn&rsquo;t. It means people can say &ldquo;I don&rsquo;t understand this&rdquo; or &ldquo;I think this is the wrong approach&rdquo; without fear. That&rsquo;s harder to build than it sounds.</p>
<p>The most useful thing I did was start admitting my own mistakes publicly. When I made a bad architectural call, I said so in standup. When I didn&rsquo;t know the answer, I said that too. It felt uncomfortable at first. But within a few weeks, other people started doing the same thing. Problems surfaced earlier. Fixes came faster.</p>
<p>The other thing that mattered was how we handled incidents. We ran blameless postmortems for every significant outage. No finger pointing. Just: what happened, why did our systems allow it, and what do we change so it doesn&rsquo;t happen again. We focused on the process, not the person. After a few rounds of this, people stopped hiding mistakes and started reporting them immediately. That alone probably saved us from two or three serious production incidents.</p>
<h3 id="conflict-is-data">Conflict is data</h3>
<p>In a small team, disagreements are inevitable. The question is whether you treat them as a threat or as useful information.</p>
<p>We had a multi-week argument about service boundaries. Two engineers had fundamentally different views on where to draw the lines. My instinct was to pick a side and move on. Instead, I made them write down their positions and trade-offs. Once everything was on paper, the disagreement was about specifics, not personalities, and we found a middle path that neither of them had originally proposed.</p>
<p>Now I encourage disagreement, as long as it stays about the work. When it gets personal, I address it immediately. Tolerating that even once sends a signal that undermines everything else.</p>
<h3 id="what-i-would-do-differently">What I would do differently</h3>
<p>I would hire for collaboration earlier and more deliberately. I would start writing decisions down from day one, not after the first crisis. I would split teams sooner. And I would be more honest with myself about when a personnel problem is actually a personnel problem, not a process problem.</p>
<p>None of this is revolutionary. But doing these things consistently, with discipline instead of heroics, made a bigger difference than any technical choice we made all year.</p>
<h3 id="the-team-you-build-builds-the-product">The team you build builds the product</h3>
<p>Building an effective engineering team isn&rsquo;t about assembling the best individuals. It&rsquo;s about creating conditions where normal people do exceptional work together. Write things down. Keep teams small and focused. Make it safe to disagree and to fail. Address problems when they are small. That&rsquo;s most of it.</p>
]]></content:encoded></item><item><title>Why We Chose Go for Our Backend Services</title><link>https://lawzava.com/blog/2016-11-28-why-we-chose-go-for-backend-services/</link><pubDate>Mon, 28 Nov 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-11-28-why-we-chose-go-for-backend-services/</guid><description>How Go became the default backend language at a mobility startup and a fintech startup, what it replaced, and the honest tradeoffs we accepted along the way.</description><content:encoded><![CDATA[<p>At a mobility startup we had a fleet of shared bikes, a mobile app, and a backend stitched together from Python and Node services. The system worked. Mostly. But every time we pushed past a few hundred concurrent riders the cracks showed: event loops choking on CPU-bound geofencing, Python processes eating RAM, and deploys that required matching the exact virtualenv on the target box or everything fell over.</p>
<p>The fintech startup had a different shape but similar pain. Financial news aggregation means constant HTTP polling, WebSocket fanout, and ML pipeline orchestration. The Node services were fast to write and slow to operate. Debugging a callback-hell crash at 3 AM on a service that processes market data isn&rsquo;t my idea of a good time.</p>
<p>We needed something that compiled to a single binary, handled thousands of concurrent connections without drama, and let a small team move fast without an ops nightmare.</p>
<h2 id="quick-take">Quick take</h2>
<p>Go won us over with fast compiles, dead-simple deployment, and goroutines that actually scale. The error handling verbosity is real and generics are nowhere in sight, but for network-heavy backend services in 2016, nothing else hit the same balance of speed, simplicity, and operational sanity.</p>
<h3 id="why-go">Why Go</h3>
<p>I started experimenting with Go around 1.5 and by the time 1.6 and 1.7 landed, I was convinced. Three things sold me.</p>
<p><strong>Compilation speed.</strong> Our Python and Node services had no compile step, which sounds like an advantage until you realize the &ldquo;compile step&rdquo; just moved to production as runtime errors. Go gave us a real compiler that caught problems early and still built a full service in seconds. Coming from a brief stint with Java microservices, this felt like cheating.</p>
<p><strong>Single binary deploys.</strong> One binary. No runtime. No dependency manager on the server. No &ldquo;works on my machine.&rdquo; We could cross-compile for Linux on a Mac, <code>scp</code> the binary to a server, and it ran. Later we moved to Docker, but even there the images were tiny because there was nothing to install.</p>
<p><strong>Goroutines and channels.</strong> This was the real hook. At the mobility startup, every bike sends a GPS heartbeat every few seconds. At the fintech startup, every news source gets polled on its own schedule. Both problems are embarrassingly concurrent. Go made that concurrency feel natural rather than bolted on.</p>
<p>Here is a simplified version of how we handled bike heartbeats:</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">processHeartbeats</span>(<span style="color:#a6e22e">heartbeats</span> <span style="color:#f92672">&lt;-</span><span style="color:#66d9ef">chan</span> <span style="color:#a6e22e">Heartbeat</span>, <span style="color:#a6e22e">store</span> <span style="color:#f92672">*</span><span style="color:#a6e22e">BikeStore</span>) {
</span></span><span style="display:flex;"><span>	<span style="color:#66d9ef">for</span> <span style="color:#a6e22e">hb</span> <span style="color:#f92672">:=</span> <span style="color:#66d9ef">range</span> <span style="color:#a6e22e">heartbeats</span> {
</span></span><span style="display:flex;"><span>		<span style="color:#66d9ef">go</span> <span style="color:#66d9ef">func</span>(<span style="color:#a6e22e">hb</span> <span style="color:#a6e22e">Heartbeat</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">store</span>.<span style="color:#a6e22e">UpdatePosition</span>(<span style="color:#a6e22e">hb</span>.<span style="color:#a6e22e">BikeID</span>, <span style="color:#a6e22e">hb</span>.<span style="color:#a6e22e">Lat</span>, <span style="color:#a6e22e">hb</span>.<span style="color:#a6e22e">Lng</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:#a6e22e">log</span>.<span style="color:#a6e22e">Printf</span>(<span style="color:#e6db74">&#34;failed to update bike %s: %v&#34;</span>, <span style="color:#a6e22e">hb</span>.<span style="color:#a6e22e">BikeID</span>, <span style="color:#a6e22e">err</span>)
</span></span><span style="display:flex;"><span>				<span style="color:#66d9ef">return</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">outsideGeofence</span>(<span style="color:#a6e22e">hb</span>.<span style="color:#a6e22e">Lat</span>, <span style="color:#a6e22e">hb</span>.<span style="color:#a6e22e">Lng</span>) {
</span></span><span style="display:flex;"><span>				<span style="color:#a6e22e">alertOps</span>(<span style="color:#a6e22e">hb</span>.<span style="color:#a6e22e">BikeID</span>)
</span></span><span style="display:flex;"><span>			}
</span></span><span style="display:flex;"><span>		}(<span style="color:#a6e22e">hb</span>)
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Nothing clever. No framework. Just goroutines, a channel, and straightforward control flow. A junior engineer could read this and understand what happens. That mattered to me more than elegance.</p>
<h3 id="what-we-gained">What we gained</h3>
<p>The improvements were immediate and measurable.</p>
<p>Memory usage on the bike tracking service dropped from around 400 MB under the Python version to about 30 MB with Go. Tail latency at p99 went from unpredictable spikes to a consistent sub-10ms for the heartbeat path. Deploy time went from &ldquo;run Ansible and hope&rdquo; to &ldquo;copy binary and restart.&rdquo;</p>
<p>At the fintech startup the news ingestion pipeline handled 3x the source count on the same hardware after the rewrite. We stopped worrying about whether a stalled HTTP client would block the event loop because there was no event loop to block.</p>
<p>The standard library deserves credit too. <code>net/http</code>, <code>encoding/json</code>, <code>crypto/tls</code> &ndash; all solid, all built in. We pulled in very few external dependencies in the first year, which meant fewer things to audit and fewer things to break.</p>
<h3 id="the-honest-downsides">The honest downsides</h3>
<p>Go isn&rsquo;t perfect. I would be lying if I said the tradeoffs didn&rsquo;t sting sometimes.</p>
<p><strong>Error handling verbosity.</strong> <code>if err != nil</code> after every single call. I&rsquo;ve written that line thousands of times. It makes control flow explicit, which I genuinely value, but it also adds noise. Some files are 40% error checks. You get used to it, but it never stops being verbose.</p>
<p><strong>No generics.</strong> In late 2016 this is Go&rsquo;s most obvious gap. Every time I write a utility function that works on a slice, I either duplicate it for each type or reach for <code>interface{}</code> and lose type safety. We ended up with a <code>pkg/sliceutil</code> package full of type-specific helpers that felt like something a code generator should handle. It works, but it isn&rsquo;t satisfying.</p>
<p><strong>Smaller ecosystem.</strong> Python has a library for everything. Go in 2016 has a library for most things, but the quality varies and some areas are just thin. ORM support, for instance, was rough. We ended up writing raw SQL with <code>database/sql</code> and honestly that turned out to be a better long-term choice, but it was more work upfront.</p>
<p><strong>Dependency management.</strong> In 2016, Go&rsquo;s dependency story is a mess. Vendoring, <code>godep</code>, <code>glide</code>, and no official solution. We settled on <code>glide</code> and it mostly worked, but it wasn&rsquo;t a confidence-inspiring experience. This is the one area where I envied the Node and Python ecosystems.</p>
<h3 id="what-i-would-tell-another-cto">What I would tell another CTO</h3>
<p>If your backend is mostly network services &ndash; APIs, data pipelines, real-time processing &ndash; Go is a strong default in 2016. The language is deliberately boring, and boring is a feature when you&rsquo;re running production systems with a small team.</p>
<p>Don&rsquo;t adopt Go because it&rsquo;s trendy. Adopt it because you want fast compiles, predictable performance, and deploys that don&rsquo;t require a prayer. Then accept the verbosity and the missing generics as the price of admission.</p>
<p>Start with one service. Pick the one that hurts the most operationally. Rewrite it in Go, measure the difference, and let the results speak. That&rsquo;s what we did at the mobility startup with the heartbeat service, and within two months every new backend service was Go by default.</p>
<h3 id="looking-ahead">Looking ahead</h3>
<p>Go 1.7 just landed with context in the standard library, which cleaned up a lot of our cancellation and timeout patterns. The tooling keeps getting better. <code>gofmt</code> alone has saved us more arguments than any code review policy.</p>
<p>I don&rsquo;t know if Go will get generics. I hope it does. But even without them, the language has earned its place in our stack through discipline and simplicity. Not everything needs to be expressive. Sometimes you just need it to work, to be fast, and to be obvious.</p>
<p>That&rsquo;s Go for me. And I&rsquo;m not going back.</p>
]]></content:encoded></item><item><title>The Economics of State: Why Scaling Up Beats Sharding (Until It Doesn't)</title><link>https://lawzava.com/blog/2016-11-14-scaling-postgresql-replication-sharding-beyond/</link><pubDate>Mon, 14 Nov 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-11-14-scaling-postgresql-replication-sharding-beyond/</guid><description>A production-grounded case for exhausting single-server headroom with pooling, replicas, and partitioning before taking on sharding complexity.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>The short version: state scales cheapest when you get more out of one machine before you distribute. Connection pooling is free headroom, replication is the first major lever, and partitioning buys time before sharding complexity. I&rsquo;ve been through this progression at the fintech startup and the order matters more than any single technique.</p>
<hr>
<h3 id="the-context">The context</h3>
<p>At the fintech startup we ingest financial news from thousands of sources. Earnings calls, filings, analyst reports, social signals &ndash; all of it lands in PostgreSQL. When I joined as CTO, the database was a single server doing everything: writes from the ingestion pipeline, reads from the API, analytics queries from internal tools. It worked. Then the data volume doubled in three months and it stopped working.</p>
<p>This post is what I learned fixing it. Not theory. The actual progression we followed, the configs we used, and the tradeoffs we hit.</p>
<h3 id="find-the-bottleneck-first">Find the bottleneck first</h3>
<p>Everything looks the same when the database is slow. Queries pile up, latency spikes, the app feels sluggish. But the fix depends entirely on where the pressure is coming from.</p>
<p>Read pressure means CPU is saturated serving SELECT queries. Write pressure means WAL generation and fsync are the bottleneck. Connection pressure means the process-per-connection model is eating memory. Storage pressure means the tables are too big for efficient vacuuming and indexing.</p>
<p>At the fintech startup, we had all four. But they didn&rsquo;t all matter equally. Connection pressure was killing us first because the ingestion workers each held their own connection. We had maybe 300 workers and the server was spending more time context-switching between backends than doing actual work.</p>
<h3 id="connection-pooling-with-pgbouncer">Connection pooling with PgBouncer</h3>
<p>This was the first fix and it was nearly free. PgBouncer sits between the application and PostgreSQL, multiplexes hundreds of client connections over a small pool of real database connections, and eliminates the fork-per-connection overhead.</p>
<p>Here is close to what we ran:</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-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#66d9ef">[databases]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">fintech_db</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">host=127.0.0.1 port=5433 dbname=fintech_db</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">[pgbouncer]</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">listen_addr</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">0.0.0.0</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">listen_port</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">5432</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">auth_type</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">md5</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">auth_file</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">/etc/pgbouncer/userlist.txt</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">pool_mode</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">transaction</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">max_client_conn</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">2000</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">default_pool_size</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">30</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">reserve_pool_size</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">5</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">reserve_pool_timeout</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">3</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">server_idle_timeout</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">300</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">server_lifetime</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">3600</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">log_connections</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">0</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">log_disconnections</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">0</span>
</span></span></code></pre></div><p>A few things worth noting. <code>pool_mode = transaction</code> is the right default for almost every workload. It returns the server connection to the pool after each transaction completes, so 2000 application connections share 30 real database connections. Session mode keeps the connection pinned for the entire client session, which defeats the purpose. Statement mode is the most aggressive but breaks anything that uses multi-statement transactions or prepared statements.</p>
<p><code>reserve_pool_size</code> gives you a small buffer for traffic spikes. If all 30 connections are busy, PgBouncer will open up to 5 more for a short window before rejecting clients. This saved us during batch ingestion runs where write volume would spike for a few minutes.</p>
<p>The result: our 300 ingestion workers plus API servers plus internal tools all shared 30 actual PostgreSQL backends. Memory usage on the database server dropped by 40%. Query latency improved because the server wasn&rsquo;t spending cycles managing hundreds of idle connections.</p>
<h3 id="streaming-replication">Streaming replication</h3>
<p>Pooling bought us time. The next bottleneck was read traffic. The API served search results, financial summaries, and news feeds &ndash; all read-heavy queries competing with the write path on a single server.</p>
<p>PostgreSQL streaming replication is straightforward. The primary streams WAL (Write-Ahead Log) segments to one or more standbys, which replay them and serve read traffic. Here is the relevant part of the primary&rsquo;s configuration:</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-ini" data-lang="ini"><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:#a6e22e">wal_level</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">replica</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">max_wal_senders</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">5</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">wal_keep_segments</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">64</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># For monitoring replication lag</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">track_commit_timestamp</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">on</span>
</span></span></code></pre></div><p>And the standby&rsquo;s <code>recovery.conf</code>:</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-ini" data-lang="ini"><span style="display:flex;"><span><span style="color:#a6e22e">standby_mode</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">on</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">primary_conninfo</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;host=primary.internal port=5432 user=replicator password=xxx&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">trigger_file</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#39;/tmp/postgresql.trigger&#39;</span>
</span></span></code></pre></div><p>We set up two read replicas behind a simple connection routing layer in Go. Write queries went to the primary. Read queries went to the replicas using round-robin. Nothing fancy.</p>
<p>The thing people underestimate about replication is lag. A user writes a comment, the API returns success, the user refreshes and the comment isn&rsquo;t there because the replica hasn&rsquo;t caught up. At the fintech startup this mattered less because our data was financial news &ndash; a few hundred milliseconds of lag on search results is invisible. But for any read-after-write path, you need to either route reads back to the primary for a short window or track replication position and only read from replicas that have caught up.</p>
<p>We monitored lag with a simple query on the replicas:</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:#66d9ef">SELECT</span>
</span></span><span style="display:flex;"><span>  now() <span style="color:#f92672">-</span> pg_last_xact_replay_timestamp() <span style="color:#66d9ef">AS</span> replication_lag;
</span></span></code></pre></div><p>In steady state we saw 50-200ms of lag. During batch ingestion spikes it would climb to 1-2 seconds. We set alerts at 5 seconds. If lag hits 10 seconds, something is wrong with the replica&rsquo;s I/O or the WAL shipping is backing up.</p>
<h3 id="partitioning-for-high-volume-tables">Partitioning for high-volume tables</h3>
<p>After pooling and replicas, the next problem was table size. Our main events table held every financial event we had ever ingested. Hundreds of millions of rows. VACUUM took hours. Index rebuilds blocked writes. Queries that should have been fast were scanning enormous B-trees.</p>
<p>In 2016, PostgreSQL doesn&rsquo;t have native declarative partitioning. That&rsquo;s coming in version 10. What we&rsquo;ve is table inheritance with CHECK constraints and manual routing. It&rsquo;s ugly. It works.</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">-- Parent table
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> financial_events (
</span></span><span style="display:flex;"><span>    id          BIGSERIAL,
</span></span><span style="display:flex;"><span>    event_time  <span style="color:#66d9ef">TIMESTAMP</span> <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
</span></span><span style="display:flex;"><span>    source_id   INTEGER <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
</span></span><span style="display:flex;"><span>    event_type  VARCHAR(<span style="color:#ae81ff">50</span>) <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>,
</span></span><span style="display:flex;"><span>    payload     JSONB <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</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">-- Monthly partitions
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> financial_events_2016_10 (
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">CHECK</span> (event_time <span style="color:#f92672">&gt;=</span> <span style="color:#e6db74">&#39;2016-10-01&#39;</span> <span style="color:#66d9ef">AND</span> event_time <span style="color:#f92672">&lt;</span> <span style="color:#e6db74">&#39;2016-11-01&#39;</span>)
</span></span><span style="display:flex;"><span>) <span style="color:#66d9ef">INHERITS</span> (financial_events);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TABLE</span> financial_events_2016_11 (
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">CHECK</span> (event_time <span style="color:#f92672">&gt;=</span> <span style="color:#e6db74">&#39;2016-11-01&#39;</span> <span style="color:#66d9ef">AND</span> event_time <span style="color:#f92672">&lt;</span> <span style="color:#e6db74">&#39;2016-12-01&#39;</span>)
</span></span><span style="display:flex;"><span>) <span style="color:#66d9ef">INHERITS</span> (financial_events);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- Indexes on each partition, not the parent
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_fe_2016_10_time <span style="color:#66d9ef">ON</span> financial_events_2016_10 (event_time);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_fe_2016_10_source <span style="color:#66d9ef">ON</span> financial_events_2016_10 (source_id);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_fe_2016_11_time <span style="color:#66d9ef">ON</span> financial_events_2016_11 (event_time);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> idx_fe_2016_11_source <span style="color:#66d9ef">ON</span> financial_events_2016_11 (source_id);
</span></span></code></pre></div><p>The insert routing was a trigger function:</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:#66d9ef">CREATE</span> <span style="color:#66d9ef">OR</span> <span style="color:#66d9ef">REPLACE</span> <span style="color:#66d9ef">FUNCTION</span> financial_events_insert_trigger()
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">RETURNS</span> <span style="color:#66d9ef">TRIGGER</span> <span style="color:#66d9ef">AS</span> <span style="color:#960050;background-color:#1e0010">$$</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">BEGIN</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">IF</span> <span style="color:#66d9ef">NEW</span>.event_time <span style="color:#f92672">&gt;=</span> <span style="color:#e6db74">&#39;2016-11-01&#39;</span> <span style="color:#66d9ef">AND</span> <span style="color:#66d9ef">NEW</span>.event_time <span style="color:#f92672">&lt;</span> <span style="color:#e6db74">&#39;2016-12-01&#39;</span> <span style="color:#66d9ef">THEN</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">INSERT</span> <span style="color:#66d9ef">INTO</span> financial_events_2016_11 <span style="color:#66d9ef">VALUES</span> (<span style="color:#66d9ef">NEW</span>.<span style="color:#f92672">*</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">ELSIF</span> <span style="color:#66d9ef">NEW</span>.event_time <span style="color:#f92672">&gt;=</span> <span style="color:#e6db74">&#39;2016-10-01&#39;</span> <span style="color:#66d9ef">AND</span> <span style="color:#66d9ef">NEW</span>.event_time <span style="color:#f92672">&lt;</span> <span style="color:#e6db74">&#39;2016-11-01&#39;</span> <span style="color:#66d9ef">THEN</span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">INSERT</span> <span style="color:#66d9ef">INTO</span> financial_events_2016_10 <span style="color:#66d9ef">VALUES</span> (<span style="color:#66d9ef">NEW</span>.<span style="color:#f92672">*</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">-- ... older months
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">ELSE</span>
</span></span><span style="display:flex;"><span>        RAISE <span style="color:#66d9ef">EXCEPTION</span> <span style="color:#e6db74">&#39;No partition for event_time %&#39;</span>, <span style="color:#66d9ef">NEW</span>.event_time;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">END</span> <span style="color:#66d9ef">IF</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">RETURN</span> <span style="color:#66d9ef">NULL</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">END</span>;
</span></span><span style="display:flex;"><span><span style="color:#960050;background-color:#1e0010">$$</span> <span style="color:#66d9ef">LANGUAGE</span> plpgsql;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">CREATE</span> <span style="color:#66d9ef">TRIGGER</span> insert_financial_events
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">BEFORE</span> <span style="color:#66d9ef">INSERT</span> <span style="color:#66d9ef">ON</span> financial_events
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">FOR</span> <span style="color:#66d9ef">EACH</span> <span style="color:#66d9ef">ROW</span> <span style="color:#66d9ef">EXECUTE</span> <span style="color:#66d9ef">FUNCTION</span> financial_events_insert_trigger();
</span></span></code></pre></div><p>Yes, you have to maintain this trigger as you create new partitions. We wrote a cron job in Go that created next month&rsquo;s partition and updated the trigger on the first of every month. Not glamorous. Reliable.</p>
<p>The wins were immediate. VACUUM on a monthly partition with 20 million rows takes minutes, not hours. Queries with <code>WHERE event_time BETWEEN ...</code> hit only the relevant partitions because constraint exclusion prunes the rest. And when data aged past our retention window, we dropped entire partitions instead of running massive DELETE queries &ndash; <code>DROP TABLE financial_events_2015_06</code> is instant and generates zero dead tuples.</p>
<h3 id="sharding-the-last-resort">Sharding: the last resort</h3>
<p>We didn&rsquo;t shard at the fintech startup. I want to be honest about that. We got to the edge where it was on the table, but the combination of pooling, two replicas, and monthly partitioning handled our volume. We were ingesting roughly 50,000 events per minute at peak and PostgreSQL on decent hardware with these optimizations kept up.</p>
<p>But I&rsquo;ve seen sharding done at a previous company, and my strong opinion is: don&rsquo;t do it unless you have exhausted everything above and a single primary genuinely can&rsquo;t handle the write throughput.</p>
<p>Sharding means splitting data across independent PostgreSQL instances. You pick a shard key &ndash; usually a tenant ID or a hash of some natural key &ndash; and route writes and reads to the correct shard. The application or a middleware layer owns the routing.</p>
<p>The hidden costs are brutal.  <a href="/blog/2016-08-15-database-migrations-without-downtime/"
   
   >Schema migrations</a>
 have to be applied to every shard. Cross-shard queries become application-level aggregation. Rebalancing shards when data distribution skews means moving live data between databases. Transactions that span shards are either impossible or require two-phase commit, which is slow and fragile. Every operational runbook gets multiplied by the number of shards.</p>
<p>If you&rsquo;re at the point where sharding is necessary, you have a big enough team and budget to handle the operational complexity. If you don&rsquo;t have that team, sharding will hurt more than the performance problem it solves.</p>
<h3 id="the-progression-matters">The progression matters</h3>
<p>The order isn&rsquo;t arbitrary.</p>
<ol>
<li><strong>Pooling</strong> is nearly free and should be in place from day one. There&rsquo;s no reason to let PostgreSQL manage hundreds of connections directly.</li>
<li><strong>Read replicas</strong> are the first real scaling lever. They offload the most common pressure (reads) with minimal application changes.</li>
<li><strong>Partitioning</strong> makes large tables manageable and solves vacuum, indexing, and retention problems that replicas don&rsquo;t help with.</li>
<li><strong>Sharding</strong> is the nuclear option. Powerful. Expensive. Irreversible in practice.</li>
</ol>
<p>Each step is worth a significant amount of effort to delay the next one. We spent a week tuning PgBouncer and it delayed the need for replicas by two months. We spent two weeks setting up replication and it delayed the partitioning work by four months. Partitioning has delayed any sharding conversation indefinitely.</p>
<h3 id="what-i-would-do-differently">What I would do differently</h3>
<p>I would set up PgBouncer before the first production deploy, not after the first connection storm. I would build the partitioning infrastructure from the start for any table expected to grow past 50 million rows. And I would invest more in monitoring replication lag early &ndash; we flew blind for the first few weeks after setting up replicas and got lucky that lag never caused a visible bug.</p>
<p>Scaling PostgreSQL is a sequence, not a leap. Each step buys you months. Skip the sequence and you pay for every shortcut at once.</p>
]]></content:encoded></item><item><title>Building a Security-First Engineering Culture</title><link>https://lawzava.com/blog/2016-10-03-building-security-first-engineering-culture/</link><pubDate>Mon, 03 Oct 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-10-03-building-security-first-engineering-culture/</guid><description>Security culture is not a training program or a tool purchase. It is a set of habits that leadership enforces through consistency, not speeches.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Stop treating security as a gate at the end of the pipeline. Embed it into how your team thinks, designs, and ships. This requires specific policies, visible leadership, and the willingness to slow down a release when the answer to &ldquo;what happens if this gets compromised?&rdquo; is &ldquo;I don&rsquo;t know.&rdquo;</p>
<h3 id="security-culture-isnt-a-project">Security culture isn&rsquo;t a project</h3>
<p>I&rsquo;ve watched teams try to buy their way into security. They purchase a scanner, run a penetration test once a year, and call it done. Then a junior engineer hardcodes an API key, pushes it to a public repo, and suddenly the scanner doesn&rsquo;t matter.</p>
<p>Security culture is the thing that prevents that push in the first place. Not a tool. A habit.</p>
<p>At the fintech startup, we handle financial data. Market signals, user portfolios, payment information. There&rsquo;s no version of &ldquo;we&rsquo;ll fix it later&rdquo; that regulators or users will accept. Security had to be foundational from the start, not bolted on after the first scare.</p>
<h3 id="what-cyber-defense-exercises-taught-me">What cyber-defense exercises taught me</h3>
<p>A principle I carry from national cyber-defense exercises: the single biggest lesson wasn&rsquo;t technical. It was organizational. The teams that performed well weren&rsquo;t the ones with the best tools. They were the ones where every person understood the threat model and acted accordingly without waiting for permission.</p>
<p>That stuck with me. Security at scale is a culture problem, not an engineering problem.</p>
<h3 id="the-policies-i-actually-enforce">The policies I actually enforce</h3>
<p>Theory is cheap. Here are the specific rules I&rsquo;ve implemented across teams.</p>
<p><strong>No secrets in code. Ever.</strong> We use environment variables and a secrets manager. The CI pipeline fails if it detects anything that looks like a key or credential in the codebase. This is automated, not optional, and not overridable without a written justification that I personally review.</p>
<p><strong>Every pull request gets a security question.</strong> Not a full threat model. Just one question: &ldquo;What is the worst thing that happens if this code is exploited?&rdquo; If the author can&rsquo;t answer it, the PR doesn&rsquo;t merge. This forces engineers to think about attack surface as part of their daily work, not as a separate exercise.</p>
<p><strong>Least privilege by default.</strong> New services start with zero permissions and add only what they need. New employees get read access to the repositories they work on and nothing else. Escalation requires a request and a reason. I review access quarterly and revoke anything that isn&rsquo;t actively justified.</p>
<p><strong>Dependency updates aren&rsquo;t optional.</strong> We track dependencies weekly. Known vulnerabilities get patched within 48 hours for critical severity, one week for high. This is a policy, not a suggestion. I&rsquo;ve delayed feature work to meet these windows.</p>
<p><strong>Incident response is rehearsed.</strong> We run a tabletop exercise every quarter. Not a checkbox drill. A real scenario where I throw a curveball halfway through to see how the team adapts. The ones who have done this three or four times respond to real incidents with calm instead of panic.</p>
<h3 id="the-hard-part-is-consistency">The hard part is consistency</h3>
<p>Any team can write a security policy document. The hard part is enforcing it on the days when you&rsquo;re behind on a deadline and the shortcut is right there.</p>
<p>I&rsquo;ve blocked releases. I&rsquo;ve told product managers that a feature would ship a week late because the authentication flow wasn&rsquo;t reviewed. Those conversations are uncomfortable. They are also the moments that define whether your culture is real or performative.</p>
<p>Engineers watch what leadership does, not what leadership says. If the CTO merges a PR that skips the security review because &ldquo;we need to ship,&rdquo; every engineer on the team learns that security is negotiable. It takes one exception to undo months of habit building.</p>
<h3 id="security-champions-scale-your-coverage">Security champions scale your coverage</h3>
<p>I can&rsquo;t review every line of code. No security team can. The answer isn&rsquo;t to hire more security people. It&rsquo;s to make every engineer a little bit dangerous.</p>
<p>At the fintech startup, I designated one engineer on each team as a security champion. Not a full-time role. Maybe 10-15% of their time. They attend a monthly session where we review recent vulnerabilities in our stack, discuss attack patterns relevant to fintech, and update our threat model.</p>
<p>These champions become the first line of defense in code reviews. They catch the obvious issues before they reach me, and they raise the questions that nobody else on the team would think to ask.</p>
<p>The key is that being a security champion is respected. It counts in performance reviews. It isn&rsquo;t extra work on top of their real job. It&rsquo;s part of their real job.</p>
<h3 id="make-the-secure-path-the-easy-path">Make the secure path the easy path</h3>
<p>If doing the right thing is harder than doing the wrong thing, people will do the wrong thing. This isn&rsquo;t a moral failing. It&rsquo;s human behavior.</p>
<p>We built internal libraries that handle authentication, input validation, and encryption correctly. When an engineer needs to call an external API, they use our wrapper that handles TLS verification, credential injection, and audit logging automatically. The secure path is also the path with less code to write.</p>
<p>We templated our infrastructure so that new services deploy with network isolation, encrypted storage, and logging enabled by default. An engineer has to go out of their way to deploy something insecure. That friction is intentional.</p>
<h3 id="incidents-are-data-not-blame">Incidents are data, not blame</h3>
<p>When something goes wrong, and it will, the response determines whether your culture strengthens or collapses.</p>
<p>We run blameless postmortems. Not because blame feels bad, but because blame destroys information flow. If engineers fear punishment, they hide mistakes. Hidden mistakes compound. The breach that takes down a company is rarely the first failure. It&rsquo;s the tenth failure that nobody reported because the first person who reported one got burned.</p>
<p>Every postmortem produces exactly two outputs: a timeline of what happened, and a list of specific changes to prevent recurrence. Not &ldquo;be more careful.&rdquo; Specific changes. A new automated check. A revised permission. A policy update.</p>
<h3 id="security-is-a-daily-decision">Security is a daily decision</h3>
<p>Security culture is discipline. It&rsquo;s the same decision made correctly a thousand times, even when it&rsquo;s inconvenient. Especially when it&rsquo;s inconvenient.</p>
<p>You don&rsquo;t build it with a training video or an annual audit. You build it by making security part of every design discussion, every code review, every deployment decision. You build it by enforcing the standards you set, including on yourself.</p>
<p>Discipline over heroics. Every time.</p>
]]></content:encoded></item><item><title>Why Every Developer Should Understand Networking</title><link>https://lawzava.com/blog/2016-09-19-why-every-developer-should-understand-networking/</link><pubDate>Mon, 19 Sep 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-09-19-why-every-developer-should-understand-networking/</guid><description>Too many developers treat the network as magic. It bites them in production every time.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Learn how DNS, TCP, and load balancers actually work, or keep debugging the wrong layer at 2 AM.</p>
<h3 id="the-network-isnt-magic">The network isn&rsquo;t magic</h3>
<p>I keep having the same conversation at a mobility startup. A developer opens a ticket: &ldquo;the API is randomly slow.&rdquo; I ask them to run <code>ss -tan</code> on the box. Blank stare. They have never heard of socket states.</p>
<p>This isn&rsquo;t a knowledge gap I can ignore. If you write software that talks over a network &ndash; and in 2016, that&rsquo;s almost all software &ndash; then networking is part of your job. Not optional. Not &ldquo;nice to have.&rdquo; Part of the job.</p>
<h3 id="three-bugs-that-keep-showing-up">Three bugs that keep showing up</h3>
<p><strong>DNS caching gone wrong.</strong> We had a service that cached DNS lookups forever because the HTTP client&rsquo;s default resolver never respected TTL. We rotated an IP behind a load balancer, and that one service kept hammering the old address for hours. The fix was two lines of config. The outage was forty-five minutes of confusion because nobody thought to check resolution.</p>
<p><strong>TIME_WAIT exhaustion.</strong> A microservice opened a new TCP connection for every request to our mapping provider. Under load, the box ran out of ephemeral ports. Connections backed up, timeouts cascaded, and the bike unlock flow broke. The developer who wrote it had no idea that TCP connections linger in TIME_WAIT after close. Connection pooling fixed it. Understanding why connection pooling matters would have prevented it.</p>
<p><strong>Load balancer health checks.</strong> We added a new backend behind an Nginx upstream. The health check was hitting a path that returned 200 even when the database was unreachable. Traffic routed to a box that couldn&rsquo;t serve real requests. Fifteen minutes of partial outage because nobody understood what the load balancer was actually checking.</p>
<p>None of these required deep packet analysis. None required a networking degree. They required knowing the basics well enough to ask the right question.</p>
<h3 id="why-developers-avoid-it">Why developers avoid it</h3>
<p>Networking feels like someone else&rsquo;s problem. There&rsquo;s an infrastructure team, or a cloud provider, or an abstraction layer that&rsquo;s supposed to handle it. And those things help. But abstractions leak. They always leak. When they do, the developer staring at the logs needs to know which layer is broken.</p>
<p>&ldquo;It works on my machine&rdquo; is almost always a networking statement. Different DNS, different routes, different timeouts, different connection behavior. If you can&rsquo;t reason about the network, you can&rsquo;t debug the gap between your laptop and production.</p>
<h3 id="where-to-start">Where to start</h3>
<p>You don&rsquo;t need to read RFCs. You need a working mental model. These resources got our team there:</p>
<ul>
<li><strong>&ldquo;TCP/IP Illustrated, Volume 1&rdquo; by Stevens.</strong> Dense but precise. Read the chapters on TCP connection states and you will understand half the production issues you have ever seen.</li>
<li><strong>Julia Evans&rsquo; networking zines.</strong> Approachable, visual, and surprisingly deep. Start with the one on DNS.</li>
<li><strong><code>ss</code>, <code>dig</code>, <code>curl -v</code>, <code>tcpdump</code>.</strong> Run them. Break things on purpose. Watch what happens when you kill a connection mid-handshake or poison a DNS cache.</li>
</ul>
<p>The network isn&rsquo;t going to become simpler. The developers who understand it will debug faster, design better systems, and stop filing tickets that say &ldquo;the API is randomly slow.&rdquo;</p>
]]></content:encoded></item><item><title>Database Migrations Without Downtime</title><link>https://lawzava.com/blog/2016-08-15-database-migrations-without-downtime/</link><pubDate>Mon, 15 Aug 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-08-15-database-migrations-without-downtime/</guid><description>A practical guide to evolving schemas without maintenance windows by keeping old and new code compatible at every step.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>If your migration strategy is &ldquo;take the site down, run ALTER TABLE, pray, bring it back up,&rdquo; you&rsquo;re doing it wrong. Every schema change can be decomposed into steps that keep old and new code running simultaneously. It takes more discipline but zero heroics.</p>
<h3 id="the-problem-with-just-run-the-migration">The problem with &ldquo;just run the migration&rdquo;</h3>
<p>At the fintech startup we serve financial data. Market data doesn&rsquo;t pause because your engineering team needs a maintenance window. A feed that goes dark during trading hours is a feed that loses subscribers. When I took over database operations, the existing pattern was to schedule migrations at 2 AM on Sundays, take the API offline, run the DDL, and hope the application came back cleanly. It worked until it didn&rsquo;t.</p>
<p>The breaking point was a column type change on a table with 40 million rows of historical price data. The migration ran for 47 minutes. During that time the table was locked, the API returned errors, and a downstream consumer silently switched to a stale cache it never recovered from. We spent the next two days cleaning up data consistency issues.</p>
<p>After that I decided every migration would be zero-downtime or it wouldn&rsquo;t ship.</p>
<h3 id="the-expand-and-contract-pattern">The expand-and-contract pattern</h3>
<p>The core idea is simple. Instead of making a breaking change in one step, you split it into phases where the schema is always compatible with whatever application code is currently running.</p>
<p><strong>Expand</strong>: add the new structure alongside the old one. Both coexist.</p>
<p><strong>Migrate</strong>: deploy code that writes to both old and new, reads from new. Backfill historical data.</p>
<p><strong>Contract</strong>: once every running instance uses the new structure, remove the old one.</p>
<p>This pattern handles nearly every schema change. The details vary, but the rhythm stays the same.</p>
<h3 id="adding-a-column-safely">Adding a column safely</h3>
<p>The simplest case. A nullable column with no default doesn&rsquo;t rewrite the table in PostgreSQL. It&rsquo;s metadata-only and takes a lock for milliseconds.</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:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> trades <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">COLUMN</span> settlement_date DATE;
</span></span></code></pre></div><p>Fast. Safe. Old code ignores the column. New code starts writing to it.</p>
<p>If you need a NOT NULL constraint, don&rsquo;t add it in the same statement. PostgreSQL will scan the entire table to verify the constraint, holding an ACCESS EXCLUSIVE lock the whole time. Instead:</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">-- Step 1: add nullable column
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> trades <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">COLUMN</span> settlement_date DATE;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- Step 2: backfill in batches (see below)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- Step 3: add constraint without full table scan
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> trades <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">CONSTRAINT</span> trades_settlement_not_null
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">CHECK</span> (settlement_date <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">NULL</span>) <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">VALID</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- Step 4: validate separately (only takes a SHARE UPDATE EXCLUSIVE lock)
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> trades VALIDATE <span style="color:#66d9ef">CONSTRAINT</span> trades_settlement_not_null;
</span></span></code></pre></div><p>The <code>NOT VALID</code> trick is critical on large tables. It tells PostgreSQL to enforce the constraint on new writes immediately but skip the full table scan. The <code>VALIDATE</code> step runs later with a weaker lock that doesn&rsquo;t block writes. On our 40-million-row tables, this was the difference between a 200-millisecond migration and a 20-minute outage.</p>
<h3 id="renaming-a-column">Renaming a column</h3>
<p>You don&rsquo;t rename columns in production. Full stop.</p>
<p>What you actually do is expand-and-contract. Here is the concrete sequence we used when renaming <code>price</code> to <code>unit_price</code> on the trades table:</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">-- Phase 1: Expand
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> trades <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">COLUMN</span> unit_price NUMERIC(<span style="color:#ae81ff">18</span>,<span style="color:#ae81ff">8</span>);
</span></span></code></pre></div><p>Deploy code that writes to both <code>price</code> and <code>unit_price</code>. Reads come from <code>unit_price</code> with a fallback to <code>price</code>.</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">-- Phase 2: Backfill
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">UPDATE</span> trades <span style="color:#66d9ef">SET</span> unit_price <span style="color:#f92672">=</span> price
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> unit_price <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">AND</span> id <span style="color:#66d9ef">BETWEEN</span> <span style="color:#ae81ff">1</span> <span style="color:#66d9ef">AND</span> <span style="color:#ae81ff">100000</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- Repeat in batches...
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">UPDATE</span> trades <span style="color:#66d9ef">SET</span> unit_price <span style="color:#f92672">=</span> price
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> unit_price <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">AND</span> id <span style="color:#66d9ef">BETWEEN</span> <span style="color:#ae81ff">100001</span> <span style="color:#66d9ef">AND</span> <span style="color:#ae81ff">200000</span>;
</span></span></code></pre></div><p>Once backfill is complete and every application instance reads from <code>unit_price</code>:</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">-- Phase 3: Contract
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> trades <span style="color:#66d9ef">DROP</span> <span style="color:#66d9ef">COLUMN</span> price;
</span></span></code></pre></div><p>Three deployments minimum. That&rsquo;s the cost. The benefit is zero downtime and a clean rollback at every step.</p>
<h3 id="changing-a-column-type">Changing a column type</h3>
<p>Same pattern, different details. We had a case where an instrument identifier was stored as <code>INTEGER</code> but needed to become <code>TEXT</code> to support a new exchange format.</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">-- Expand
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> instruments <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">COLUMN</span> external_id_new TEXT;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">-- Backfill
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">UPDATE</span> instruments <span style="color:#66d9ef">SET</span> external_id_new <span style="color:#f92672">=</span> external_id::TEXT
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> external_id_new <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">AND</span> id <span style="color:#66d9ef">BETWEEN</span> <span style="color:#ae81ff">1</span> <span style="color:#66d9ef">AND</span> <span style="color:#ae81ff">50000</span>;
</span></span></code></pre></div><p>Deploy dual-write code. Cut over reads. Drop the old column.</p>
<p>The temptation is to use <code>ALTER COLUMN ... TYPE TEXT</code> which rewrites the entire table under an exclusive lock. On a table with millions of rows of financial data that&rsquo;s actively being queried, that isn&rsquo;t an option.</p>
<h3 id="batched-backfills-that-dont-kill-the-database">Batched backfills that don&rsquo;t kill the database</h3>
<p>Large backfills are where most zero-downtime migrations go wrong. A single <code>UPDATE ... WHERE condition</code> on 40 million rows will generate enormous WAL, bloat the table, and hold locks that block concurrent operations.</p>
<p>The pattern I use:</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:#66d9ef">DO</span> <span style="color:#960050;background-color:#1e0010">$$</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">DECLARE</span>
</span></span><span style="display:flex;"><span>  batch_size INT :<span style="color:#f92672">=</span> <span style="color:#ae81ff">5000</span>;
</span></span><span style="display:flex;"><span>  max_id BIGINT;
</span></span><span style="display:flex;"><span>  current_id BIGINT :<span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">BEGIN</span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">SELECT</span> <span style="color:#66d9ef">MAX</span>(id) <span style="color:#66d9ef">INTO</span> max_id <span style="color:#66d9ef">FROM</span> trades;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  WHILE current_id <span style="color:#f92672">&lt;</span> max_id LOOP
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">UPDATE</span> trades
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">SET</span> unit_price <span style="color:#f92672">=</span> price
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">WHERE</span> id <span style="color:#f92672">&gt;</span> current_id
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">AND</span> id <span style="color:#f92672">&lt;=</span> current_id <span style="color:#f92672">+</span> batch_size
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">AND</span> unit_price <span style="color:#66d9ef">IS</span> <span style="color:#66d9ef">NULL</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    current_id :<span style="color:#f92672">=</span> current_id <span style="color:#f92672">+</span> batch_size;
</span></span><span style="display:flex;"><span>    PERFORM pg_sleep(<span style="color:#ae81ff">0</span>.<span style="color:#ae81ff">1</span>);  <span style="color:#75715e">-- breathe
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>    <span style="color:#66d9ef">COMMIT</span>;
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">END</span> LOOP;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">END</span> <span style="color:#960050;background-color:#1e0010">$$</span>;
</span></span></code></pre></div><p>The <code>pg_sleep</code> matters. Without it, you saturate I/O and  <a href="/blog/2016-07-18-building-resilient-systems-lessons-from-production-failures/"
   
   >replication lag</a>
 spikes. At the fintech startup our replicas served read traffic for dashboards and analytics. A backfill that caused 30 seconds of replication lag would show stale prices to every user watching a portfolio. We settled on batches of 5,000 rows with 100ms pauses. The backfill took longer but production stayed healthy.</p>
<h3 id="indexes-without-locking">Indexes without locking</h3>
<p><code>CREATE INDEX</code> on PostgreSQL takes a SHARE lock on the table, which blocks writes for the duration of the build. On a large table, that can be minutes.</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:#66d9ef">CREATE</span> <span style="color:#66d9ef">INDEX</span> CONCURRENTLY idx_trades_settlement <span style="color:#66d9ef">ON</span> trades(settlement_date);
</span></span></code></pre></div><p><code>CONCURRENTLY</code> builds the index without blocking writes. It takes longer and does two table scans instead of one, but it&rsquo;s the only option for production tables. There are two caveats worth knowing:</p>
<p>First, it can&rsquo;t run inside a transaction. If your migration tool wraps everything in a transaction, you need to handle this case separately.</p>
<p>Second, if it fails partway through, it leaves an invalid index behind. Check <code>pg_stat_user_indexes</code> and drop the invalid one before retrying.</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:#66d9ef">SELECT</span> indexrelid::regclass, indisvalid
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">FROM</span> pg_index
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">WHERE</span> <span style="color:#66d9ef">NOT</span> indisvalid;
</span></span></code></pre></div><h3 id="foreign-keys-without-blocking">Foreign keys without blocking</h3>
<p>Adding a foreign key constraint with <code>ADD CONSTRAINT ... FOREIGN KEY</code> does a full table scan under an ACCESS EXCLUSIVE lock. Same trick as NOT NULL:</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:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> trades
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">ADD</span> <span style="color:#66d9ef">CONSTRAINT</span> fk_trades_instrument
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">FOREIGN</span> <span style="color:#66d9ef">KEY</span> (instrument_id) <span style="color:#66d9ef">REFERENCES</span> instruments(id)
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">NOT</span> <span style="color:#66d9ef">VALID</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">ALTER</span> <span style="color:#66d9ef">TABLE</span> trades VALIDATE <span style="color:#66d9ef">CONSTRAINT</span> fk_trades_instrument;
</span></span></code></pre></div><p>The first statement adds the constraint for new writes only, taking a brief lock. The second validates existing rows with a much weaker lock.</p>
<h3 id="the-deployment-sequence">The deployment sequence</h3>
<p>The order matters. Getting it wrong means you have a window where the application and schema disagree.</p>
<ol>
<li>
<p><strong>Run expand migration.</strong> New columns, new indexes (concurrently), new constraints (NOT VALID). Schema is now compatible with both old and new code.</p>
</li>
<li>
<p><strong>Deploy new application code.</strong> Writes to both old and new columns. Reads from new with fallback to old.</p>
</li>
<li>
<p><strong>Run backfill.</strong> In batches, during low traffic if possible. Monitor replication lag and query latency.</p>
</li>
<li>
<p><strong>Verify completeness.</strong> <code>SELECT COUNT(*) FROM trades WHERE unit_price IS NULL</code> should be zero.</p>
</li>
<li>
<p><strong>Deploy read-cutover code.</strong> Reads exclusively from new columns. Still writes to both.</p>
</li>
<li>
<p><strong>Run contract migration.</strong> Drop old columns, drop temporary constraints. Schema is clean.</p>
</li>
</ol>
<p>Each step is independently reversible. If the backfill causes problems, pause it. If the new code has bugs, roll back the deployment. The old schema is still there. This is the entire point.</p>
<h3 id="what-this-costs">What this costs</h3>
<p>More deployments. More code that handles two schemas simultaneously. Migrations that used to be one PR become three or four. Application code carries temporary dual-write logic that gets cleaned up in the contract phase.</p>
<p>It&rsquo;s more work. But at the fintech startup, the alternative was telling financial data consumers that we needed a maintenance window during market hours. That conversation never goes well.</p>
<p>The discipline pays off in a different way too. When every migration follows expand-and-contract,  <a href="/blog/2016-06-06-continuous-deployment-without-chaos/"
   
   >deployments become boring</a>
. Nobody pages you at 2 AM for a schema change. Nobody holds their breath during a release. The process is mechanical and predictable.</p>
<p>The best migration is the one nobody notices. Boring by design, invisible in production.</p>
]]></content:encoded></item><item><title>Hiring Engineers When You Can't Compete on Salary</title><link>https://lawzava.com/blog/2016-08-01-hiring-engineers-when-you-cant-compete-on-salary/</link><pubDate>Mon, 01 Aug 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-08-01-hiring-engineers-when-you-cant-compete-on-salary/</guid><description>You cannot outpay Big Tech, but you can outshine it on impact, growth, autonomy, and clarity. This is how to hire great engineers with a startup offer in 2016.</description><content:encoded><![CDATA[<p><strong>Stop trying to match Big Tech comp. You will lose that fight every time.</strong> Sell what they structurally can&rsquo;t offer: real ownership, fast feedback loops, and the chance to build something from scratch with a small team that actually ships.</p>
<p>I&rsquo;ve been building the engineering team at a mobility startup for the past several months. Shared mobility is still early. We&rsquo;re pre-Lime, pre-Bird, pre-everything. The concept is unproven, the funding is modest, and the salary bands reflect that reality.</p>
<p>Every good candidate I talk to has at least one offer from a company that can pay 30-50% more than I can. Some of them have offers with stock grants that look like a second salary. I can&rsquo;t match that. I learned early to stop pretending I could.</p>
<p>But we&rsquo;ve still managed to hire well. Here is what actually worked.</p>
<h3 id="be-honest-about-money-on-the-first-call">Be honest about money on the first call</h3>
<p>Nothing kills trust faster than dancing around compensation. I bring it up in the first conversation. &ldquo;Here is what we can pay. Here is the equity. Here is what the equity is worth today, which isn&rsquo;t much, and here is what it could be worth if we execute.&rdquo; No hand-waving. No &ldquo;competitive salary&rdquo; language that means nothing.</p>
<p>Most engineers respect directness. The ones who are only optimizing for cash will self-select out, and that&rsquo;s fine. The ones who stay in the conversation are evaluating the whole picture, and I want to talk to those people.</p>
<h3 id="show-the-work-not-a-pitch-deck">Show the work, not a pitch deck</h3>
<p>At the mobility startup I started doing something that felt risky at first: I gave candidates access to our actual codebase during the interview process. Not all of it, but enough to see how we build. Our code reviews. Our deployment pipeline. Our monitoring setup. The real thing, not a sanitized version.</p>
<p>This did two things. First, it filtered for engineers who cared about craft. If someone looked at our setup and got excited about the problems, that told me more than any whiteboard exercise. Second, it made the job tangible. They weren&rsquo;t imagining what the work might be. They could see it.</p>
<p>One engineer told me he accepted our offer over a much higher one because, and I&rsquo;m quoting here, &ldquo;your codebase looked like people gave a damn.&rdquo; That stuck with me.</p>
<h3 id="sell-speed-not-startup-culture">Sell speed, not &ldquo;startup culture&rdquo;</h3>
<p>I never use the phrase startup culture in interviews. It means nothing. What I do talk about is speed. At the mobility startup, an engineer can go from idea to production in a day. Not because we skip process, but because our process is light and the team is small enough that decisions happen fast.</p>
<p>I tell candidates about specific examples. Last month one of our engineers noticed a pattern in user drop-offs, proposed a fix, built it, shipped it, and we saw the numbers move within 48 hours. At a big company that same change goes through three teams, two planning cycles, and a prioritization meeting. Maybe it ships in a quarter.</p>
<p>Speed isn&rsquo;t about working more hours. It&rsquo;s about fewer layers between you and the outcome. That&rsquo;s genuinely compelling to a certain kind of engineer, and those are exactly the engineers I want.</p>
<h3 id="autonomy-has-to-be-real">Autonomy has to be real</h3>
<p>Every startup claims to offer autonomy. Most of them mean &ldquo;we don&rsquo;t have enough managers yet.&rdquo; That isn&rsquo;t autonomy. That&rsquo;s chaos.</p>
<p>Real autonomy means an engineer picks the approach, owns the tradeoffs, and lives with the consequences. At the mobility startup, our engineers choose their tools. They design their own systems. They are on call for what they build, which means they build things that don&rsquo;t break at 3 AM.</p>
<p>When I interview, I&rsquo;m specific about this. I describe a recent technical decision and who made it. Not me. The engineer closest to the problem. If a candidate has been stuck in an environment where every architecture choice needs three sign-offs, that story lands hard.</p>
<h3 id="hire-for-the-team-not-the-role">Hire for the team, not the role</h3>
<p>I can&rsquo;t afford specialists. What I can afford is sharp generalists who get better at everything by doing everything. At our size, an engineer might write a service, set up the monitoring, debug a production issue, and review someone else&rsquo;s database migration in the same week.</p>
<p>Some people hate that. Others thrive on it. I screen for the second group by asking about the last time they worked outside their comfort zone and what they learned. The answers tell me whether someone wants range or just wants to go deeper on one thing. Neither is wrong, but only one fits where we&rsquo;re right now.</p>
<h3 id="the-peer-interview-matters-most">The peer interview matters most</h3>
<p>The single best recruiting tool I have is my existing team. When a candidate meets the people they would work with every day and those people are sharp, thoughtful, and honest about the hard parts of the job, that&rsquo;s more persuasive than anything I can say.</p>
<p>I make sure candidates spend real time with the team. Not a panel interview. A working session or a design conversation. Something that feels like the actual job. If they click with the team, the salary gap shrinks in their mental math. If they don&rsquo;t click, no amount of money would have made it work anyway.</p>
<h3 id="what-i-got-wrong-early-on">What I got wrong early on</h3>
<p>I wasted time in the beginning trying to compete on perks. Free lunches, flexible hours, the usual startup bingo card. None of it moved the needle. Engineers saw through it immediately.</p>
<p>What moved the needle was being specific and being honest. Specific about the problems we&rsquo;re solving, the systems we&rsquo;re building, and where we&rsquo;re going. Honest about what is hard, what is broken, and what we haven&rsquo;t figured out yet.</p>
<p>The best hires we&rsquo;ve made came from conversations where I said something like, &ldquo;Our deployment process isn&rsquo;t great yet and you would own fixing it.&rdquo; That kind of honesty attracts builders. Glossy pitches attract people who want to be at a company that sounds good, not people who want to do the work.</p>
<h3 id="the-uncomfortable-truth">The uncomfortable truth</h3>
<p>You will lose candidates to higher offers. It will happen regularly. Some of those candidates would have been great, and it will sting.</p>
<p>But the engineers who join you despite the lower salary are making a deliberate choice. They are choosing the work, the team, and the trajectory over the paycheck. From what I&rsquo;ve seen, those people build better things and stay longer. Not always. But often enough that I&rsquo;ve stopped trying to win the compensation game and started focusing on winning the &ldquo;is this work worth doing&rdquo; game instead.</p>
<p>That&rsquo;s a game a startup can actually win.</p>
]]></content:encoded></item><item><title>Building Resilient Systems: Lessons from Production Failures</title><link>https://lawzava.com/blog/2016-07-18-building-resilient-systems-lessons-from-production-failures/</link><pubDate>Mon, 18 Jul 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-07-18-building-resilient-systems-lessons-from-production-failures/</guid><description>Production incidents show where architecture bends and breaks. Lessons on designing for failure, limiting blast radius, and making recovery routine.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Your failover probably doesn&rsquo;t work. Test it before 3 AM teaches you that lesson instead.</p>
<h3 id="the-night-our-failover-lied-to-us">The Night Our Failover Lied to Us</h3>
<p>June, peak season at a mobility startup. We had just crossed a threshold where our fleet was large enough that evening ride demand was genuinely stressing our backend. The database was a PostgreSQL primary with a streaming replica. Standard setup. We had tested promotion of the replica exactly once, four months earlier, during a quiet Tuesday afternoon. It worked fine then.</p>
<p>At 2:47 AM on a Thursday, the primary ran out of disk. My fault. I had bumped up WAL retention for debugging a replication lag issue two weeks prior and forgot to revert it. The primary filled its volume, panicked, and went read-only.</p>
<p>No problem, I thought. We have a replica. I&rsquo;ll promote it and we&rsquo;re back.</p>
<p>The replica was 11 hours behind.</p>
<p>Eleven hours. The replication lag had been silently growing for days. Our monitoring checked that the replica process was running. It didn&rsquo;t check how far behind it actually was. The process was alive and healthy. The data wasn&rsquo;t.</p>
<p>So now I&rsquo;m sitting in my kitchen at 3 AM with a read-only primary that has current data and a replica that thinks it&rsquo;s yesterday afternoon. If I promote the replica, I lose every ride, every payment, every account change from the last 11 hours. If I don&rsquo;t promote it, the entire system stays read-only and nobody can start a ride.</p>
<p>I ended up provisioning a new volume, copying the data directory from the primary, and mounting it with more space. Took about 40 minutes. Forty minutes of complete service outage during which bikes were locked all over the city and our support inbox was filling up.</p>
<p>The postmortem was humbling. We had a failover strategy that we believed worked because we had tested it once under ideal conditions. We had monitoring that checked the wrong thing. And the root cause was a config change I made and forgot about. No exotic bug. No sophisticated attack. Just a forgotten setting, a lazy health check, and an untested assumption.</p>
<p>That night changed how I think about resilience.</p>
<h3 id="failures-are-ordinary-cascades-arent">Failures Are Ordinary. Cascades Aren&rsquo;t.</h3>
<p>Every system I&rsquo;ve operated has failed. Networks drop packets. Services crash when they leak memory. Hardware dies. Certificates expire. Third-party APIs go down at the worst possible moment.</p>
<p>None of that&rsquo;s surprising. The question is never whether something will fail. It&rsquo;s whether one failure drags everything else down with it.</p>
<p>After the mobility startup&rsquo;s incident, I started categorizing failures differently. I stopped caring about the probability of individual failures and started obsessing over blast radius. A database going read-only is a problem. A database going read-only that also kills authentication, ride tracking, and payment processing is a catastrophe.</p>
<h3 id="degradation-is-a-feature">Degradation Is a Feature</h3>
<p>When a dependency fails, the system needs a plan that isn&rsquo;t &ldquo;wait and hope.&rdquo; At the mobility startup, after the outage, we built explicit degradation modes. If the database went read-only, users could still end active rides using cached state. They couldn&rsquo;t start new ones, but at least nobody was stranded.</p>
<p>This isn&rsquo;t a good experience. It&rsquo;s a usable one. That distinction matters more than most engineers think. Users tolerate &ldquo;the app is slow right now&rdquo; much better than &ldquo;the app is completely dead.&rdquo; Give them something, anything, while you fix the real problem.</p>
<p>A recommendation engine falls back to popular items. A payment system queues orders for later. A search feature lets people browse categories. None of these are ideal. All of them buy you time.</p>
<h3 id="isolation-keep-the-fire-in-one-room">Isolation: Keep the Fire in One Room</h3>
<p>The reason our outage at the mobility startup was total instead of partial is that everything depended on everything. One database, one connection pool, one failure domain. A spike in ride-end processing could starve the authentication path. A slow query in analytics could block real-time tracking.</p>
<p>After the incident, we separated concerns aggressively.</p>
<p><strong>Bulkheads.</strong> Dedicated connection pools for critical paths versus background work. If the analytics queries get slow, they burn their own pool, not the one serving live rides.</p>
<p><strong>Circuit breakers.</strong> If a dependency is unhealthy, hammering it makes things worse. We wrapped external calls so that after enough failures, the system stops trying and returns a fallback immediately. Simple implementation:</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">class</span> <span style="color:#a6e22e">CircuitBreaker</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">def</span> <span style="color:#a6e22e">call</span>(self, func, <span style="color:#f92672">*</span>args, <span style="color:#f92672">**</span>kwargs):
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> self<span style="color:#f92672">.</span>state <span style="color:#f92672">==</span> <span style="color:#e6db74">&#34;open&#34;</span> <span style="color:#f92672">and</span> <span style="color:#f92672">not</span> self<span style="color:#f92672">.</span>_should_attempt_recovery():
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">raise</span> CircuitOpenError()
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">try</span>:
</span></span><span style="display:flex;"><span>            result <span style="color:#f92672">=</span> func(<span style="color:#f92672">*</span>args, <span style="color:#f92672">**</span>kwargs)
</span></span><span style="display:flex;"><span>            self<span style="color:#f92672">.</span>_on_success()
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">return</span> result
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">except</span> <span style="color:#a6e22e">Exception</span>:
</span></span><span style="display:flex;"><span>            self<span style="color:#f92672">.</span>_on_failure()
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">raise</span>
</span></span></code></pre></div><p>Nothing clever. That&rsquo;s the point. Resilience mechanisms should be boring and predictable.</p>
<p><strong>Timeouts on everything.</strong> Every external call gets a timeout. Without one, a slow dependency consumes your capacity until you&rsquo;re the slow dependency for someone else. Start generous, tighten based on real latency data.</p>
<p><strong>Rate limiting.</strong> Overload is a failure mode. Treat it like one. When you hit a limit, respond clearly and fast so callers know when to retry.</p>
<h3 id="redundancy-you-actually-test">Redundancy You Actually Test</h3>
<p>We had a replica. We thought we had redundancy. We didn&rsquo;t. We had a replica that made us feel safe without actually being safe.</p>
<p>After that night, the rule became: if you haven&rsquo;t failed over to it this month, it doesn&rsquo;t count as redundancy. Stateless services are easy. Any instance handles any request. Stateful systems are harder. Replication is the tool, but the tradeoffs matter. Synchronous replication costs latency and gives you stronger durability. Asynchronous replication is faster but you can lose recent writes. Pick one and understand what you&rsquo;re accepting.</p>
<p>The important part is exercising the failover path regularly. Not reading a runbook. Actually doing it. In production, during business hours, with someone watching the metrics.</p>
<h3 id="observability-that-checks-the-right-thing">Observability That Checks the Right Thing</h3>
<p>Our monitoring checked &ldquo;is the replica process running?&rdquo; It should have checked &ldquo;how many bytes behind is the replica?&rdquo; Those are very different questions.</p>
<p>After the outage, I rewrote our monitoring with a simple principle: monitor what the user experiences, not what the machine reports. Error rates. Latency percentiles. Successful ride starts per minute. If those numbers move, something is wrong, and you don&rsquo;t need to know which machine is unhappy to start responding.</p>
<p>Structured logging with correlation IDs so you can trace a request across services. Distributed tracing to see where time goes. These aren&rsquo;t optional for anything beyond a single-process application.</p>
<p>But the biggest lesson was this: your monitoring must not share fate with the thing it monitors. If your alerting depends on the same database that just died, you won&rsquo;t get the alert. We learned that one the hard way too, but that&rsquo;s a different story.</p>
<h3 id="recovery-as-a-practiced-skill">Recovery as a Practiced Skill</h3>
<p>Automated recovery should be the default for common failures. Health checks, orchestrator restarts, connection retry logic. These fix most issues without waking anyone up.</p>
<p>Rollback needs to be fast and safe. Keep previous builds ready. Make database migrations backward-compatible so you can roll back the application without rolling back the schema. Practice the rollback path before you need it at 3 AM.</p>
<p>After major incidents, bring services back gradually. Watch the metrics. Pause if anything looks wrong. The urge to flip everything back on at once is strong. Resist it.</p>
<h3 id="discipline-over-heroics">Discipline Over Heroics</h3>
<p>The 3 AM kitchen table fix worked. I got the system back up. People called it heroic. It wasn&rsquo;t. It was a failure of discipline that required an emergency response.</p>
<p>Heroic fixes feel good in the moment. Blameless postmortems that ship actual fixes feel better six months later when the same scenario hits and the system handles it without waking anyone up.</p>
<p>On-call should be sustainable. Clear escalation paths. Well-maintained runbooks. If your on-call rotation burns people out, your system isn&rsquo;t resilient, you&rsquo;re just subsidizing its fragility with human suffering.</p>
<h3 id="build-for-the-3-am-you-havent-met-yet">Build for the 3 AM you haven&rsquo;t met yet</h3>
<p>Build for failure. Contain blast radius. Rehearse recovery. Monitor what matters. When these become routine, most production failures turn into minor disruptions instead of long outages.</p>
<p>The replica lag incident at the mobility startup was painful. But it taught me something I still carry: the system you think you have and the system you actually have are different things. The only way to close that gap is to test your assumptions regularly and honestly. Not once on a quiet Tuesday. Every month, under real load, with real stakes.</p>
]]></content:encoded></item><item><title>The Real Cost of Running Your Own Servers in 2016</title><link>https://lawzava.com/blog/2016-07-05-the-real-cost-of-running-your-own-servers/</link><pubDate>Tue, 05 Jul 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-07-05-the-real-cost-of-running-your-own-servers/</guid><description>Most startups have no business running their own servers. The math is not close.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Your startup isn&rsquo;t Netflix. Use managed cloud services and ship your product.</p>
<h3 id="stop-pretending">Stop Pretending</h3>
<p>I keep meeting startup founders who want to run their own servers. They pull up a spreadsheet showing that a dedicated box costs less per month than the equivalent EC2 instance. They are right about that one number and wrong about everything else.</p>
<p>Running your own hardware means you&rsquo;re now in two businesses: whatever your startup actually does, and running a small data center. Most teams can&rsquo;t do one of those well. Trying to do both is how you burn six months and ship nothing.</p>
<h3 id="the-math-nobody-wants-to-do">The Math Nobody Wants to Do</h3>
<p>A decent server costs around $5,000. Two for redundancy, so $10,000 up front. Now add the parts people skip.</p>
<p>Colocation: $500 to $1,000 a month for power, cooling, and rack space. Call it $750.</p>
<p>An infrastructure engineer who can keep those boxes alive, patched, and secure. In 2016 that&rsquo;s $120,000 a year minimum, fully loaded. Probably more. Even part-time, you&rsquo;re spending real money on someone whose job isn&rsquo;t your product.</p>
<p>Networking gear, firewalls, backups, monitoring. Another few thousand a year if you&rsquo;re doing it properly. More if you aren&rsquo;t, because you will pay for it later in downtime and data loss.</p>
<p>Add it up for one year: $10,000 hardware plus $9,000 colo plus $120,000 engineer plus maybe $5,000 in miscellaneous gear and software. That&rsquo;s roughly $144,000 to run two servers.</p>
<p>The same capacity on AWS runs maybe $300 to $500 a month depending on instance type and reservations. Call it $5,000 a year. Even with managed database, load balancer, and S3 you&rsquo;re probably under $15,000. And you didn&rsquo;t hire anyone whose job is keeping hardware alive.</p>
<p>The gap isn&rsquo;t subtle. It&rsquo;s an order of magnitude.</p>
<h3 id="but-what-about-scale">But What About Scale</h3>
<p>Yes, at serious scale the economics shift. If you&rsquo;re running hundreds of servers at steady load around the clock, owned hardware can make sense. That&rsquo;s a real conversation for companies with predictable workloads and existing ops teams.</p>
<p>You&rsquo;re a startup. You don&rsquo;t have predictable workloads. You don&rsquo;t have an ops team. You might not have product-market fit yet. Optimizing infrastructure cost isn&rsquo;t your problem. Shipping fast enough to survive is your problem.</p>
<h3 id="the-hidden-cost-that-kills-you">The Hidden Cost That Kills You</h3>
<p>The real damage isn&rsquo;t even the money. It&rsquo;s the time.</p>
<p>Provisioning a new server in your colo takes days or weeks. Ordering, shipping, racking, imaging, hardening. On AWS you have a new instance in minutes. When you need to test a hypothesis, run an experiment, or scale for a launch, that difference is existential.</p>
<p>I&rsquo;ve watched startups spend months building infrastructure that AWS would have given them out of the box. Months they didn&rsquo;t have. Some of them are gone now.</p>
<h3 id="when-i-would-consider-it">When I Would Consider It</h3>
<p>You have a stable, profitable business with predictable load. You already employ infrastructure engineers. You have compliance requirements that make cloud genuinely harder. You&rsquo;re processing data volumes where transfer costs dominate.</p>
<p>If none of those apply, you shouldn&rsquo;t be thinking about this.</p>
<p>For a startup in 2016, running your own servers is almost always vanity dressed up as frugality. The cloud isn&rsquo;t cheap, but it&rsquo;s cheaper than hiring ops staff, renting rack space, and losing months of engineering time to problems that are already solved.</p>
<p>Use AWS. Use managed Postgres. Use managed Redis. Ship your product. Worry about optimizing infrastructure costs when you have the kind of revenue that makes it worth optimizing.</p>
<p>Spend money on the problems that are actually yours. Infrastructure is someone else&rsquo;s problem until your revenue proves otherwise.</p>
]]></content:encoded></item><item><title>Continuous Deployment Without the Chaos</title><link>https://lawzava.com/blog/2016-06-06-continuous-deployment-without-chaos/</link><pubDate>Mon, 06 Jun 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-06-06-continuous-deployment-without-chaos/</guid><description>Continuous deployment is a discipline problem, not a tooling problem. We deploy a mobility startup&amp;amp;rsquo;s backend dozens of times a day because we built habits first.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>The teams that deploy often without breaking things aren&rsquo;t smarter or better staffed. They are more disciplined. CD is a set of habits enforced by automation, not a Jenkins pipeline you install and forget.</p>
<h3 id="deploying-dozens-of-times-a-day">Deploying Dozens of Times a Day</h3>
<p>At a mobility startup we deploy our backend services multiple times a day. Not because we decided to be trendy. Because our business requires it. When you&rsquo;re running a shared mobility platform, a broken deploy means bikes that don&rsquo;t unlock, payments that don&rsquo;t process, and users who open a competitor&rsquo;s app. The cost of a bad release is immediate and measurable.</p>
<p>That urgency forced us to take continuous deployment seriously much earlier than I expected. We were a small team, moving fast, and the only way to ship that fast without constant fires was to build discipline into the pipeline itself.</p>
<h3 id="cd-is-a-discipline-problem">CD Is a Discipline Problem</h3>
<p>Most teams that fail at continuous deployment fail because they treat it as a tooling problem. They install Jenkins, wire up a webhook, and wonder why production keeps breaking on Friday afternoon.</p>
<p>The tooling is the easy part. Jenkins works. GitLab CI is getting better by the month. The hard part is the set of habits that make frequent deploys safe. Habits like writing tests that actually catch regressions. Habits like reviewing changes with deployment risk in mind. Habits like keeping every change small enough to reason about when something goes wrong at midnight.</p>
<p>I&rsquo;ve watched teams with excellent pipelines ship broken code because nobody enforced the discipline around the pipeline. And I&rsquo;ve watched teams with mediocre tooling ship reliably because their habits were strong. Discipline beats tooling every time.</p>
<h3 id="the-pipeline-we-actually-run">The Pipeline We Actually Run</h3>
<p>Our Jenkins setup is straightforward. On every push, it runs the unit tests. If those pass, it builds the Docker image, tags it with the commit SHA, and pushes it to our private registry. Integration tests run against that image. If everything is green, it deploys to staging automatically. Production deploys happen after a manual approval step that takes about ten seconds because the person approving has already seen the diff, the test results, and the staging behavior.</p>
<p>The whole cycle from push to production takes under fifteen minutes on a good day. That speed matters. When the feedback loop is short, developers catch their own mistakes before context switches away. When the loop is long, commits pile up, responsibility diffuses, and nobody knows which change caused the problem.</p>
<p>We also run a nightly full regression suite that catches the slower, more expensive test cases. Those don&rsquo;t block individual deploys, but a red nightly build stops all production deploys the next morning until someone fixes it. No exceptions.</p>
<h3 id="tests-you-can-trust">Tests You Can Trust</h3>
<p>A continuous deployment pipeline is only as strong as the tests it runs. Flaky tests are poison. A test that fails randomly teaches the team to ignore failures. Once that habit forms, real failures get waved through too.</p>
<p>We spent weeks stabilizing our test suite before we trusted it to gate production deploys. That meant removing tests that depended on timing, isolating tests that shared state, and replacing slow end-to-end tests with faster contract tests where possible. It was unglamorous work. It was also the single highest-leverage investment we made in our deployment process.</p>
<p>The rule is simple: if a test fails, it means something is wrong with the code, not with the test. Any test that violates that rule gets fixed or deleted. There&rsquo;s no middle ground.</p>
<h3 id="small-changes-fast-rollback">Small Changes, Fast Rollback</h3>
<p>Every change we deploy is small. Not because we have a policy document that says so. Because small changes are easier to review, easier to test, and critically easier to roll back.</p>
<p>Rollback isn&rsquo;t an afterthought in our process. It&rsquo;s a first-class operation. We keep the previous three Docker images tagged and ready. Rolling back means pointing the load balancer at the previous image. It takes less than a minute. We practice it regularly, not just when things are on fire.</p>
<p>The worst continuous deployment failures I&rsquo;ve seen happened because rollback was theoretical. Someone wrote a wiki page describing the rollback steps, nobody ever tested them, and when the moment came, the steps didn&rsquo;t work. A rollback plan that hasn&rsquo;t been exercised isn&rsquo;t a plan. It&rsquo;s a wish.</p>
<h3 id="monitoring-that-closes-the-loop">Monitoring That Closes the Loop</h3>
<p>Deploying fast without watching the result is just shipping bugs faster. After every production deploy, we watch error rates, response latency, and a handful of business metrics for at least ten minutes. If anything moves in the wrong direction, we roll back first and investigate second.</p>
<p>This is where most teams cut corners. They deploy, see green in the pipeline, and move on. But the pipeline only tells you the tests passed. It doesn&rsquo;t tell you that the new code path is three times slower under real traffic, or that an edge case in the mobile client is now returning 500s.</p>
<p>We use basic Grafana dashboards with alerts that fire if error rate or p99 latency crosses a threshold within the first fifteen minutes after deploy. Nothing sophisticated. Just enough signal to catch the obvious problems before users do.</p>
<h3 id="when-cd-doesnt-fit">When CD Doesn&rsquo;t Fit</h3>
<p>I&rsquo;m not a purist about this. Some systems shouldn&rsquo;t deploy on every green build. Anything that touches payment processing gets an extra review gate. Schema migrations go through a separate, more careful process. And we don&rsquo;t pretend that mobile app releases can follow the same cadence as backend services when the App Store review cycle exists.</p>
<p>The point of continuous deployment isn&rsquo;t to deploy everything all the time. It&rsquo;s to make deployment a non-event for the systems where speed matters. For everything else, continuous delivery, where the artifact is always ready but the final step is manual, is perfectly fine.</p>
<h3 id="the-real-lesson">The Real Lesson</h3>
<p>After months of running this process, the lesson I keep coming back to is this: continuous deployment isn&rsquo;t about the pipeline. It&rsquo;s about the team&rsquo;s relationship with production.</p>
<p>When developers own the deploy and own the monitoring, they write different code. They write smaller changes. They write better tests. They think about rollback before they write the feature. That shift in mindset is worth more than any pipeline configuration.</p>
<p>The teams that deploy well aren&rsquo;t the ones with the best tools. They are the ones where shipping to production is a habit backed by discipline, not an event driven by hope.</p>
<h3 id="ship-the-habits-first">Ship the habits first</h3>
<p>If you want continuous deployment, start with discipline. Make your tests trustworthy. Make your changes small. Make rollback fast and practiced. Make monitoring non-negotiable. The tooling will follow. Jenkins, GitLab CI, whatever comes next &ndash; none of it matters if the habits aren&rsquo;t there first.</p>
]]></content:encoded></item><item><title>Security Incident Response for Startups</title><link>https://lawzava.com/blog/2016-05-23-security-incident-response-for-startups/</link><pubDate>Mon, 23 May 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-05-23-security-incident-response-for-startups/</guid><description>A practical incident response playbook for small teams: define incidents, assign owners, contain fast, investigate calmly, and recover with clear communication.</description><content:encoded><![CDATA[<p>It was a Tuesday in February, around 11 PM. I was finishing a late sprint at our shared mobility startup when our monitoring lit up. Unusual outbound traffic from one of our API servers. Not a spike in user traffic. Not a deploy gone wrong. Something else entirely.</p>
<p>I pulled up the logs and saw authenticated requests hitting an internal endpoint we had deprecated months ago but never torn down. The requests were coming from a session token that belonged to an engineer who had left the company six weeks earlier. His credentials should have been revoked. They weren&rsquo;t. Somewhere between the offboarding checklist and our actual IAM setup, we had a gap.</p>
<p>The next four hours taught me more about incident response than any exercise or drill ever could. Not because the technical problem was hard. It wasn&rsquo;t. The deprecated endpoint was read-only, the data it exposed was limited, and the token hadn&rsquo;t been used for anything beyond what looked like automated probing. But the organizational response was chaos. Nobody knew who was supposed to make decisions. Our CEO was calling me every ten minutes asking for updates I didn&rsquo;t have. One engineer started revoking tokens across the board, which knocked real users offline. Another started restoring from backups before we understood what had actually happened.</p>
<p>We contained it within a few hours. The blast radius was small. But the recovery took three days because of the collateral damage from our own panic. The token revocation took down a payment processing integration. The premature backup restore overwrote six hours of legitimate transaction data. We spent more time cleaning up our response than we spent on the actual incident.</p>
<p>That night changed how I think about security incidents. The technical compromise was a footnote. The real damage came from not having a plan. From people acting fast without acting together. From urgency without structure.</p>
<p>I&rsquo;ve since built incident response processes at three different companies and helped several more. The lesson is always the same: the plan doesn&rsquo;t need to be sophisticated. It needs to exist, it needs to be practiced, and it needs to be clear about who does what. That&rsquo;s the bar. Most startups don&rsquo;t clear it.</p>
<p>Here is what I&rsquo;ve learned.</p>
<h3 id="the-line-between-event-and-incident">The line between event and incident</h3>
<p>Draw it before you need it. A security event is suspicious activity that might be nothing. A security incident is confirmed or strongly suspected unauthorized access that demands immediate action.</p>
<p>When you&rsquo;re unsure, treat it as an incident and downgrade later. This is a lesson reinforced in national cyber-defense exercises, and it applies just as much to a twelve-person startup. You can walk back an overreaction. You can&rsquo;t walk back a delayed containment that let an attacker pivot to your production database.</p>
<p>The distinction matters because it determines who gets woken up and how fast. If everything is an incident, people stop responding. If nothing is, you miss the real ones.</p>
<h3 id="ownership-before-the-alarm-goes-off">Ownership before the alarm goes off</h3>
<p>At our mobility startup, the problem wasn&rsquo;t that nobody cared. Everyone cared. That was the problem. Five people all making independent decisions in a crisis is worse than one person making imperfect decisions with authority.</p>
<p>You need three roles defined ahead of time:</p>
<p><strong>Incident commander.</strong> This person makes decisions and keeps the response coordinated. They don&rsquo;t need to be the most technical person. They need to be calm, organized, and empowered to say &ldquo;stop&rdquo; when someone is about to make things worse.</p>
<p><strong>Technical lead.</strong> This person drives the investigation and remediation. They decide what logs to pull, what systems to isolate, and what the containment strategy looks like.</p>
<p><strong>Communications lead.</strong> This person keeps internal and external messaging accurate. At a five-person startup, this might be the CEO. At a fifty-person company, it might be someone in ops or legal.</p>
<p>At a small startup, one person wears multiple hats. Fine. But the ownership has to be explicit. Write it down. Put it in the wiki. And for the love of everything, make sure the contact information works at 11 PM on a Tuesday. Test it quarterly. Phone numbers change. People leave. On-call lists rot faster than you think.</p>
<h3 id="severity-in-plain-language">Severity in plain language</h3>
<p>Fancy severity matrices are a waste of time at a startup. You need four levels that everyone understands without looking anything up:</p>
<p><strong>Critical.</strong> Active data exfiltration or production systems are compromised. Drop everything.</p>
<p><strong>High.</strong> Confirmed unauthorized access, scope unclear. Assemble the response team now.</p>
<p><strong>Medium.</strong> Suspected compromise, limited scope. Investigate within hours, not days.</p>
<p><strong>Low.</strong> Suspicious activity that needs a closer look. Triage it during business hours.</p>
<p>The most important thing about your severity levels is that people actually use them. If your incident commander has to consult a decision tree to figure out whether an incident is a P1 or a P2, you have failed.</p>
<h3 id="containment-is-where-most-startups-break">Containment is where most startups break</h3>
<p>Containment is the step where panic does the most damage. The goal is to stop the bleeding without cutting off the patient&rsquo;s blood supply.</p>
<p>Revoke compromised credentials immediately. Isolate affected systems from the network, but preserve evidence while you do it. Cut off active data exfiltration paths. If malware is involved, isolate the system and keep the sample for analysis.</p>
<p>Every containment action has a tradeoff. Revoking all tokens is safe but might take down production. Isolating a server preserves evidence but removes capacity. These are real decisions with real business impact. Make them intentionally, communicate the impact, and document why you chose what you chose.</p>
<p>The engineer at my startup who revoked every token in the system wasn&rsquo;t wrong to act fast. He was wrong to act alone, without telling anyone, during an active incident. By the time I realized what had happened, our payment integration was down and we were fielding angry support tickets alongside the security investigation. Two fires instead of one.</p>
<h3 id="investigation-slow-down-before-you-speed-up">Investigation: slow down before you speed up</h3>
<p>Once the immediate bleeding stops, resist the urge to fix everything at once. Investigation is about building a timeline and understanding the blast radius.</p>
<p>Pull logs across authentication, application, and network layers. Preserve forensic images before you start making changes. Build a timeline of attacker activity. Collect indicators of compromise so you can sweep for related access you might have missed.</p>
<p>Don&rsquo;t rush to restore service before you understand how the attacker got in. I&rsquo;ve seen startups bring compromised systems back online with the same vulnerability still open because they were in a hurry to get back to normal. That isn&rsquo;t recovery. That&rsquo;s a second incident waiting to happen.</p>
<h3 id="eradication-and-recovery">Eradication and recovery</h3>
<p>Eradication means removing every foothold. Close the vulnerability that allowed the initial access. Remove any malware or backdoors. Reset credentials that could have been compromised. Verify that no unauthorized accounts were created.</p>
<p>Attackers who know what they are doing leave multiple persistence mechanisms. A revoked token isn&rsquo;t enough if they also dropped an SSH key or created a service account. Validate your cleanup thoroughly.</p>
<p>Recovery means rebuilding from known-good sources when possible. Restore data from clean backups if needed. Bring systems back gradually and watch closely for signs of re-entry. The monitoring should be tighter after an incident, not looser.</p>
<h3 id="communication-during-a-crisis">Communication during a crisis</h3>
<p>Internal communication should be regular, calm, and honest. Keep leadership aligned on severity and business risk. Inform affected teams. Separate confirmed facts from open questions in every update. Nothing erodes trust faster than a retraction.</p>
<p>External communication should be coordinated with legal counsel, especially if customer data is involved. Move quickly, but don&rsquo;t speculate. Explain what happened, what data was affected, what you&rsquo;re doing about it, and what customers should do. If you&rsquo;re in fintech or health, map your regulatory notification requirements now. Not during the incident. Now.</p>
<p>At the shared mobility startup, we got this part right almost by accident. Our CEO was a former lawyer and insisted on reviewing every external statement. It slowed us down by an hour. It saved us from saying something incorrect that would have required a correction, which always looks worse than a slight delay.</p>
<h3 id="documentation-as-you-go">Documentation as you go</h3>
<p>Write it down in real time. Decisions, actions, evidence, timestamps. You will need this record for compliance, for legal protection, and for learning. It also prevents confusion when responders rotate or when you need to brief someone new.</p>
<p>Use a shared document. A Google Doc is fine. A dedicated incident management tool is better. The format matters less than the habit. If you aren&rsquo;t documenting during the incident, you won&rsquo;t remember accurately afterward. Memory under stress is unreliable.</p>
<h3 id="preparation-that-costs-almost-nothing">Preparation that costs almost nothing</h3>
<p><strong>Know your critical assets.</strong> What systems hold sensitive data? What access paths could cause maximum damage? If you can&rsquo;t answer these questions in under a minute, you aren&rsquo;t ready.</p>
<p><strong>Build detection that actually works.</strong> Centralized logs. Alerts on unusual patterns. At minimum, you should know when someone authenticates from a new location or when an API sees traffic outside normal bounds. This doesn&rsquo;t require expensive tooling. It requires attention.</p>
<p><strong>Write draft communication templates.</strong> Under pressure, writing coherent messages is harder than you think. Have templates for internal updates, customer notifications, and regulatory disclosures. Fill in the specifics during the incident, but don&rsquo;t start from a blank page.</p>
<p><strong>Run tabletop exercises.</strong> Pick a plausible scenario: stolen laptop, exposed database, compromised former employee credentials. Walk through the response as a team. These discussions take an hour and reveal gaps that would cost you days during a real incident.</p>
<p>The former employee scenario isn&rsquo;t hypothetical. It&rsquo;s what hit us. If we had run that exercise once, someone would have asked whether our offboarding process actually revoked all access. We would have checked. We would have found the gap. The incident would never have happened.</p>
<h3 id="the-real-lesson">The real lesson</h3>
<p>Incident response planning isn&rsquo;t paranoia. It&rsquo;s the cheapest insurance a startup can buy. A simple playbook, three named roles, and one tabletop exercise per quarter will put you ahead of ninety percent of companies your size.</p>
<p>You will still have incidents. The goal isn&rsquo;t to prevent them all. The goal is to respond with discipline instead of panic, contain the damage instead of amplifying it, and come out the other side with a team that trusts each other more, not less.</p>
<p>Preparation is what separates an incident from a disaster. Every time.</p>
]]></content:encoded></item><item><title>API Design Principles That Stand the Test of Time</title><link>https://lawzava.com/blog/2016-05-09-api-design-principles-that-stand-the-test-of-time/</link><pubDate>Mon, 09 May 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-05-09-api-design-principles-that-stand-the-test-of-time/</guid><description>Lessons from building a fintech financial data API: the REST conventions that actually matter, the ones that don&amp;amp;rsquo;t, and why consistency beats cleverness.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Most API design advice obsesses over naming conventions and HTTP verb purity. The stuff that actually keeps an API alive across years of rewrites is boring: predictable error shapes, stable pagination, and never surprising the client.</p>
<h3 id="the-api-that-taught-me">The API that taught me</h3>
<p>I&rsquo;ve been building APIs for a while, but the one that forced me to care about design was the fintech startup&rsquo;s financial data API. Financial news, sentiment scores, trending stories, all served to clients who ranged from solo quant traders to institutional data teams. These people don&rsquo;t file bug reports. They just leave.</p>
<p>The first version of that API was functional. It returned data. It had endpoints. But within weeks of onboarding external consumers, I learned that &ldquo;functional&rdquo; and &ldquo;well-designed&rdquo; are completely different things. A functional API returns the right data. A well-designed API lets a stranger integrate in an afternoon without asking you a single question.</p>
<h3 id="conventions-that-matter-vs-conventions-that-dont">Conventions that matter vs conventions that don&rsquo;t</h3>
<p>The REST community has opinions about everything. Plural vs singular nouns, nested resources, HATEOAS, content negotiation. I&rsquo;ve opinions too, and they come from watching real clients integrate with real APIs.</p>
<p><strong>What actually matters:</strong></p>
<p>Consistent response shapes. Every list endpoint should paginate the same way. Every error should have the same structure. Every create should return the created resource. I can&rsquo;t overstate this. When a client library author can write one generic handler for all your endpoints, you have won.</p>
<p>HTTP status codes used correctly. Not creatively, correctly. 200 for success, 201 for creation, 400 for bad input, 404 for missing resources, 429 for rate limits. If you return 200 with an error body, you&rsquo;re forcing every client to parse the response before knowing if the request worked. That&rsquo;s a tax on every integration.</p>
<p>Predictable field naming. Pick snake_case or camelCase and never mix them. We went with snake_case for the fintech startup&rsquo;s API because the majority of our consumers were Python shops. It doesn&rsquo;t matter which you pick. It matters that you pick one.</p>
<p><strong>What doesn&rsquo;t matter nearly as much as people think:</strong></p>
<p>Whether your endpoint says <code>/stories</code> or <code>/story</code>. Plural is the convention, fine. But nobody has ever failed an integration because of noun plurality.</p>
<p>Deeply nested resource paths. <code>/users/123/portfolios/456/watchlists/789/items</code> looks RESTful but is a nightmare to cache, a nightmare to document, and a nightmare for client library authors. Flatten it. Use query parameters.</p>
<p>HATEOAS. I know this is heresy. In theory, self-describing APIs with hypermedia links are beautiful. In practice, I&rsquo;ve never seen a client actually follow links dynamically. They hardcode paths. Design for that reality.</p>
<h3 id="show-me-the-code">Show me the code</h3>
<p>Here is what a poorly designed endpoint looks like in practice. I&rsquo;ve seen this pattern dozens of times.</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:#75715e"># Bad: mixing concerns, inconsistent response shape,</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># status code lies about outcome</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@app.route</span>(<span style="color:#e6db74">&#39;/api/getStories&#39;</span>, methods<span style="color:#f92672">=</span>[<span style="color:#e6db74">&#39;POST&#39;</span>])
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">get_stories</span>():
</span></span><span style="display:flex;"><span>    ticker <span style="color:#f92672">=</span> request<span style="color:#f92672">.</span>json<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;ticker&#39;</span>)
</span></span><span style="display:flex;"><span>    stories <span style="color:#f92672">=</span> db<span style="color:#f92672">.</span>query_stories(ticker)
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">if</span> <span style="color:#f92672">not</span> stories:
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> jsonify({
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;success&#39;</span>: <span style="color:#66d9ef">False</span>,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;message&#39;</span>: <span style="color:#e6db74">&#39;No stories found&#39;</span>
</span></span><span style="display:flex;"><span>        }), <span style="color:#ae81ff">200</span>  <span style="color:#75715e"># 200 for an empty result? for an error? who knows</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> jsonify({
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;success&#39;</span>: <span style="color:#66d9ef">True</span>,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;result&#39;</span>: stories,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;total&#39;</span>: len(stories)
</span></span><span style="display:flex;"><span>    }), <span style="color:#ae81ff">200</span>
</span></span></code></pre></div><p>POST for a read operation. <code>getStories</code> as the path instead of a resource noun. <code>success</code> boolean instead of HTTP status codes. No pagination. The response shape changes based on the result. Every client has to write branching logic just to parse this.</p>
<p>Now compare that with the approach we settled on at the fintech startup.</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:#75715e"># Good: proper verb, resource-oriented path,</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># consistent response envelope, pagination from day one</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">@app.route</span>(<span style="color:#e6db74">&#39;/api/v1/stories&#39;</span>, methods<span style="color:#f92672">=</span>[<span style="color:#e6db74">&#39;GET&#39;</span>])
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">def</span> <span style="color:#a6e22e">list_stories</span>():
</span></span><span style="display:flex;"><span>    ticker <span style="color:#f92672">=</span> request<span style="color:#f92672">.</span>args<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;ticker&#39;</span>)
</span></span><span style="display:flex;"><span>    cursor <span style="color:#f92672">=</span> request<span style="color:#f92672">.</span>args<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;cursor&#39;</span>)
</span></span><span style="display:flex;"><span>    limit <span style="color:#f92672">=</span> min(int(request<span style="color:#f92672">.</span>args<span style="color:#f92672">.</span>get(<span style="color:#e6db74">&#39;limit&#39;</span>, <span style="color:#ae81ff">20</span>)), <span style="color:#ae81ff">100</span>)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    stories, next_cursor <span style="color:#f92672">=</span> db<span style="color:#f92672">.</span>query_stories(
</span></span><span style="display:flex;"><span>        ticker<span style="color:#f92672">=</span>ticker, cursor<span style="color:#f92672">=</span>cursor, limit<span style="color:#f92672">=</span>limit
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> jsonify({
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;data&#39;</span>: stories,
</span></span><span style="display:flex;"><span>        <span style="color:#e6db74">&#39;pagination&#39;</span>: {
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;next_cursor&#39;</span>: next_cursor,
</span></span><span style="display:flex;"><span>            <span style="color:#e6db74">&#39;has_more&#39;</span>: next_cursor <span style="color:#f92672">is</span> <span style="color:#f92672">not</span> <span style="color:#66d9ef">None</span>
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }), <span style="color:#ae81ff">200</span>
</span></span></code></pre></div><p>GET for reads. Resource noun in the path. Cursor-based pagination from day one. A consistent envelope that looks identical whether you&rsquo;re listing stories, tickers, or users. An empty list returns an empty <code>data</code> array with <code>has_more: false</code>. No special cases.</p>
<h3 id="pagination-isnt-optional">Pagination isn&rsquo;t optional</h3>
<p>The fintech startup API serves financial news. News volume spikes. When a company announces earnings or a market event hits, you can go from 50 stories to 5,000 in an hour. If your list endpoint doesn&rsquo;t paginate, you will find out the hard way.</p>
<p>We used cursor-based pagination from the start. Offset pagination is simpler to explain, but it breaks when data changes between requests. In a feed of financial news where stories are constantly being added, offset pagination skips and duplicates items. Cursors are opaque to the client, stable across inserts, and give you room to change the underlying query without breaking the contract.</p>
<h3 id="errors-are-part-of-the-interface">Errors are part of the interface</h3>
<p>When a quant trader&rsquo;s script hits a validation error at 2am, they aren&rsquo;t going to email support. They are going to read the error response and either fix it or switch providers.</p>
<p>Every error from our API included a machine-readable code, a human-readable message, and a request ID for tracing. Validation errors included the field name and the reason. Rate limit responses included <code>Retry-After</code> and the remaining quota. This isn&rsquo;t generosity. This is self-preservation. Good error messages reduce support tickets.</p>
<h3 id="version-from-the-start">Version from the start</h3>
<p>We put <code>/v1/</code> in the URL path. Not because it&rsquo;s the most elegant approach, but because it&rsquo;s the most visible. When something breaks, you can see the version in the logs, in the curl command, in the client configuration. Header-based versioning is cleaner in theory, but in practice it&rsquo;s invisible exactly when you need it most.</p>
<p>The rule we followed: adding fields is safe, removing fields isn&rsquo;t. Changing a type is a breaking change. Changing an error format is a breaking change. If you aren&rsquo;t sure whether something is breaking, it&rsquo;s breaking.</p>
<h3 id="the-contract-that-matters">The contract that matters</h3>
<p>API design isn&rsquo;t about following REST rules for purity. It&rsquo;s about reducing the cost of integration for people who aren&rsquo;t you. Consistent shapes, honest status codes, pagination from day one, useful errors, and visible versioning. That&rsquo;s the whole list. The fintech startup API survived multiple backend rewrites because we got those basics right early. The implementation changed. The contract held.</p>
]]></content:encoded></item><item><title>Postgres vs MySQL in 2016: A Practical Comparison</title><link>https://lawzava.com/blog/2016-04-12-postgres-vs-mysql-practical-comparison/</link><pubDate>Tue, 12 Apr 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-04-12-postgres-vs-mysql-practical-comparison/</guid><description>A grounded look at PostgreSQL and MySQL as of April 2016, focusing on integrity, query power, and operational tradeoffs rather than benchmark hype.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>Pick Postgres. If your workload is dead-simple CRUD and your team already bleeds MySQL, fine, stay there. For everything else in 2016, PostgreSQL gives you more database and fewer workarounds.</p>
<hr>
<h3 id="the-short-answer">The short answer</h3>
<p>I&rsquo;m building the fintech startup on PostgreSQL. Financial data, JSONB documents, full-text search across thousands of sources, strict schema enforcement for anything that touches money. I evaluated MySQL honestly. Postgres won on every axis that matters to me.</p>
<p>That doesn&rsquo;t make MySQL bad. It makes the decision obvious for my workload. Here is how I see the tradeoffs.</p>
<h3 id="the-comparison">The comparison</h3>
<table>
  <thead>
      <tr>
          <th>Capability</th>
          <th>PostgreSQL</th>
          <th>MySQL (InnoDB, 5.7)</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Data integrity</strong></td>
          <td>Strict by default. Type violations, overflows, and constraint breaches are errors. CHECK constraints enforced.</td>
          <td>Lenient by default. Will silently truncate or coerce unless you enable strict mode. CHECK constraints parsed but <strong>not enforced</strong>.</td>
      </tr>
      <tr>
          <td><strong>Transactional DDL</strong></td>
          <td>Yes. Failed migrations roll back cleanly.</td>
          <td>No. DDL auto-commits. A failed migration leaves you half-changed.</td>
      </tr>
      <tr>
          <td><strong>JSONB</strong></td>
          <td>First-class. GIN-indexed, queryable with operators, fast.</td>
          <td>JSON type exists but no binary storage, limited indexing. Practical queries need generated columns.</td>
      </tr>
      <tr>
          <td><strong>Full-text search</strong></td>
          <td>Built in. Dictionaries, ranking, language support. Good enough to skip Elasticsearch for many cases.</td>
          <td>Basic keyword matching. Serviceable for simple search, but you will add Solr or Elastic quickly.</td>
      </tr>
      <tr>
          <td><strong>Window functions</strong></td>
          <td>Yes, mature.</td>
          <td>No. Not until 8.0 (years away). Analytics queries become subquery nightmares.</td>
      </tr>
      <tr>
          <td><strong>CTEs</strong></td>
          <td>Yes. Recursive CTEs too.</td>
          <td>No. Same story as window functions.</td>
      </tr>
      <tr>
          <td><strong>Custom types/operators</strong></td>
          <td>Yes. You can build domain-specific behavior inside the database.</td>
          <td>Limited UDFs. No custom operators or types.</td>
      </tr>
      <tr>
          <td><strong>Concurrency model</strong></td>
          <td>MVCC with new row versions. Requires vacuum.</td>
          <td>MVCC via undo logs. Purge is less visible operationally.</td>
      </tr>
      <tr>
          <td><strong>Connection handling</strong></td>
          <td>Process-per-connection. Needs PgBouncer at scale.</td>
          <td>Thread-per-connection. Handles high connection counts more easily out of the box.</td>
      </tr>
      <tr>
          <td><strong>Replication</strong></td>
          <td>Streaming (physical). Reliable, simple, but replicates the whole cluster. Logical replication is third-party in 2016.</td>
          <td>Row-based, statement-based, or mixed. More flexible for partial replication. More edge cases.</td>
      </tr>
      <tr>
          <td><strong>Ecosystem/hosting</strong></td>
          <td>Smaller managed ecosystem. RDS supports it well. Fewer one-click options.</td>
          <td>Everywhere. Every cheap host, every managed platform. Largest install base.</td>
      </tr>
      <tr>
          <td><strong>Upgrades</strong></td>
          <td>Major version upgrades need planning. pg_upgrade helps but it isn&rsquo;t seamless.</td>
          <td>Generally smoother in-place upgrades.</td>
      </tr>
  </tbody>
</table>
<h3 id="where-postgres-pulls-ahead">Where Postgres pulls ahead</h3>
<p><strong>Correctness is the default.</strong> I don&rsquo;t want my database silently truncating a currency field or accepting a string where an integer belongs. Postgres refuses bad data. MySQL lets it through unless you configure it not to. In financial systems, the database being strict isn&rsquo;t a feature request. It&rsquo;s the minimum.</p>
<p><strong>JSONB changes what you can do.</strong> At the fintech startup we store semi-structured financial events alongside relational data. Postgres lets me index into JSONB, query nested fields, and join it with relational tables in one query. With MySQL I would be serializing JSON, pulling it into the application, and filtering there. That isn&rsquo;t a comparison. That&rsquo;s a generation gap.</p>
<p><strong>Full-text search removes a dependency.</strong> We search across news sources, filings, and analyst content. Postgres full-text search with <code>ts_vector</code>, dictionaries, and ranking handles this without bolting on a separate search cluster. One fewer service to operate, monitor, and keep in sync.</p>
<p><strong>Window functions and CTEs aren&rsquo;t optional.</strong> If you do any reporting or analytics, you need them. MySQL not having them in 2016 means your choices are ugly subqueries, dumping data into a separate analytics tool, or doing the math in application code. Postgres just does it.</p>
<h3 id="where-mysql-wins">Where MySQL wins</h3>
<p>I&rsquo;ll give MySQL its due.</p>
<p><strong>Connection scaling.</strong> Postgres forks a process per connection. At a few hundred connections, memory adds up fast and you need a pooler. MySQL handles thousands of threads without breaking a sweat. If your architecture has many direct database connections and you don&rsquo;t want to manage PgBouncer, that matters.</p>
<p><strong>Operational simplicity for upgrades.</strong> MySQL major version upgrades tend to be less painful. Postgres upgrades have gotten better, but they still require more planning and occasionally downtime.</p>
<p><strong>Ubiquity.</strong> MySQL is everywhere. Every shared host, every tutorial, every legacy system. If you&rsquo;re inheriting a MySQL codebase and the schema is simple, migrating to Postgres just because you prefer it is a waste of time. Use what is there.</p>
<h3 id="my-decision-framework">My decision framework</h3>
<p>Three questions:</p>
<ol>
<li>
<p><strong>Does your data need to be correct, or just present?</strong> If correctness matters&ndash;financial data, health records, billing&ndash;Postgres. Its strictness isn&rsquo;t friction. It&rsquo;s protection.</p>
</li>
<li>
<p><strong>Do you need more than basic SELECT/INSERT/UPDATE?</strong> If you need JSONB, full-text search, window functions, CTEs, or custom types, Postgres gives you those today. MySQL will make you bolt on external tools or wait for features that aren&rsquo;t shipping in 2016.</p>
</li>
<li>
<p><strong>Is your team already deep in MySQL?</strong> Then stay. Database expertise matters more than database features. A well-tuned MySQL with a team that knows it will outperform a poorly operated Postgres every time.</p>
</li>
</ol>
<h3 id="the-honest-take">The honest take</h3>
<p>Most of the &ldquo;Postgres vs MySQL&rdquo; content online bends over backward to be balanced. I won&rsquo;t. For my workloads&ndash;financial data, mixed relational and document storage, search, reporting&ndash;Postgres isn&rsquo;t a marginal winner. It&rsquo;s the obvious choice.</p>
<p>MySQL is a fine database for simpler workloads and teams that know it well. But if you&rsquo;re starting fresh in 2016 and your needs are anything beyond basic CRUD, pick Postgres. You will thank yourself when the first complex query lands and you don&rsquo;t have to rewrite it as three subqueries and an application-side join.</p>
<p>The best database decision is the pragmatic one, not the tribal one. Pick the tool that does more of the work for you and stop arguing about logos.</p>
]]></content:encoded></item><item><title>Building a DevOps Culture from Scratch</title><link>https://lawzava.com/blog/2016-03-10-building-devops-culture-from-scratch/</link><pubDate>Thu, 10 Mar 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-03-10-building-devops-culture-from-scratch/</guid><description>DevOps is a cultural shift, not a job title. A practical path to shared responsibility, fast feedback, and resilient delivery without hand-wavy promises.</description><content:encoded><![CDATA[<p>I keep seeing the same pattern. A company posts a job listing for a &ldquo;DevOps engineer,&rdquo; hires someone smart, hands them a Jira board, and waits for magic. Six months later the deploy pipeline is marginally better but the teams still don&rsquo;t trust each other. The hire burns out. Everyone blames the tools.</p>
<p>DevOps isn&rsquo;t a role. It isn&rsquo;t Jenkins. It isn&rsquo;t even a set of practices you can copy from a blog post. It&rsquo;s a cultural shift where the people who build software and the people who run it share the same goals, the same pain, and the same wins.</p>
<p>I learned this the hard way at a mobility startup.</p>
<h2 id="what-happened-at-the-mobility-startup">What Happened at the Mobility Startup</h2>
<p>When I joined as CTO, we had a classic split. Three backend engineers shipping features as fast as possible. One ops person &ndash; call him Max &ndash; who kept the lights on. The engineers would push code on Friday afternoon, Max would get paged at 2 a.m. Saturday, and Monday morning would start with a blame session disguised as a standup.</p>
<p>Max was good at his job. The engineers were good at theirs. The system between them was broken.</p>
<p>The fix wasn&rsquo;t a tool. It was a conversation. I put the entire team in a room and said: starting next week, the person who ships the code carries the pager for it. Not forever &ndash; just for the first 48 hours after deploy. If your code wakes someone up, that someone is you.</p>
<p>The reaction was roughly what you would expect. The engineers hated it. Max loved it. But within two weeks, something shifted. Deploys got smaller. Engineers started writing health checks without being asked. Someone added a runbook for the payment service, unprompted. The Friday afternoon deploys stopped entirely.</p>
<p>Nobody told them to do any of that. The incentive structure changed, and the behavior followed.</p>
<h2 id="quick-take">Quick take</h2>
<p>Stop hiring &ldquo;DevOps engineers&rdquo; and start fixing the incentives that make your dev and ops teams fight each other. Culture first, tools second.</p>
<h2 id="start-with-shared-pain-not-shared-tools">Start With Shared Pain, Not Shared Tools</h2>
<p>That experience taught me something I keep coming back to: if you want DevOps culture, start by making everyone feel the consequences of their decisions.</p>
<p>Developers who never see production alerts write code that&rsquo;s hard to operate. Ops people who never sit in planning meetings treat every deploy as a threat. The gap isn&rsquo;t technical. It&rsquo;s empathy.</p>
<p>The cheapest thing you can do is cross-pollinate. Have a developer shadow on-call for a week. Have the ops person sit in sprint planning. Run a postmortem together after the next incident and focus on the system, not the person. I&rsquo;ve never seen a team do this and not come out with a better understanding of each other.</p>
<h2 id="pick-a-pilot-team-and-let-them-own-it">Pick a Pilot Team and Let Them Own It</h2>
<p>Don&rsquo;t try to transform the whole company at once. Pick one team. Give them ownership of their deploy pipeline and their on-call. Let them figure out what works. Measure what happens.</p>
<p>At the mobility startup, that pilot was the team running the bike availability service. Small blast radius, clear metrics, leadership willing to experiment. They went from deploying once a week to deploying daily within a month. Not because I told them to, but because shorter cycles meant smaller changes meant fewer pages.</p>
<p>The rest of the engineering org watched this happen. Nobody had to sell them on DevOps after that. They just asked when they could start.</p>
<h2 id="the-technical-stuff-matters-but-it-isnt-first">The Technical Stuff Matters, But It Isn&rsquo;t First</h2>
<p>Once the culture is moving, you need the technical foundations to support it. Infrastructure as code so environments are reproducible. A CI pipeline so every commit gets tested. Monitoring that answers &ldquo;is this thing working&rdquo; without requiring tribal knowledge.</p>
<p>In 2016, that means Ansible or Chef for config management, Jenkins or CircleCI for builds, and something like the ELK stack or Graphite for observability. The specific tools matter less than the principle: everything should be automated, version-controlled, and visible to the whole team.</p>
<p>But here is the thing &ndash; I&rsquo;ve seen teams with perfect pipelines and terrible culture. The pipeline doesn&rsquo;t fix trust. It just makes deploys faster. If the team still throws code over the wall and blames each other when it breaks, a faster pipeline just accelerates the dysfunction.</p>
<h2 id="traps-ive-walked-into">Traps I&rsquo;ve Walked Into</h2>
<p><strong>The hero trap.</strong> At the mobility startup, Max was the hero. He could fix anything at 3 a.m. That felt like a strength until he took a week off and nobody knew how to restart the payment gateway. Heroes are a single point of failure wearing a cape. Spread the knowledge.</p>
<p><strong>Tool obsession.</strong> We spent two weeks evaluating orchestration platforms when the real problem was that nobody knew what was running in production. A shared spreadsheet would have been more useful than Kubernetes at that point. Solve the problem you actually have.</p>
<p><strong>Burnout disguised as ownership.</strong> If you give developers pager duty without reducing their feature load, you aren&rsquo;t building DevOps culture. You&rsquo;re just distributing the suffering more evenly. Shared responsibility means shared capacity planning too.</p>
<h2 id="its-a-slow-game">It&rsquo;s a Slow Game</h2>
<p>DevOps culture doesn&rsquo;t happen in a quarter. At the mobility startup, it took about six months before the dev-vs-ops tension actually went away. The team went from monthly releases with weekend war rooms to daily deploys that nobody thought twice about. But it was gradual, messy, and full of setbacks.</p>
<p>The wins compound though. Smaller deploys mean fewer incidents. Fewer incidents mean less firefighting. Less firefighting means more time for the work that actually matters. Once that flywheel starts turning, it&rsquo;s hard to stop.</p>
<p>If you&rsquo;re starting from zero, that&rsquo;s fine. Most teams are. Start with the incentives, not the tools. Make people feel what production actually looks like. Pick a small team and let them prove the model. The culture will follow the work.</p>
]]></content:encoded></item><item><title>The True Cost of Technical Debt</title><link>https://lawzava.com/blog/2016-02-22-the-true-cost-of-technical-debt/</link><pubDate>Mon, 22 Feb 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-02-22-the-true-cost-of-technical-debt/</guid><description>A pragmatic look at technical debt: how it shows up, how to measure it, and how to make a business case for paying it down without stalling delivery.</description><content:encoded><![CDATA[<p>Every engineering team I&rsquo;ve worked with complains about technical debt. At a mobility startup, at the fintech startup, at every company I&rsquo;ve been part of. The conversations sound the same. &ldquo;We need to refactor the billing module.&rdquo; &ldquo;The test suite is broken.&rdquo; &ldquo;Nobody understands how the queue system works anymore.&rdquo;</p>
<p>And then nothing happens. Or worse, leadership approves a vague &ldquo;cleanup sprint&rdquo; that accomplishes nothing measurable, and next quarter the same complaints come back louder.</p>
<p>The debt isn&rsquo;t the problem. The inability to quantify it is.</p>
<p>Ward Cunningham coined the metaphor in 1992, and he chose finance deliberately. Debt has a principal and an interest rate. You can calculate whether it&rsquo;s worth carrying or paying down. But most engineering teams skip the math entirely. They treat tech debt like a feeling &ndash; something you gripe about in retros but never attach a number to it.</p>
<p>I&rsquo;ve been guilty of this. Early at the mobility startup, I&rsquo;d walk into planning meetings and say things like &ldquo;the data layer is fragile&rdquo; or &ldquo;deploys are getting riskier.&rdquo; True statements. Completely useless for getting resources allocated. The product team heard &ldquo;engineers want to rewrite things instead of shipping features.&rdquo; Which, from their perspective, was a reasonable interpretation.</p>
<p>What changed was when I started tracking three things. First, how long the same type of task took six months ago versus now. Second, how many incidents traced back to the same modules. Third, how much engineer time went into those incidents &ndash; not just the fix, but the investigation, the communication, the customer support follow-up.</p>
<p>The numbers were ugly. A task category that averaged two days in mid-2015 was averaging five days by early 2016. Same complexity, same team size. The difference was the accumulated mess in three interconnected services. One module was responsible for four incidents in a single quarter, each burning two to three engineer-days when you counted everything.</p>
<p>Suddenly I wasn&rsquo;t saying &ldquo;the billing module is fragile.&rdquo; I was saying &ldquo;this module cost us roughly 40 engineer-days last quarter in incidents alone, and it&rsquo;s adding three days to every feature that touches payments.&rdquo; That&rsquo;s a number a CEO can reason about. That&rsquo;s a number you can weigh against a feature roadmap.</p>
<p>The fix doesn&rsquo;t need to be dramatic. At the fintech startup, we allocated 15% of sprint capacity to debt work &ndash; not negotiable, not borrowable. Small, targeted improvements. Tighten a function while you&rsquo;re already in the file. Add the missing integration test before you move on. Replace the brittle dependency before it causes another 2am page.</p>
<p>Compound interest works both ways. Small consistent payments on debt keep the codebase healthy enough to move fast. Skip those payments and the interest compounds until a &ldquo;small change&rdquo; takes a week and every deploy is a coin flip.</p>
<p>The teams that handle debt well aren&rsquo;t the ones with clean codebases. They&rsquo;re the ones that can tell you exactly what their debt costs. They measure cycle time trends. They track incident attribution. They know which modules are hotspots and what those hotspots cost per quarter in real engineer hours.</p>
<p>If you can&rsquo;t put a number on your tech debt, you can&rsquo;t make a rational decision about it. And if you can&rsquo;t make a rational decision, you&rsquo;ll either ignore it until it cripples you or waste time on cleanup that doesn&rsquo;t matter.</p>
<p>Measure the pain. Do the math. Present the tradeoff in terms the business already understands. That&rsquo;s the whole strategy. Everything else is just complaining.</p>
]]></content:encoded></item><item><title>Why Microservices Aren't Always the Answer</title><link>https://lawzava.com/blog/2016-01-15-why-microservices-arent-always-the-answer/</link><pubDate>Fri, 15 Jan 2016 00:00:00 +0000</pubDate><guid>https://lawzava.com/blog/2016-01-15-why-microservices-arent-always-the-answer/</guid><description>Most teams adopt microservices too early and pay for complexity they don&amp;amp;rsquo;t need yet. A well-structured monolith is faster, simpler, and keeps your options open.</description><content:encoded><![CDATA[<h2 id="quick-take">Quick take</h2>
<p>If you have fewer than four teams and your domain boundaries are still shifting, you almost certainly don&rsquo;t need microservices. You need a clean monolith and the discipline to keep it modular.</p>
<h3 id="the-split-that-cost-us-three-months">The split that cost us three months</h3>
<p>At a mobility startup we had a Go monolith that handled bike availability, user accounts, payments, and ride tracking. It was about 40k lines, well-tested, deployed in under two minutes. It worked.</p>
<p>Then we decided to extract payments into its own service. The reasoning sounded right: payments are sensitive, they have different scaling characteristics, and we wanted to isolate failures. Classic microservices pitch.</p>
<p>What actually happened: we spent three months building the extraction. We needed a new deploy pipeline, a message contract between services, retry logic for the network boundary, and a way to keep ride state consistent when the payment service was slow or down. We went from a function call that took microseconds to a network call that introduced latency, partial failures, and a new category of bugs we had never dealt with before.</p>
<p>The team was four engineers. We could have spent those three months shipping features. Instead we shipped infrastructure.</p>
<h3 id="the-real-cost-of-splitting-early">The real cost of splitting early</h3>
<p>Microservices solve an organizational problem, not a technical one. When you have multiple teams that need to ship independently without stepping on each other, services aligned to team boundaries are powerful. That&rsquo;s a real benefit.</p>
<p>But most teams I see adopting microservices don&rsquo;t have that problem. They have five to ten engineers, a shared codebase, and a product that&rsquo;s still changing shape weekly. At that stage, splitting into services means:</p>
<p><strong>More deploy pipelines to maintain.</strong> Each service needs its own CI, its own monitoring, its own alerting. That&rsquo;s real maintenance overhead for a small team.</p>
<p><strong>Distributed data pain.</strong> A query that was a simple SQL join becomes a cross-service call. A transaction that was ACID becomes an eventually consistent workflow. At the mobility startup, a single &ldquo;end ride and charge user&rdquo; operation went from one database transaction to a choreography of events across two services with compensating actions for failure cases. The code tripled in complexity.</p>
<p><strong>Testing gets hard.</strong> A monolith test suite runs in one process. Service tests require either mocking everything (which hides real bugs) or running integration environments that need their own care. We went from <code>go test ./...</code> taking 30 seconds to an integration suite that took 12 minutes and broke for infrastructure reasons as often as code reasons.</p>
<p><strong>Debugging gets hard.</strong> A stack trace in a monolith tells you exactly what happened. A distributed trace across services requires correlation IDs, centralized logging, and tracing infrastructure. In 2016, that tooling is still immature.</p>
<h3 id="when-a-monolith-is-the-right-call">When a monolith is the right call</h3>
<p>A monolith isn&rsquo;t a dirty word. A well-structured monolith with clear package boundaries, explicit interfaces between modules, and owned data per module gives you most of the architectural benefits of services without the operational tax.</p>
<p>At the fintech startup we kept the backend as a single deployable for much longer than conventional wisdom suggested. Financial news ingestion, NLP processing, user-facing API, all in one app. The key was strict internal boundaries. The NLP module exposed a clean interface. The API layer never reached into ingestion internals. Data ownership was explicit even though it shared a database.</p>
<p>This let us move fast. A new feature that touched multiple concerns was a single PR, a single deploy, a single rollback if something went wrong. We weren&rsquo;t coordinating releases across services or debugging network failures between components that used to be function calls.</p>
<h3 id="the-modular-monolith">The modular monolith</h3>
<p>The pattern I keep coming back to is the modular monolith. One deployable unit, strict internal boundaries, explicit interfaces between modules. You get the code organization benefits without paying the distributed systems tax.</p>
<p>The key disciplines:</p>
<ul>
<li>Each module owns its data. No reaching into another module&rsquo;s tables.</li>
<li>Modules communicate through defined interfaces, not by importing each other&rsquo;s internals.</li>
<li>Dependencies between modules are visible and intentional.</li>
</ul>
<p>This isn&rsquo;t easy. It requires code review discipline and a team that cares about boundaries. But it&rsquo;s dramatically simpler than operating a fleet of services, and it keeps the option to extract a service later when you have a concrete reason.</p>
<h3 id="when-you-actually-need-services">When you actually need services</h3>
<p>There are legitimate reasons to split. I&rsquo;ve seen three that hold up in practice:</p>
<p><strong>Radically different scaling needs.</strong> If one component handles 100x the traffic of everything else, separating it can save real money and improve reliability. At the mobility startup, the bike location tracking service eventually did need to be separate because it processed GPS updates at a rate that would have required over-provisioning the entire monolith.</p>
<p><strong>Independent team ownership.</strong> When you have genuinely separate teams that are blocked by coordinated releases, aligning service boundaries to team boundaries unblocks delivery. But this is a team problem, not a technology problem. If you have one team, you don&rsquo;t have this problem.</p>
<p><strong>Hard compliance boundaries.</strong> PCI, SOX, specific regulatory requirements that mandate isolation. These are real constraints, not aspirational architecture goals.</p>
<p>Notice what isn&rsquo;t on the list: &ldquo;because Netflix does it&rdquo; or &ldquo;because we might need to scale someday.&rdquo; Premature optimization applied to architecture is just as wasteful as premature optimization applied to code.</p>
<h3 id="if-youre-going-to-split-do-it-incrementally">If you&rsquo;re going to split, do it incrementally</h3>
<p>If you have a monolith and genuine reasons to extract a service, don&rsquo;t rewrite. Extract one piece. Run it alongside the monolith. Learn everything about operating two things instead of one. Then decide if the trade-off was worth it before extracting the next piece.</p>
<p>The operational foundations you need are the same ones that make a monolith healthy: good logging, monitoring, deployment automation, and incident response. Build those first. They pay off regardless of your architecture.</p>
<h3 id="the-bottom-line">The bottom line</h3>
<p>Microservices are a trade-off, not an upgrade. They buy organizational independence at the cost of operational complexity. For most teams I talk to, the honest answer is: you aren&rsquo;t big enough to need them yet, and your monolith isn&rsquo;t the thing slowing you down.</p>
<p>Ship features. Keep your boundaries clean. Split when the pain of coordination is real and measured, not hypothetical. That&rsquo;s the job.</p>
]]></content:encoded></item></channel></rss>