<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: surajrkhonde</title>
    <description>The latest articles on DEV Community by surajrkhonde (@surajrkhonde).</description>
    <link>https://dev.to/surajrkhonde</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1105683%2F1391e193-ba7a-4e01-8e5e-4e607fd467db.png</url>
      <title>DEV Community: surajrkhonde</title>
      <link>https://dev.to/surajrkhonde</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/surajrkhonde"/>
    <language>en</language>
    <item>
      <title>Where Does RAG Actually Cost You Money? (Episode 6)</title>
      <dc:creator>surajrkhonde</dc:creator>
      <pubDate>Sun, 09 Aug 2026 14:29:18 +0000</pubDate>
      <link>https://dev.to/surajrkhonde/where-does-rag-actually-cost-you-money-episode-6-4l4o</link>
      <guid>https://dev.to/surajrkhonde/where-does-rag-actually-cost-you-money-episode-6-4l4o</guid>
      <description>&lt;h3&gt;
  
  
  Why Fewer, Better-Chosen Chunks Beat a Bigger, More Expensive Model
&lt;/h3&gt;




&lt;p&gt;For a while, my retrieval setting was &lt;code&gt;top_k = 10&lt;/code&gt;. It felt responsible. If I only pulled back 3 chunks and the right one wasn't among them, the answer would be wrong for a reason that had nothing to do with the model. Pulling back 10 felt like insurance — cast a wide net, let the LLM sort out what actually mattered.&lt;/p&gt;

&lt;p&gt;The bot's answers didn't get better. Some of them got worse.&lt;/p&gt;

&lt;p&gt;I remember one question in particular — something specific, with a clear correct chunk sitting somewhere in the middle of those 10. The LLM's answer wandered. It grabbed a detail from a chunk that was only loosely related, ignored the one that actually answered the question, and produced something that read confidently but missed the point. The right information was &lt;em&gt;in the prompt&lt;/em&gt;. It just wasn't the information the model reached for.&lt;/p&gt;

&lt;p&gt;That's when I went back and actually read what I'd been sending. Ten chunks, most of them tangential, all competing for the model's attention in one long prompt. The correct answer wasn't easy to find in there — even for me, reading it slowly, on purpose. I'd been treating "send more context" as a safety net. It was actually working against the model, and against my bill, on every single request.&lt;/p&gt;

&lt;p&gt;So I tried the opposite: retrieve more candidates than I needed, but add a reranking step that scores them for actual relevance, and only send the LLM the top 3 after that. Fewer tokens per prompt. And, somewhat counter to what I expected, &lt;em&gt;better&lt;/em&gt; answers — not despite sending less, but because of it.&lt;/p&gt;

&lt;p&gt;That flipped something I'd assumed without ever testing it: more retrieved context isn't more safety. Past a point, it's noise the model has to wade through, paid for at input-token prices, on every question.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Idea This Whole Article Is About
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Retrieval quality determines how much you end up overpaying the LLM to compensate for it. Sending more chunks isn't a safety margin — it's a tax that also makes the model's job harder.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every earlier episode in this series was about getting clean material into the vector database. This one is about the last decision before that material reaches the model: how much of it, and how well-chosen, actually makes it into the prompt. Get that wrong, and no amount of extraction quality, chunking care, or metadata precision from Episodes 2 through 5 will save you — the right answer can be sitting in the prompt and still get missed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost 1 — The Over-Retrieval Tax
&lt;/h2&gt;

&lt;p&gt;The most direct cost is the one from the story: every extra chunk in &lt;code&gt;top_k&lt;/code&gt; is extra input tokens, paid on every single query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;top_k = 3
      │
      ▼
Fewer tokens in the prompt, cheaper per query

top_k = 10
      │
      ▼
More tokens in the prompt, more expensive per query
      │
      ▼
...and most of those extra chunks are redundant or irrelevant
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This part is intuitive once you say it out loud — more chunks costs more tokens. What's easy to miss is that this cost buys you very little if the extra chunks aren't actually useful. You're not paying for more safety. You're paying for more text the model has to read past.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost 2 — The Attention Tax (Lost in the Middle)
&lt;/h2&gt;

&lt;p&gt;This is the cost that surprised me in the story, and it doesn't show up as a token count — it shows up as answer quality.&lt;/p&gt;

&lt;p&gt;Language models don't weigh every part of a long prompt equally. Information buried in the middle of a long, noisy context is measurably easier for a model to underweight or miss than information near the start or end of a tightly-focused prompt. This is often described as the "lost in the middle" effect, and it means a longer prompt isn't just more expensive — it can be &lt;em&gt;actively worse&lt;/em&gt; at surfacing the right answer, even when the right chunk is technically present.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Long, noisy prompt (top_k = 10)
      │
      ▼
Correct chunk is in there, but surrounded by 9 distractors
      │
      ▼
Model's attention gets diluted across all of them
      │
      ▼
Answer misses or underweights the correct chunk
      │
      ▼
Wrong or incomplete answer, despite having the right information
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the cost that turns "safety margin" logic on its head. More context isn't neutral-at-worst. Past a point, it can make the model's job harder while also making you pay more for the privilege.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost 3 — Reranking: Paying a Little to Save a Lot
&lt;/h2&gt;

&lt;p&gt;If sending fewer, better chunks is the goal, you need a way to know which chunks are actually the best ones — vector similarity alone isn't always precise enough to trust for the final cut.&lt;/p&gt;

&lt;p&gt;A reranker sits between retrieval and generation: retrieve a wider set of candidates cheaply, score them for relevance with a more precise (and more expensive per-item) model, then send only the top few to the LLM.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Retrieve 20-30 candidates (cheap, wide net)
      │
      ▼
Rerank them for actual relevance (adds compute + latency, but small per item)
      │
      ▼
Send only the top 3 to the LLM (expensive-per-token stage)
      │
      ▼
Net effect: extra cost at a cheap stage, savings at the expensive stage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the same shape as every quality-vs-cost decision earlier in this series — you're moving cost to an earlier, cheaper stage to avoid paying for it at a later, more expensive one. Reranking isn't free. But it's usually cheap relative to the LLM tokens it saves, because scoring candidate relevance is a fundamentally lighter task than generating a full answer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost 4 — Paying at the Wrong Layer
&lt;/h2&gt;

&lt;p&gt;This is the cost I think is easiest to fall into without noticing, because it looks like progress instead of a symptom.&lt;/p&gt;

&lt;p&gt;When retrieval quality is poor, one common instinct is to reach for a bigger, more capable model — reasoning that a smarter model can compensate for messier context. Sometimes it can, partially. But it's an expensive way to patch a cheaper problem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Retrieval sends noisy, poorly-chosen context
      │
      ▼
Answers are inconsistent
      │
      ▼
Team upgrades to a larger, more expensive model to compensate
      │
      ▼
Cost per query goes up significantly
      │
      ▼
Underlying retrieval problem is still there, just harder to see
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A bigger model paying attention to bad context is still paying attention to bad context — it's just more expensive while doing it. The fix that actually addresses the problem (better retrieval, reranking, tighter top_k) usually costs far less than the fix that just papers over it (a bigger model). This is the retrieval-episode version of Episode 2's lesson: fixing quality upstream is almost always cheaper than compensating for it downstream.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting a Number on It
&lt;/h2&gt;

&lt;p&gt;Same pattern as the earlier episodes — a small worked example to make the trade-off tangible. &lt;strong&gt;These figures are illustrative, not measured from a real system.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

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

top_k = 10, no reranking
      │
      ▼
~10 chunks × ~300 tokens each ≈ 3,000 prompt tokens
      │
      ▼
Correct chunk present, but diluted among 9 others

versus

top_k = 30 candidates → reranked → top 3 sent to LLM
      │
      ▼
~3 chunks × ~300 tokens each ≈ 900 prompt tokens
      │
      ▼
Correct chunk present, and it's one of only 3 — much harder to miss
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Roughly a &lt;strong&gt;3x drop in prompt tokens on every single query&lt;/strong&gt;, plus a real shot at fewer wrong-answer retries, for the cost of one lightweight reranking step that's cheap relative to what it's replacing. This is the same shape of math as Episode 3's chunk-size example — a small change earlier in the pipeline, multiplied by every request that follows it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost Isn't Just a Dollar Amount, Again
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LLM input tokens&lt;/strong&gt; — the direct, most visible cost of over-retrieval&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer quality&lt;/strong&gt; — the "lost in the middle" tax, paid in wrong or incomplete answers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reranking compute&lt;/strong&gt; — a small, deliberate cost spent to avoid a larger one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model tier&lt;/strong&gt; — the cost of quietly upgrading models to compensate for a retrieval problem instead of fixing it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency&lt;/strong&gt; — every added stage (wider retrieval, reranking) adds some time, traded against fewer retries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engineering time&lt;/strong&gt; — tuning top_k and rerank thresholds isn't a one-time setting, it drifts as your document set grows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User trust&lt;/strong&gt; — spent every time a technically-present answer gets missed anyway&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Bringing It Back to One Sentence
&lt;/h2&gt;

&lt;p&gt;More context was never the safety net it felt like. Past a certain point, every extra chunk you retrieve is a small, compounding tax — paid in tokens, paid in the model's attention, and sometimes paid twice over when a team responds to bad retrieval by reaching for a bigger model instead of a better one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The best retrieval system isn't the one that sends the most. It's the one that's confident enough to send less.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing the Series
&lt;/h2&gt;

&lt;p&gt;I started this series certain that embeddings were where RAG became expensive. Six episodes later, the actual pattern looks nothing like that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Extraction   → decides what knowledge survives at all
Chunking     → decides how that knowledge gets sliced, and how much it costs to slice it
Metadata     → decides which slices retrieval is even allowed to consider
Vector DB    → decides what it costs to keep all of it instantly reachable
Retrieval    → decides how much of it actually reaches the model, and how well it's chosen
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of those is a quality-vs-cost decision, made quietly, usually without realizing it was a decision at all. And every one of them compounds — a mistake made once at ingestion gets paid for again on every query that touches it, for as long as it stays unfixed.&lt;/p&gt;

&lt;p&gt;The LLM call at the end was never really the expensive part on its own. It's the stage that inherits every decision made before it, and charges you for all of them at once, every single time someone asks a question.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Less noise, more action. Series closed — for now.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>computerscience</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Where Does RAG Actually Cost You Money? (Episode 5)</title>
      <dc:creator>surajrkhonde</dc:creator>
      <pubDate>Thu, 06 Aug 2026 12:20:41 +0000</pubDate>
      <link>https://dev.to/surajrkhonde/where-does-rag-actually-cost-you-money-episode-5-183d</link>
      <guid>https://dev.to/surajrkhonde/where-does-rag-actually-cost-you-money-episode-5-183d</guid>
      <description>&lt;h3&gt;
  
  
  Why a System That Just Stores Numbers Becomes One of the Most Expensive Things You Run
&lt;/h3&gt;




&lt;p&gt;Vector database day one felt like a non-event. A few thousand vectors, a single node, queries coming back in well under a hundred milliseconds. I remember thinking this was the easy part of the pipeline — extraction had bugs, chunking had trade-offs, metadata had gaps, but the vector database just... worked. Store a vector, search a vector, done.&lt;/p&gt;

&lt;p&gt;Then real production load hit it. Not a demo. Not a test set. The actual document volume, the actual query traffic, running continuously instead of in short bursts.&lt;/p&gt;

&lt;p&gt;Latency crept up first. Queries that used to return instantly started taking noticeably longer, especially during the hours ingestion jobs were running alongside live traffic. Then memory usage started climbing in a way that didn't match how much data I thought I'd added. Then a bulk update — adding a batch of new documents — locked things up long enough that new content wasn't searchable for a while after it was technically "in" the database.&lt;/p&gt;

&lt;p&gt;None of that happened on day one, with a few thousand vectors on a single node. All of it showed up once the numbers got real. That's when it hit me: I'd been thinking of the vector database as &lt;em&gt;storage&lt;/em&gt;. It isn't. It's a live system that has to stay in memory, stay indexed, and stay searchable in milliseconds, all day, every day — and every one of those requirements taxes a different resource as scale goes up.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Idea This Whole Article Is About
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A vector database isn't a place you keep vectors. It's a system you keep running — in memory, 24/7 — and storage, indexing, querying, and updating each tax it in a completely different way.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Embeddings get created once per chunk. The vector database has to hold the result of that forever, and answer a similarity search against it instantly, no matter how large it's grown or how often it's changing underneath you. That's a fundamentally different kind of cost than anything earlier in the pipeline — it doesn't happen once. It runs continuously, whether or not anyone's asking a question right now.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost 1 — Storage Is Bigger Than It Looks
&lt;/h2&gt;

&lt;p&gt;The first surprise: the index costs more space than the raw vectors do.&lt;/p&gt;

&lt;p&gt;A raw vector is just numbers — dimension count times bytes per number. But most vector databases don't search raw vectors directly at scale. They build an index structure on top (commonly something like HNSW or IVF) to make search fast, and that index structure carries its own overhead — graph connections, cluster centroids, auxiliary bookkeeping — sitting on top of the vectors themselves.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Raw vectors
      │
      ▼
Index structure built on top (HNSW / IVF / etc.)
      │
      ▼
Index overhead adds meaningfully more memory than the vectors alone
      │
      ▼
Actual footprint is noticeably bigger than "vector count × dimension size" suggests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Illustrative, not measured:&lt;/strong&gt; a million vectors at a common embedding size might sit around a few gigabytes as raw numbers — but once you add index overhead, that same million vectors can realistically occupy a good deal more than that in actual memory. The exact multiplier depends entirely on the index type and settings you choose. The point isn't the specific number — it's that "vector count times dimension" undercounts the real footprint every time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost 2 — RAM Is the Real Recurring Bill
&lt;/h2&gt;

&lt;p&gt;Storage on disk is cheap. Storage in memory is not — and for the index to answer queries in milliseconds, most of it needs to live in RAM, not on disk.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Index needs to stay in memory for fast search
      │
      ▼
More vectors → more RAM required to hold the index
      │
      ▼
RAM is the most expensive resource per gigabyte in most infrastructure
      │
      ▼
This cost exists 24/7, whether or not a single query is running right now
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the cost that makes a vector database feel different from a normal database. A normal database can happily keep most of its data on disk and page things in as needed. A vector index that has to page in and out of memory constantly to answer a similarity search stops being fast — so the whole point of paying for RAM is to avoid that. You're not paying to store the data. You're paying to keep it &lt;em&gt;instantly reachable&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost 3 — Building the Index Isn't Free Either
&lt;/h2&gt;

&lt;p&gt;Adding a vector to a flat list is trivial. Adding a vector to an HNSW graph means recalculating where it fits relative to its neighbors — that's real CPU work, and it happens for every single vector you add, not just at the end.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;New batch of documents ingested
      │
      ▼
Each new vector needs to be placed correctly in the index graph
      │
      ▼
CPU-intensive, scales with both index size and batch size
      │
      ▼
Large bulk ingests can visibly slow down or briefly block live queries
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the "bulk update locked things up" moment from the opening story. Index construction isn't a side effect of storage — it's an active, ongoing computation that has to happen every time new data arrives, and it competes for the same CPU and memory that's simultaneously trying to serve live search traffic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost 4 — Querying Gets More Expensive as the Index Grows
&lt;/h2&gt;

&lt;p&gt;Even with a good index, search compute isn't free, and it isn't flat as data grows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Small index
      │
      ▼
Search touches a small neighborhood of the graph → fast, cheap

Large index (same query, much more data)
      │
      ▼
Search has to traverse more of the graph to find the same quality of match
      │
      ▼
More compute per query, potentially slower response, especially under concurrent load
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Approximate indexes exist specifically to keep this cost from growing linearly with data size — but "approximate" is doing real work in that sentence. There's a dial (often called something like &lt;code&gt;ef&lt;/code&gt; or &lt;code&gt;nprobe&lt;/code&gt; depending on the system) that trades search thoroughness for speed. Turn it down for speed, and you're now trading a little bit of retrieval accuracy for it — which quietly becomes a different cost, a few episodes back: a slightly worse match retrieved, a slightly less complete answer, a retry.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost 5 — Latency at Scale
&lt;/h2&gt;

&lt;p&gt;This is really Costs 2 through 4, felt by an actual user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;More vectors + more concurrent queries
      │
      ▼
More RAM pressure, more CPU contention, larger graph to traverse
      │
      ▼
Response time per query creeps upward
      │
      ▼
Users notice the assistant feels "slower" than it used to
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing has to be broken for this to happen. Nothing throws an error. The system just gradually gets heavier as it grows, and unless someone is watching query latency over time, this cost hides in plain sight — until a user finally says the bot feels sluggish, and there's no single bug to point at, just accumulated scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost 6 — Replicas and High Availability
&lt;/h2&gt;

&lt;p&gt;A single node holding your entire index is one hardware failure away from your whole RAG system going down. So production systems run replicas — multiple copies of the same index, able to serve queries in parallel and take over if one node fails.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 node   → 1× RAM cost, 1× CPU cost, no redundancy
3 nodes  → 3× RAM cost, 3× CPU cost, real redundancy and more query throughput
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a straightforward multiplier, and it's easy to underestimate because it doesn't feel like a "new" cost — it feels like the same cost, just safer. But every one of Costs 1 through 5 above gets multiplied by however many replicas you decide you need for uptime and query throughput.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting a Number on It
&lt;/h2&gt;

&lt;p&gt;Same as Episode 3 — a small worked example makes this easier to feel than a paragraph of description alone. &lt;strong&gt;These figures are illustrative, not measured from a real system&lt;/strong&gt; — the point is the shape of the trade-off, not the exact digits.&lt;br&gt;
&lt;/p&gt;

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

1 million vectors, single replica
      │
      ▼
Raw vectors: a few GB
Index overhead on top: noticeably more
      │
      ▼
Rough ballpark: somewhere in the low tens of GB of RAM to keep it fast
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now scale that by replicas and by a common cost-saving move:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 replica   →  ~X GB RAM   (baseline, no redundancy)
3 replicas  →  ~3× X GB RAM  (same data, held three times, for uptime + throughput)

Apply quantization (e.g. compress vectors to lower precision)
      │
      ▼
RAM footprint drops meaningfully — often by roughly half or more,
depending on the technique
      │
      ▼
In exchange: a small, tunable dip in search accuracy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing about the vector count changed in that last step. The only thing that moved was a compression setting — and it moved the RAM bill in one direction and the accuracy dial in the other. That's the entire vector database cost story in one example: every lever you pull to bring the resource bill down quietly pulls a different lever — accuracy, latency, or engineering effort — in the opposite direction.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost 7 — Updating and Deleting (the one nobody plans for)
&lt;/h2&gt;

&lt;p&gt;Adding new vectors is one problem. Removing or updating old ones is a different, uglier problem, and it's the vector-database version of Episode 3's rechunking cost.&lt;/p&gt;

&lt;p&gt;Many index structures don't handle deletion cleanly. A "deleted" vector is often just marked as a tombstone rather than actually removed — the index still has to carry it around, still touches it during traversal, until a periodic compaction or rebuild actually reclaims that space.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Document gets updated or removed
      │
      ▼
Old vector marked as deleted, not actually removed
      │
      ▼
Index keeps carrying the dead weight
      │
      ▼
Search quality and speed slowly degrade as tombstones accumulate
      │
      ▼
Eventually requires a full index rebuild to actually clean up
      │
      ▼
Rebuild is CPU-heavy and, depending on the system, can mean a period of reduced search quality or availability while it runs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the cost that quietly punishes systems where documents change often — policies get updated, products get discontinued, prices change. Every one of those real-world updates leaves a small trace in the index that has to be paid for eventually, usually by an engineer noticing search quality has degraded and tracing it back to a compaction that never ran.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Production Actually Makes This Better
&lt;/h2&gt;

&lt;p&gt;None of the above means vector databases are a lost cause at scale — it means the cost has to be actively managed instead of assumed away. A few things production systems lean on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quantization&lt;/strong&gt; — storing vectors at lower precision (for example, converting from 32-bit floats to 8-bit integers or similar compressed forms) to cut RAM usage substantially, at a small, tunable cost to search accuracy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tiered storage&lt;/strong&gt; — keeping frequently-accessed or recent vectors in memory, and pushing colder, rarely-queried vectors to disk, so RAM is spent where it actually earns its cost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sharding&lt;/strong&gt; — splitting the index across multiple nodes by some logical boundary, so no single node has to hold the entire dataset in memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Metadata pre-filtering&lt;/strong&gt; — the Episode 4 payoff shows up here directly. Filtering by metadata &lt;em&gt;before&lt;/em&gt; the similarity search runs means the search only has to traverse a relevant subset of the index, not the whole thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batched, off-peak reindexing&lt;/strong&gt; — running index rebuilds and compaction during low-traffic windows instead of live, so Cost 7's cleanup doesn't compete with real user queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caching&lt;/strong&gt; — for genuinely repeated queries, skipping the vector search entirely and serving a cached result, which is the cheapest possible answer to "how do I reduce vector DB load."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these is a real trade-off, not a free win — quantization costs some accuracy, tiered storage costs some latency on cold data, sharding costs engineering complexity. Production tuning isn't about eliminating these costs. It's about deciding, deliberately, which resource you'd rather spend.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost Isn't Just a Dollar Amount, Again
&lt;/h2&gt;

&lt;p&gt;Same pattern as the last two episodes. The vector database spends:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RAM&lt;/strong&gt; — the single biggest recurring cost, paid 24/7 regardless of query volume&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CPU&lt;/strong&gt; — index construction, rebuilds, and compaction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disk I/O&lt;/strong&gt; — for tiered or on-disk portions of the index&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network&lt;/strong&gt; — moving query results and replicating data across nodes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency&lt;/strong&gt; — felt directly by users as the system grows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Availability&lt;/strong&gt; — the cost of &lt;em&gt;not&lt;/em&gt; having enough replicas when a node fails&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search accuracy&lt;/strong&gt; — every speed optimization trades away some amount of this&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engineering time&lt;/strong&gt; — tuning index parameters, planning reindex windows, monitoring degradation that never throws an error&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User trust&lt;/strong&gt; — spent quietly, the moment "the bot feels slow" becomes something people mention out loud&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Bringing It Back to One Sentence
&lt;/h2&gt;

&lt;p&gt;Everything earlier in this series — extraction, chunking, metadata — happens once, or close to it, per document. The vector database is the first stage in this series that never stops running. It's not a cost you pay once at ingestion. It's a cost you keep paying, every second the system is on, that grows with every document you add and every replica you need for it to stay fast and available.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A vector database doesn't charge you for the vectors. It charges you for keeping them instantly reachable — and "instantly reachable, always" is one of the most expensive promises in the entire pipeline.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Coming up in Episode 6
&lt;/h3&gt;

&lt;p&gt;The vector database taught me that just keeping data searchable is expensive on its own. But there's a question I've been quietly avoiding: even with a fast, well-tuned index, am I actually sending the LLM the &lt;em&gt;right&lt;/em&gt; chunks — or just the closest ones?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why fewer, better-chosen chunks usually beat a bigger, more expensive model.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Less noise, more action. Let's dig.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>productivity</category>
      <category>computerscience</category>
    </item>
    <item>
      <title>Episode 6 — Watching Something You Can't See</title>
      <dc:creator>surajrkhonde</dc:creator>
      <pubDate>Wed, 05 Aug 2026 03:40:11 +0000</pubDate>
      <link>https://dev.to/surajrkhonde/episode-6-watching-something-you-cant-see-15eb</link>
      <guid>https://dev.to/surajrkhonde/episode-6-watching-something-you-cant-see-15eb</guid>
      <description>&lt;p&gt;&lt;em&gt;Week 3. "The deploy is done. Everything's green. Now what am I actually supposed to be looking at?"&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Previously
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Runner
    ↓
Cache
    ↓
Artifact
    ↓
Deployment

Today
    ↓
Monitoring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; The canary rolled out fine yesterday. 100% traffic, all healthy. I closed my laptop. Was that wrong?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Not wrong, exactly. But let me ask you something first. Your service is running on a server somewhere. Right now, this second — is it healthy?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; I mean... I assume so? Nobody's messaged me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; "Nobody's messaged me" isn't an answer. It's the absence of one. That's the entire problem monitoring exists to solve.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Thing Nobody Says Out Loud
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Here's an uncomfortable fact about production systems: you cannot see them. Not directly. You're not standing next to the server, watching electricity move through it. Everything you know about whether it's healthy is a claim — something a piece of software told you, that you're choosing to trust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; That sounds obvious when you say it, but I don't think I've ever actually thought about it that way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Most engineers don't, until the gap between "the system told me it's fine" and "the system is actually fine" bites them. Monitoring is the discipline of shrinking that gap — of making sure what you're told is close to what's actually true, and told to you fast enough to matter.&lt;/p&gt;




&lt;h3&gt;
  
  
  📒 Senior Engineer's Notebook
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;You don't monitor a system because you don't trust it. You monitor it because you can't see it. Trust isn't the issue — visibility is.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  The Car Dashboard Analogy
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Can you make this concrete?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Think about driving a car. You can't see the engine. You can't see the oil level, the coolant temperature, how much fuel is actually left in the tank, mid-drive. All of that is invisible to you, sealed inside metal, while you're doing 100 km/h.&lt;/p&gt;

&lt;p&gt;So the car gives you a dashboard. Speed, fuel, engine temperature, warning lights. You're not watching the engine. You're watching &lt;em&gt;proxies&lt;/em&gt; for the engine — numbers and lights standing in for things you can't directly observe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; And a production system is the engine. Monitoring is the dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Exactly that. And just like a car, the dangerous failure mode isn't "the dashboard shows a problem." It's "the dashboard shows everything's fine, and it's wrong." A dashboard that lies is worse than no dashboard — because no dashboard, at least, you know you're flying blind.&lt;/p&gt;




&lt;h3&gt;
  
  
  What Actually Gets Measured
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Okay, so — what goes on the dashboard? What do you actually watch?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; There's a well-known starting set, sometimes called the four golden signals. Not the only things worth tracking, but the ones that catch the most real problems the fastest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Latency&lt;/strong&gt; — how long requests take. Not just the average — the average can look perfectly healthy while 5% of your users wait eight seconds. You want percentiles: p50, p95, p99. What's typical, and what's the bad end of typical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traffic&lt;/strong&gt; — how many requests you're actually getting. Without this number, every other number is meaningless. An error rate of 2% means something completely different at ten requests a minute versus ten thousand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Errors&lt;/strong&gt; — the rate of requests failing. Not just 500s — a request that "succeeds" with the wrong data is a failure your server doesn't know to report as one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Saturation&lt;/strong&gt; — how close your system is to its limit. CPU, memory, database connections, queue depth. A system can have zero errors right now and still be forty seconds from falling over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; That last one feels different from the others. The others are about what's happening. Saturation is about what's &lt;em&gt;about&lt;/em&gt; to happen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; That's a sharp distinction, and it's exactly right. Latency, traffic, and errors tell you the present. Saturation is close to the only one of the four that gives you a warning &lt;em&gt;before&lt;/em&gt; the present turns bad.&lt;/p&gt;




&lt;h3&gt;
  
  
  🪞 If I asked you this in an interview
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;"What are the four golden signals, and why those four specifically?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Latency, traffic, errors, and saturation. Together they answer the questions that matter most during an incident: is it slow, how much load is it under, is it actually failing, and how close is it to running out of capacity. Most production problems show up in at least one of these four before they show up anywhere else — which is why they're the default starting point rather than an exhaustive list.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Monitoring vs Alerting
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Is a dashboard the same thing as monitoring, then? Just — numbers on a screen?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; A dashboard is monitoring's &lt;em&gt;visible&lt;/em&gt; half. But a dashboard only helps if someone's staring at it at the exact moment something breaks. Nobody's staring at a dashboard at 3 AM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So how does anyone find out at 3 AM?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; That's &lt;strong&gt;alerting&lt;/strong&gt; — the other half. You define a threshold: error rate above 5% for two minutes, latency p99 above 3 seconds, saturation above 90%. Cross that threshold, and instead of waiting for a human to notice a graph, the system pages someone. This is where PagerDuty from Episode 1 actually connects to everything we've built since — the pager doesn't go off because a human is watching. It goes off because a machine was, continuously, and a human wasn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So monitoring is the measuring. Alerting is the "go wake somebody up" part.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Precisely. Monitoring without alerting is a dashboard nobody's watching. Alerting without monitoring doesn't exist — there's nothing to alert on.&lt;/p&gt;

&lt;p&gt;And dashboards aren't only useful during an outage, worth saying explicitly. A healthy-looking dashboard, watched over days and weeks, is how teams catch trends before they become incidents at all — latency creeping up 5% every release, memory usage climbing slightly with each deploy, error rate drifting from 0.1% to 0.4% over a month with nothing dramatic enough to trip an alert. Nothing there pages anyone. But someone glancing at trends, not just thresholds, catches the slow version of the same story the fast version tells at 3 AM.&lt;/p&gt;




&lt;h3&gt;
  
  
  📝 Production Note
&lt;/h3&gt;

&lt;p&gt;Remember correlation IDs and structured logs, from Episode 1? That's not a separate topic from monitoring — it's the raw material monitoring is built from. A dashboard showing "errors spiked at 2:14 PM" tells you &lt;em&gt;that&lt;/em&gt; something broke. Structured logs, searchable by correlation ID, are what let you find out &lt;em&gt;which&lt;/em&gt; request, for &lt;em&gt;which&lt;/em&gt; user, touching &lt;em&gt;which&lt;/em&gt; service — the difference between knowing something's wrong and knowing what to actually fix.&lt;/p&gt;

&lt;p&gt;In modern observability, engineers usually lean on three sources of truth together, not one alone: &lt;strong&gt;metrics&lt;/strong&gt; tell you &lt;em&gt;something&lt;/em&gt; is wrong — a number crossed a line. &lt;strong&gt;Logs&lt;/strong&gt; help explain &lt;em&gt;what happened&lt;/em&gt; — the specific events around it. &lt;strong&gt;Traces&lt;/strong&gt; show &lt;em&gt;where the request spent its time&lt;/em&gt; as it moved across multiple services. You'll meet traces properly once we're deep into distributed systems — for now, just know the three aren't competing tools, they're three different angles on the same question.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Alert Fatigue Trap
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; This seems easy to over-do, though. Why not just alert on everything, so nothing slips through?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Because a human being can only take so many 3 AM pages before they stop trusting them. That's called &lt;strong&gt;alert fatigue&lt;/strong&gt;, and it's one of the most common ways monitoring quietly fails — not by missing a real incident, but by burying it under noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; How does that actually play out?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Say your team gets paged fifteen times a week. Twelve of those are noise — a brief blip that self-resolved, a threshold set too aggressively, a known flaky check. After a few weeks of that, the on-call engineer starts silencing pages before fully reading them. Reflex, not laziness — self-preservation.&lt;/p&gt;

&lt;p&gt;Then, one night, page sixteen is the real one. And it gets the same half-second of attention as the twelve before it that didn't matter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So too many alerts is almost worse than too few.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; In a very specific sense, yes — because too few means a known gap. Too many means a false sense of coverage while the actual signal quietly drowns.&lt;/p&gt;

&lt;p&gt;Two terms are worth having exact language for here, because you'll hear them constantly. A &lt;strong&gt;false positive&lt;/strong&gt; is an alert that fires when nothing's actually wrong — that's the noise causing fatigue. A &lt;strong&gt;false negative&lt;/strong&gt; is the opposite and more dangerous failure: something &lt;em&gt;is&lt;/em&gt; wrong, and no alert fires at all. Tuning alerts is really just a constant tradeoff between the two — tighten thresholds to catch more real problems, and you risk more false positives; loosen them to reduce noise, and you risk missing something real. There's no setting that eliminates both.&lt;/p&gt;




&lt;h3&gt;
  
  
  👦 Junior Framing / 👨‍🦳 Senior Framing
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior framing:&lt;/strong&gt; "More alerts means better monitoring."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior framing:&lt;/strong&gt; More alerts means more noise, unless each one is something a human genuinely needs to act on right now. A good alert is a promise: if this fires, it matters, and someone needs to do something about it immediately. Break that promise enough times and the alert stops meaning anything.&lt;/p&gt;




&lt;h3&gt;
  
  
  SLOs and Error Budgets
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; How do teams decide what "healthy" even means numerically? Is 99% uptime good? 99.9%?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; This is where &lt;strong&gt;SLOs&lt;/strong&gt; — service level objectives — come in. A team explicitly decides: "99.9% of requests should succeed, measured over 30 days." That's not aspirational marketing language. It's a number the team commits to, and measures against.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Why not just aim for 100%?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Because 100% is enormously expensive, and past a certain point, chasing it stops making users happier and starts just slowing the team down. 99.9% still means about 43 minutes of downtime a month are considered acceptable — not desired, but acceptable, a deliberately budgeted amount of failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Budgeted failure. That's a strange phrase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; It's called an &lt;strong&gt;error budget&lt;/strong&gt;, and the phrase is supposed to feel strange — it's meant to change behavior. If you've used up this month's error budget on a risky deploy that caused an outage, that's a signal: slow down, stabilize, stop taking risky bets for a while. If you've got budget left, you have room to ship faster and take more chances. It turns "is it safe to deploy" from a gut feeling into an actual number.&lt;/p&gt;




&lt;h3&gt;
  
  
  🚨 Beginner Mistakes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;"I'll alert on CPU usage being high."&lt;/strong&gt; High CPU isn't inherently bad — sometimes it means the system is efficiently using the resources it has. Alert on the thing that actually matters to users: is latency degrading, are requests failing. CPU is a diagnostic detail, not the actual problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I'll set every alert threshold as tight as possible, to catch things early."&lt;/strong&gt; This is how alert fatigue starts. A threshold that fires on normal, healthy variation trains people to ignore it — right up until the day it fires for something real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"The dashboard looks fine, so we're done watching."&lt;/strong&gt; A dashboard is a snapshot of now. The four golden signals can look perfectly healthy one minute before a slow memory leak crosses a threshold. Monitoring is a continuous practice, not a one-time check after deploying.&lt;/p&gt;




&lt;h3&gt;
  
  
  🏢 Office Reality
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"We're flying blind on this service."&lt;/strong&gt; No meaningful monitoring exists for it — nobody would know if it broke until a user complained.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"That alert is noisy."&lt;/strong&gt; It fires often enough, for things that don't actually matter, that people have started ignoring it. A polite way of saying "this alert has stopped doing its job."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"We burned our error budget."&lt;/strong&gt; The team exceeded their allowed failure rate for the period — often the trigger for pausing risky launches until things stabilize.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Check the dashboards before you page anyone."&lt;/strong&gt; Standard on-call instinct — confirm what's actually happening before waking up a second person for something that might resolve on its own.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  The Alert That Almost Wasn't
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Has an alert actually caught something real for you — not in theory, an actual time?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; More than once, but here's a clean one. A saturation alert — database connection pool climbing toward its limit — fired quietly on a Tuesday afternoon. No errors yet. No user complaints. Latency barely moved.&lt;/p&gt;

&lt;p&gt;Easy to ignore. Nothing was actually broken yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; But someone looked anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Someone looked anyway, because the alert existed and the team trusted it enough not to reflexively dismiss it — which, notice, is the entire alert fatigue conversation paying off in real time. Turned out a recent deploy had a connection leak — every request opened a database connection and a rare error path failed to close it. Slow leak. Would've taken maybe three more hours to actually exhaust the pool and start failing every single request, in the middle of the evening peak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So the alert fired before anything was actually broken.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; That's the entire point of saturation as a signal. It's not telling you what's wrong right now. It's telling you what's about to be wrong, while there's still time to do something other than panic.&lt;/p&gt;




&lt;h3&gt;
  
  
  🎯 Interview Perspective
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Interviewer:&lt;/strong&gt; How would you decide what to put an alert on?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weak answer:&lt;/strong&gt; Alert on anything that could go wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strong answer:&lt;/strong&gt; Alert on symptoms that directly affect users or clearly predict an imminent failure — elevated error rate, degraded latency, saturation approaching a limit — not on every internal metric that could theoretically be interesting. Every alert should represent something a human genuinely needs to act on right now; anything less specific belongs on a dashboard for investigation, not in someone's pocket at 3 AM.&lt;/p&gt;




&lt;h3&gt;
  
  
  🎤 Explain It In One Minute
&lt;/h3&gt;

&lt;p&gt;Imagine explaining this to a teammate — without using the words &lt;em&gt;monitoring&lt;/em&gt;, &lt;em&gt;alerting&lt;/em&gt;, or &lt;em&gt;SLO&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Why is "the deploy went fine" not the same claim as "the system is healthy"?&lt;/p&gt;

&lt;p&gt;If you can answer that without reaching for the vocabulary, you understand the idea driving this entire episode.&lt;/p&gt;




&lt;h3&gt;
  
  
  Whiteboard Moment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Production system running (invisible to you directly)
    ↓
Metrics collected continuously
    (latency, traffic, errors, saturation)
    ↓
Dashboards — visible, but nobody's always watching
    ↓
Thresholds defined against SLOs
    ↓
Threshold crossed → alert fires → someone paged
    ↓
Structured logs + correlation IDs → find the specific cause
    ↓
Fixed, or escalated toward an incident
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So this whole episode is really just: you can't see it, so you measure it, and you don't wait for a human to notice the measurement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; That's the entire discipline in one sentence. Everything else — golden signals, SLOs, error budgets — is just making that sentence precise enough to actually act on.&lt;/p&gt;




&lt;h3&gt;
  
  
  What You Should Be Able to Explain Now
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;(Without looking at Google)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Can you explain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why "the deploy succeeded" and "the system is healthy" are different claims?&lt;/li&gt;
&lt;li&gt;What the four golden signals are, and why saturation is different from the other three?&lt;/li&gt;
&lt;li&gt;The difference between monitoring and alerting?&lt;/li&gt;
&lt;li&gt;Why too many alerts can be more dangerous than too few?&lt;/li&gt;
&lt;li&gt;What an SLO and an error budget actually are, in plain terms?&lt;/li&gt;
&lt;li&gt;Why saturation alerts can fire before anything is technically broken yet — and why that's the point?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If yes — you understand production visibility the way it's actually practiced, not just as a dashboard you glance at after a deploy.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Okay, so say an alert &lt;em&gt;does&lt;/em&gt; fire. For real, not a false alarm. What happens next?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Now we're back exactly where Episode 1 started — Friday, 7:02 PM, PagerDuty going off. Except this time, you're not watching from the outside. You already know what a webhook is, what a runner is, what an artifact is, how it got deployed, and what the dashboard was supposed to be telling someone the whole time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So next time, I'm not just shadowing. I'm actually part of figuring it out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; That's the idea. Incident response, properly this time — from the inside.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(End of Episode 6)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Episode 5 — Who Gets to Flip the Switch</title>
      <dc:creator>surajrkhonde</dc:creator>
      <pubDate>Mon, 03 Aug 2026 23:36:16 +0000</pubDate>
      <link>https://dev.to/surajrkhonde/episode-5-who-gets-to-flip-the-switch-2d9g</link>
      <guid>https://dev.to/surajrkhonde/episode-5-who-gets-to-flip-the-switch-2d9g</guid>
      <description>&lt;p&gt;&lt;em&gt;Week 3. "The artifact is sitting in the registry. Somebody still has to actually run it. Who, and how?"&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Previously
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer
    ↓
Runner
    ↓
Cache
    ↓
Artifact

Today
    ↓
Deployment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So the image is built, tagged, sitting in the registry. What's the simplest possible way to actually get it running in production?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; The simplest way is also the most dangerous way. Want to guess it first?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Stop the old container. Start the new one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; That's it. That's genuinely how a lot of side projects deploy. Now — what's wrong with it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; There's a gap, right? Between "stop old" and "start new," nothing's running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Exactly. That gap has a name — &lt;strong&gt;downtime&lt;/strong&gt;. Might be half a second. Might be twenty, if the new container is slow to boot, run migrations, or warm up a cache. Either way, every request that lands in that gap fails. For a side project, nobody notices. For a payment API, that gap is real money and real angry users.&lt;/p&gt;




&lt;h3&gt;
  
  
  📝 Production Note
&lt;/h3&gt;

&lt;p&gt;The naive "stop old, start new" approach is technically a valid deployment strategy — it's just usually the wrong one. It has a name too: &lt;strong&gt;recreate&lt;/strong&gt;. Some internal tools genuinely use it on purpose, because a few seconds of downtime for an admin dashboard nobody's actively using at 3 AM is a fair trade for simplicity. The mistake isn't using it — it's using it &lt;em&gt;without deciding to&lt;/em&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Real Question Underneath
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; So the actual engineering question isn't "how do I run the new version." It's: &lt;strong&gt;can old and new both be running at the same time, safely, long enough for the switch to happen with zero gap?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Both running at once sounds messy. Wouldn't they conflict?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Only if you let them share something they shouldn't — like both writing to the same in-memory cache in incompatible ways. Mostly, though, this is exactly what modern deployment strategies are built around: overlap on purpose, then cut over cleanly.&lt;/p&gt;




&lt;h3&gt;
  
  
  Blue-Green Deployment
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; First strategy: &lt;strong&gt;blue-green&lt;/strong&gt;. You keep two full production environments — call them Blue and Green. Right now, Blue is live, serving all real traffic. Green sits idle.&lt;/p&gt;

&lt;p&gt;To deploy, you start the new version on Green. Nobody's routing traffic there yet — it's just running, warming up, ready to be checked. You hit its health endpoint, run a smoke test, maybe watch its logs for a minute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; And once you're confident it's healthy?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; You flip the router. All traffic that used to go to Blue now goes to Green. Instantly — not gradually. One moment Blue is live, the next moment Green is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; And Blue?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Stays up, idle, doing nothing. On purpose. If something's wrong with Green that only shows up under real traffic, you flip the router back to Blue. Instantly. No rebuild, no redeploy — Blue was never touched.&lt;/p&gt;




&lt;h3&gt;
  
  
  👦 Junior Framing / 👨‍🦳 Senior Framing
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior framing:&lt;/strong&gt; "Rollback means redeploying the old version."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior framing:&lt;/strong&gt; Rollback means flipping a router back to something that was already running the whole time. The fastest rollback is one where you never had to deploy anything to perform it.&lt;/p&gt;




&lt;h3&gt;
  
  
  Rolling Deployment
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Blue-green needs double the infrastructure, though — two full environments, most of it sitting idle. That seems expensive for a small team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; It is, and that's exactly why most teams don't run pure blue-green. More common: &lt;strong&gt;rolling deployment&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Say you have ten instances of your app running behind a load balancer. Rolling deployment replaces them a few at a time — stop one old instance, start one new instance, wait until it's healthy, move to the next. Two or three at a time, not all at once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So at any given moment during the rollout, some requests hit the old version and some hit the new one?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Exactly. For a minute or two, both versions are genuinely serving real production traffic simultaneously. That's usually fine — but it's also exactly why "both versions must be able to coexist safely" isn't a throwaway line. If the new version writes data in a shape the old version can't read, rolling deployment will surface that mid-rollout, in production, with real users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; That sounds like a real risk, not a theoretical one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; It's one of the most common causes of a "weird, intermittent bug for about ninety seconds" report — someone hit an old instance, someone else hit a new one, and the two disagreed about something.&lt;/p&gt;




&lt;h3&gt;
  
  
  📒 Senior Engineer's Notebook
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;A rolling deployment isn't "old version, then new version." For a window in the middle, it's both — genuinely, simultaneously, in production. Any deployment strategy that overlaps old and new is really asking one question: are these two versions allowed to be strangers to each other for a few minutes?&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Canary Deployment
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Is there something more cautious than rolling? What if I don't trust the new version at all yet?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; That's &lt;strong&gt;canary deployment&lt;/strong&gt;. Instead of replacing instances in bulk, you send the new version a &lt;em&gt;small&lt;/em&gt; slice of real traffic first — maybe 5%. Everyone else stays on the old version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Why 5% specifically?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; No fixed number — depends on your traffic volume and risk tolerance. The idea is: expose the new version to &lt;em&gt;real&lt;/em&gt; conditions, real users, real edge cases your tests never thought to write — but limit the blast radius if something's wrong. If 5% of users hit a bug, that's bad. It's a much smaller bad than 100% of users hitting it.&lt;/p&gt;

&lt;p&gt;You watch error rates, latency, whatever your metrics track, on that 5% specifically. Looks healthy — increase the slice. 5%, then 25%, then 100%. Looks unhealthy at any point — send that slice back to the old version and stop the rollout.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So canary is basically a controlled experiment running in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; That's precisely what it is. The name comes from coal mining — a canary in the mine would show signs of trouble from bad air before a miner did. Same idea: let a small, contained group hit trouble first, before it reaches everyone.&lt;/p&gt;




&lt;h3&gt;
  
  
  🎯 Interview Perspective
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Interviewer:&lt;/strong&gt; When would you choose canary over blue-green?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weak answer:&lt;/strong&gt; Canary is safer, so always use canary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strong answer:&lt;/strong&gt; Blue-green gives you instant rollback but costs double the infrastructure and validates the new version with synthetic checks, not real traffic diversity. Canary costs less infrastructure and exposes the new version to genuinely real, varied traffic — but a rollback means shifting traffic back gradually, and any bug still affects some real users before it's caught. Blue-green suits changes you're fairly confident in but want an instant undo for. Canary suits changes you're genuinely unsure about and want real signal on before committing.&lt;/p&gt;




&lt;h3&gt;
  
  
  Health Checks — The Thing That Makes All of This Safe
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Every strategy you've described leans on "check it's healthy" before shifting more traffic. How does that check actually work?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; A &lt;strong&gt;health check&lt;/strong&gt; — usually a specific endpoint, something like &lt;code&gt;/healthz&lt;/code&gt;, that the new instance exposes. It doesn't just return "yes I'm running." A container can be running and still be broken — maybe it started but couldn't connect to the database.&lt;/p&gt;

&lt;p&gt;A real health check verifies the things that would make the instance &lt;em&gt;actually useless&lt;/em&gt; if they failed: can it reach the database, can it reach dependent services, has startup fully finished. Only once that endpoint returns healthy does the deployment system consider the instance eligible to receive real traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So without a health check, all of this — blue-green, rolling, canary — is just... hoping?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Precisely. The strategy decides &lt;em&gt;how much&lt;/em&gt; traffic shifts and &lt;em&gt;when&lt;/em&gt;. The health check decides whether it's actually safe to shift any at all. Remove the health check and you've built an elaborate system for exposing users to a broken deploy slightly more gradually — which isn't actually much of a win.&lt;/p&gt;




&lt;h3&gt;
  
  
  🚨 Beginner Mistakes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;"I'll skip the health check, the container starting is proof enough."&lt;/strong&gt; A container can start successfully and still be completely unable to serve a real request. "Started" and "ready" are different claims.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I'll shift 100% of traffic to canary if the first ten requests look fine."&lt;/strong&gt; Ten requests aren't a sample size. Real problems — memory leaks, rare edge cases, load-dependent bugs — often only appear after sustained, varied traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I'll test the new version by deploying straight to production and watching what happens."&lt;/strong&gt; That's canary deployment without the safety net — no controlled slice, no automatic rollback path, no separation between "found a problem" and "everyone already hit the problem."&lt;/p&gt;




&lt;h3&gt;
  
  
  Who Gets to Flip the Switch
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; All of this is mechanics. But somebody has to actually decide "yes, ship it." Who?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Depends entirely on the company — same spectrum we talked about back in Episode 1.&lt;/p&gt;

&lt;p&gt;At a small startup — usually, whoever pushed the code. Pipeline goes green, they deploy, no one else involved.&lt;/p&gt;

&lt;p&gt;At a growing company — a required approval step. Someone else has to click "approve" in the pipeline before it proceeds to production, even if the pipeline itself passed everything.&lt;/p&gt;

&lt;p&gt;At a bank — multiple named approvers, a change ticket, a scheduled deployment window, and a full audit log of exactly who approved what and when. Not because engineers there are less trusted — because the cost of an unapproved, unreviewed production change is categorically higher when real money moves through the system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So "who can deploy" isn't a technical question at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; It's a risk question wearing technical clothes. The pipeline enforces whatever answer the company has already decided on.&lt;/p&gt;




&lt;h3&gt;
  
  
  🏢 Office Reality
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Blue is live."&lt;/strong&gt; In a blue-green setup, this tells you which environment is currently receiving real traffic — useful shorthand nobody has to explain twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"The canary is unhealthy."&lt;/strong&gt; The small traffic slice on the new version is showing errors or bad metrics. Rollout should stop, not continue "to see if it improves."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"We're doing a cutover at 2 AM."&lt;/strong&gt; A full traffic switch, scheduled for low-traffic hours specifically because it's still considered risky enough to want minimal witnesses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Freeze the deploys."&lt;/strong&gt; No new deployments allowed for a period — common right before a major event (a sale, a product launch) when the cost of something breaking is unusually high.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  A Canary That Did Its Job
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Has canary actually caught something real, in your experience — or is it mostly theoretical safety?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Very real, once. A new version of a checkout service went out to 5% of traffic. Nothing dramatic — error rate ticked up, barely. Would've been easy to dismiss as noise.&lt;/p&gt;

&lt;p&gt;Someone didn't dismiss it. Turned out the new version had a bug that only triggered for a specific payment method, used by a small fraction of users. If that had gone to 100% immediately, it would've meant every user with that payment method, all day, failing at checkout — probably discovered from a spike in support tickets, an hour later, the hard way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Instead it was five percent of five percent of traffic, for a few minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; And a rollback that took seconds, because the other 95% was never touched in the first place. That's the entire value of canary in one sentence — not that it prevents bugs, but that it shrinks the blast radius of the ones you didn't catch.&lt;/p&gt;




&lt;h3&gt;
  
  
  🪞 If I asked you this in an interview
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;"Walk me through what happens, end to end, when a canary deployment detects a problem."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A small percentage of traffic is routed to the new version while the rest stays on the old one. Metrics — error rate, latency, whatever the team monitors — are compared between the two. If the new version's metrics degrade beyond a threshold, the deployment system automatically routes that traffic slice back to the old version and halts the rollout, rather than increasing the percentage further. Because only a small slice was ever exposed, the blast radius of the bug is limited to that slice, for that window of time.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  🎤 Explain It In One Minute
&lt;/h3&gt;

&lt;p&gt;Imagine explaining this to a teammate — without using the words &lt;em&gt;blue-green&lt;/em&gt;, &lt;em&gt;canary&lt;/em&gt;, or &lt;em&gt;rolling&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Why would a company deliberately send its own new code to only a small fraction of real users, instead of just launching it for everyone at once?&lt;/p&gt;

&lt;p&gt;If you can answer that without the vocabulary, you understand the reasoning — not just the pattern names.&lt;/p&gt;




&lt;h3&gt;
  
  
  Whiteboard Moment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Artifact in registry (tagged, immutable)
    ↓
Deployment approved (who's allowed depends on the company)
    ↓
New version starts on target infrastructure
    ↓
Health check confirms it's actually ready — not just running
    ↓
Traffic shifts according to strategy:
    Recreate    → all at once, brief gap
    Blue-Green  → instant full switch, old stays warm as fallback
    Rolling     → a few instances at a time, both versions coexist briefly
    Canary      → small % first, expand gradually if healthy
    ↓
Metrics watched throughout
    ↓
Healthy → rollout continues to 100%
Unhealthy → traffic reverts, rollout halts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So the pattern across every strategy is the same three questions, just answered differently: how much traffic moves, how fast, and what happens if it goes wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; That's the whole subject, compressed into one sentence. Everything else is implementation detail.&lt;/p&gt;




&lt;h3&gt;
  
  
  What You Should Be Able to Explain Now
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;(Without looking at Google)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Can you explain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why "stop old, start new" causes downtime, and when that tradeoff is actually acceptable?&lt;/li&gt;
&lt;li&gt;What blue-green buys you, and what it costs?&lt;/li&gt;
&lt;li&gt;Why rolling deployments mean two versions are briefly live together — and why that matters?&lt;/li&gt;
&lt;li&gt;Why canary deployments limit blast radius instead of preventing bugs?&lt;/li&gt;
&lt;li&gt;Why a health check is what makes every one of these strategies actually safe?&lt;/li&gt;
&lt;li&gt;Why "who's allowed to deploy" is a risk decision, not a technical one?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If yes — you understand deployment the way it's actually practiced, not just the vocabulary used to describe it in a job posting.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Okay. So say the rollout finishes. 100% of traffic, new version, all healthy. Are we done?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; For about as long as it takes someone to stop watching the dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Meaning?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Meaning "the deploy succeeded" and "the deploy is fine" are, once again, two different claims — same as "pipeline is green" back in Episode 1. A deploy can look perfect for twenty minutes and then start leaking memory. Or degrading under real end-of-day traffic that your canary window never saw.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So something has to keep watching, after the deploy is technically over.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Now you're standing right at the edge of the whiteboard we drew all the way back in Episode 1. Deployment. Then — Monitoring.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(End of Episode 5)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>productivity</category>
      <category>devops</category>
      <category>computerscience</category>
    </item>
    <item>
      <title>Episode 4 — The Thing You Actually Deploy</title>
      <dc:creator>surajrkhonde</dc:creator>
      <pubDate>Mon, 03 Aug 2026 13:12:08 +0000</pubDate>
      <link>https://dev.to/surajrkhonde/episode-4-the-thing-you-actually-deploy-5720</link>
      <guid>https://dev.to/surajrkhonde/episode-4-the-thing-you-actually-deploy-5720</guid>
      <description>&lt;p&gt;&lt;em&gt;Week 2. "My code passed tests. My dependencies are cached. So what actually gets deployed?"&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Previously
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer
    ↓
Runner
    ↓
Cache

Today
    ↓
Deployment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Last time you said "artifact" like it was obvious. It's not obvious to me. My code is just... files, in a repo, on a runner. What gets deployed?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Good instinct to stop here. Most engineers say the word "artifact" for years without being able to define it. Let's fix that today.&lt;/p&gt;




&lt;h3&gt;
  
  
  What Actually Gets Deployed
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Quick question first. Does production run &lt;em&gt;your repo&lt;/em&gt;?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; ...I mean, it runs my code, so — yes?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Think about what's sitting in your repo right now. Test files. A &lt;code&gt;.github/workflows/&lt;/code&gt; folder. Maybe a README, a &lt;code&gt;.gitignore&lt;/code&gt;, dev-only config. Does production need any of that to serve a request?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; No. None of that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; So production doesn't run your repo. It runs &lt;em&gt;something built from&lt;/em&gt; your repo — a narrower thing, with the parts that only matter during development stripped out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So what is that narrower thing, physically?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; That's the artifact.&lt;/p&gt;




&lt;h3&gt;
  
  
  What Is an Artifact?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; An &lt;strong&gt;artifact&lt;/strong&gt; is the packaged output of a build step — the exact thing that gets handed from CI to deployment. Not your source code. Not your repo. A specific, versioned, ready-to-run package.&lt;/p&gt;

&lt;p&gt;Depending on the project, it might be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A folder of compiled, minified JS and CSS — for a frontend app&lt;/li&gt;
&lt;li&gt;A single compiled binary — for a Go or Rust service&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;.jar&lt;/code&gt; file — for a Java service&lt;/li&gt;
&lt;li&gt;A Docker image — for almost anything, these days&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So "artifact" isn't one specific format. It's a category.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Exactly. The common thread isn't the file type. It's what it &lt;em&gt;represents&lt;/em&gt;: a frozen, self-contained snapshot of "this is what we're deploying," produced once, deployed possibly many times, never edited after it's built.&lt;/p&gt;




&lt;h3&gt;
  
  
  📒 Senior Engineer's Notebook
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Source code is a set of instructions for producing something. An artifact is the thing those instructions produced. You edit source code. You never edit an artifact — you build a new one.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Why Not Just Deploy the Repo Directly?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Genuinely — why not just have the production server pull the repo, like that Friday-deploy developer did in Episode 1?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; You already lived through why, you just didn't have the word for it yet. Remember what went wrong that Friday?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; He ran &lt;code&gt;npm install&lt;/code&gt; instead of &lt;code&gt;npm ci&lt;/code&gt;, so the exact dependency versions weren't guaranteed. And it ran directly on production with no record of what shipped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Both of those are artifact problems in disguise. An artifact is built &lt;em&gt;once&lt;/em&gt;, from a known commit, with a known dependency tree — and that exact same package is what runs in staging, and later in production. No "well it worked when I built it locally." No re-running &lt;code&gt;npm install&lt;/code&gt; on a live server and hoping the internet hands you the same versions twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So the artifact is what makes "promote staging to production" — that phrase from Episode 1 — actually true?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Now you're connecting things properly. "Promote staging to production" only means something if staging and production are running the &lt;em&gt;exact same bytes&lt;/em&gt;. That's only possible if you build once and deploy the same artifact everywhere, instead of rebuilding fresh at each stage and hoping nothing drifted in between.&lt;/p&gt;




&lt;h3&gt;
  
  
  📝 Production Note
&lt;/h3&gt;

&lt;p&gt;This is called "build once, deploy everywhere." Build the artifact a single time. Run it, unchanged, through staging. If it passes, run that same unchanged artifact in production. Never rebuild between environments — every rebuild is a chance for something to silently differ.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Suitcase Analogy
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Can you ground this? It's still a bit abstract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Think about packing for a trip. Your house has everything — off-season clothes, kitchen stuff, furniture, things you'll never need at your destination. You don't ship your house. You pack a suitcase: only what the trip actually needs, sealed shut, checked once at the airport.&lt;/p&gt;

&lt;p&gt;Your repo is the house. The artifact is the suitcase. The build step is the packing. And once that suitcase is checked in, nobody's supposed to open it and swap out a shirt mid-flight — what lands is what you packed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So a "build" step is just... packing the suitcase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; That's exactly it. Compile, minify, bundle, strip dev dependencies, whatever your stack needs — all of that is packing. The output is the one thing that travels.&lt;/p&gt;




&lt;h3&gt;
  
  
  Where Docker Fits In
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; You mentioned Docker images as one kind of artifact. Why has that become the default for so many teams?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Worth being precise before I answer that — not every artifact is a Docker image. A compiled Go binary is an artifact. A folder of minified JS is an artifact. Docker is just one &lt;em&gt;format&lt;/em&gt; an artifact can take. But in modern backend systems, it's become the most common one, because it packages both the application and its runtime together — not just code, but the environment that code assumes exists.&lt;/p&gt;

&lt;p&gt;Because a compiled binary or a folder of JS still depends on &lt;em&gt;something&lt;/em&gt; being true about the machine that runs it. Right Node version installed. Right OS libraries present. Right environment variables set up the same way.&lt;/p&gt;

&lt;p&gt;A Docker image packages the artifact &lt;em&gt;and&lt;/em&gt; the environment it needs together — the OS layer, the runtime, your dependencies, your built code, all frozen into one unit. You're not just shipping "here's my app," you're shipping "here's my app, and everything it needs to run, guaranteed identical wherever this image runs."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So it's the suitcase, plus a guarantee that the hotel room on the other end has exactly the outlets and water pressure you packed for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; That's a strange way to put it, but yes. That's genuinely the value. "Works on my machine" mostly stops being a sentence people say, once the machine is defined by the image instead of by whoever's laptop it happened to be built on.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Registry
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Okay, so the build step produces this Docker image. Where does it go? It can't just sit on the runner — that VM gets destroyed right after, you told me that in Episode 2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Exactly the right thing to remember. The image needs somewhere durable to live, the same way the cache needed somewhere durable to live last episode. For Docker images, that's a &lt;strong&gt;registry&lt;/strong&gt; — Docker Hub, Amazon ECR, GitHub Container Registry, and others. A registry's whole job is storing built images and handing them back out by name and version.&lt;/p&gt;

&lt;p&gt;The build step pushes the image to the registry. The deploy step, later — possibly on a completely different runner, possibly hours later — pulls that exact same image back down by its tag and runs it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So the registry is doing for artifacts what the cache did for dependencies — something outside any one runner, that survives after the runner dies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Same underlying shape, different job. You're starting to see the pattern: runners are disposable, everything that needs to survive lives somewhere else.&lt;/p&gt;

&lt;p&gt;Stay with the suitcase for a second, because the analogy actually holds all the way through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;House            →   Suitcase        →   Airport storage   →   Hotel
(your repo)          (the artifact)      (the registry)         (production)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don't ship the house. You pack a suitcase once, check it into storage, and it sits there — unopened — until it's claimed at the other end and unpacked exactly as it was packed.&lt;/p&gt;




&lt;h3&gt;
  
  
  Versioning and Tags
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; How do you know which image is which, once there are hundreds of them in a registry?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Every image gets a &lt;strong&gt;tag&lt;/strong&gt; — usually tied to the commit that built it, something like &lt;code&gt;myapp:a1b2c3d&lt;/code&gt; using the short commit hash, or &lt;code&gt;myapp:v2.4.1&lt;/code&gt; for a release. Never just &lt;code&gt;myapp:latest&lt;/code&gt; for anything that matters in production — "latest" is a moving target, and moving targets are exactly what you don't want when someone asks "what's actually running right now?"&lt;/p&gt;

&lt;p&gt;This connects to something bigger — once an artifact is pushed, treat it as &lt;strong&gt;immutable&lt;/strong&gt;. You don't reach into a pushed image and quietly change something inside it. If you need a change, you build a new artifact with a new tag. The old one still exists, unchanged, exactly as it was — which is the entire point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Is that really that strict in practice, or is it more of a guideline?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Let me tell you why it's strict.&lt;/p&gt;

&lt;p&gt;One Friday — different company, same universal Friday energy — someone deployed &lt;code&gt;myapp:latest&lt;/code&gt;. Standard move, done it a hundred times. Except twenty minutes earlier, a completely unrelated pipeline had already pushed a new image and overwritten what &lt;code&gt;latest&lt;/code&gt; pointed to. Nobody involved in either deploy knew about the other.&lt;/p&gt;

&lt;p&gt;Production broke. Someone said the four words everyone says: "let's roll back."&lt;/p&gt;

&lt;p&gt;So they rolled back — by redeploying &lt;code&gt;myapp:latest&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Which pulled the same broken image. Again. Because &lt;code&gt;latest&lt;/code&gt; was never a specific version. It was just a label sitting on whatever got pushed most recently — and the thing that was "most recent" hadn't changed in the last ten minutes, because nothing new had been pushed since the break. The rollback deployed the exact same broken bytes a second time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So the actual rollback would've needed the &lt;em&gt;previous&lt;/em&gt; tag, not &lt;code&gt;latest&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Exactly — something like &lt;code&gt;myapp:a1b2c3d&lt;/code&gt;, the commit before the break. &lt;code&gt;latest&lt;/code&gt; never points backward. It only ever points at whatever's newest, which during an incident is usually the last thing you want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So if you're already leaning on immutable, versioned tags, that mistake isn't even possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Isn't even possible. That's what "immutable" is actually buying you — not tidiness, a guarantee that "roll back" means something specific and reliable, not a coin flip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So if production breaks, you can look at the tag that's deployed and know precisely which commit it came from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; That's the entire point. It's the same instinct as "what changed recently" from Episode 1 — except now the answer isn't a guess based on recent PRs. It's a tag, sitting right there, pointing at one exact commit.&lt;/p&gt;




&lt;h3&gt;
  
  
  🪞 If I asked you this in an interview
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;"What's the difference between your source code and a build artifact?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source code is the editable instructions for producing an application — including things that never run in production, like tests and dev config. A build artifact is the packaged, versioned output of a build step: a frozen, self-contained unit — a binary, a bundle, or a Docker image — that gets deployed unchanged across every environment. You edit source code and rebuild; you never edit an artifact directly.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Why do teams build an artifact once instead of rebuilding it for each environment?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Rebuilding per environment risks subtle drift — a slightly different dependency version, a different OS patch level, a different build-time environment variable. Building once and promoting the identical artifact through staging and production guarantees that what was tested is exactly what ships, with nothing changing in between.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  🎤 Explain It In One Minute
&lt;/h3&gt;

&lt;p&gt;Imagine explaining this to a teammate — without using the words &lt;em&gt;artifact&lt;/em&gt;, &lt;em&gt;registry&lt;/em&gt;, or &lt;em&gt;Docker&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Why is it a problem if staging and production don't run the exact same build?&lt;/p&gt;

&lt;p&gt;If you can answer that without reaching for the vocabulary, you understand the idea — not just the tooling built around it.&lt;/p&gt;




&lt;h3&gt;
  
  
  🚨 Beginner Mistakes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;"I'll just tag every image &lt;code&gt;latest&lt;/code&gt; and always deploy that."&lt;/strong&gt; Then "what's running in production" stops being answerable. Every rollback, every incident investigation starts one step behind, because nobody can point at a specific version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I'll rebuild the artifact separately for staging and for production."&lt;/strong&gt; Then you're not actually testing what ships — you're testing something &lt;em&gt;similar&lt;/em&gt; to what ships, built at a different time, possibly with different dependency resolutions. Build once. Deploy that same thing everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I'll bake secrets into the Docker image at build time."&lt;/strong&gt; The image gets pushed to a registry and potentially pulled by many things later. Secrets belong in the environment the container runs in, injected at deploy time — never baked into the artifact itself.&lt;/p&gt;




&lt;h3&gt;
  
  
  🏢 Office Reality
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"What's deployed right now?"&lt;/strong&gt; Really asking: which tag, which commit, is currently running. A well-run pipeline answers this in seconds. A badly-run one turns it into an investigation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"The build is broken."&lt;/strong&gt; The build step itself failed — before an artifact was even produced. Different from a failing test; nothing was packaged at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Pin the version."&lt;/strong&gt; Stop depending on a moving tag like &lt;code&gt;latest&lt;/code&gt;; reference an exact, specific version so nothing changes underneath you unexpectedly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"The registry is down."&lt;/strong&gt; Nobody can push new images or pull existing ones. Deploys stall — even if your code and tests are perfectly fine.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  📒 Senior Engineer's Notebook
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;"It passed CI" and "it's deployable" are not the same claim. Passing CI means your source code behaved correctly. Deployable means a specific, versioned artifact exists, sitting in a registry, ready to be pulled and run — unchanged — anywhere you point it.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Whiteboard Moment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source code (repo)
    ↓
Tests pass
    ↓
Build step runs
    ↓
Artifact produced (binary / bundle / Docker image)
    ↓
Tagged with commit hash or version
    ↓
Pushed to registry (durable storage, outside the runner)
    ↓
Runner destroyed — artifact survives
    ↓
Deploy step pulls exact same artifact by tag
    ↓
Same artifact runs in staging, then production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So the runner's job was never really "run my app." It was "produce one trustworthy, labeled package, then hand it somewhere durable and disappear."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; That's the cleanest way anyone's put it back to me. The runner is a factory floor, not a warehouse. It doesn't store anything. It produces, tags, ships, and shuts down.&lt;/p&gt;




&lt;h3&gt;
  
  
  What You Should Be Able to Explain Now
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;(Without looking at Google)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Can you explain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why production doesn't run your repo directly?&lt;/li&gt;
&lt;li&gt;What an artifact actually is, independent of file format?&lt;/li&gt;
&lt;li&gt;Why "build once, deploy everywhere" matters more than it sounds like it should?&lt;/li&gt;
&lt;li&gt;What a Docker image adds on top of a plain build artifact?&lt;/li&gt;
&lt;li&gt;Why a registry exists, and what problem it solves?&lt;/li&gt;
&lt;li&gt;Why tagging with &lt;code&gt;latest&lt;/code&gt; is a trap in production?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If yes — you now understand the handoff between CI and deployment better than most engineers who've shipped to production for years without asking what "artifact" really meant.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Okay. So there's a tagged, versioned image sitting in a registry, ready to go. Someone still has to actually get it running in production, right? That's not automatic just because it exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Right — and that's where it gets political, not just technical. Who's allowed to press that button? What happens to the &lt;em&gt;old&lt;/em&gt; version that's currently serving real users while the new one comes up? What if the new one is broken and real traffic is already hitting it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; That's the deployment part. The part I actually thought CI/CD was, back in Episode 1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; It is. And now you've earned the context to understand why it's not as simple as "run the new image."&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(End of Episode 4)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>programming</category>
      <category>productivity</category>
      <category>cicd</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Episode 3 — The Cache Problem</title>
      <dc:creator>surajrkhonde</dc:creator>
      <pubDate>Mon, 03 Aug 2026 07:33:35 +0000</pubDate>
      <link>https://dev.to/surajrkhonde/episode-3-the-cache-problem-278i</link>
      <guid>https://dev.to/surajrkhonde/episode-3-the-cache-problem-278i</guid>
      <description>&lt;p&gt;&lt;em&gt;Week 2. "Why does my pipeline take eleven minutes to install packages I installed yesterday?"&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Last time you left me on a cliffhanger — &lt;code&gt;npm ci&lt;/code&gt; is about to run. Where do the dependencies actually come from? Does the runner already have them?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Think back to Episode 2. What did we say about hosted runners?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Fresh VM every time. Nothing left over from the last job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; So answer your own question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; ...it downloads everything. Every single time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Every single time. From npm's registry, over the internet, into a brand new empty machine that has never seen your project before.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Eleven Minutes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; That seems insane. I have four hundred packages. That's going to be slow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; How slow, exactly? Don't guess — think about what's actually happening. The runner resolves four hundred package versions, downloads each one from a registry across the internet, and writes thousands of files to disk. On your laptop, with an already-warm npm cache, this might take fifteen seconds. On a brand new VM with nothing cached, anywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So the same install could take eleven minutes in CI and fifteen seconds on my machine?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Easily. And now multiply that by thirty pipeline runs a day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; That's hours of the team just... waiting for packages to download. Every day.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Now you've found the real problem. Not "is CI slow" — pipelines aren't inherently slow. It's specifically: &lt;strong&gt;why do we keep paying the same download cost, over and over, for dependencies that haven't changed?&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  📝 Production Note
&lt;/h3&gt;

&lt;p&gt;Most teams don't notice this cost until their pipeline queue backs up during a busy afternoon. Thirty engineers pushing code, thirty fresh VMs, thirty full dependency downloads — all hitting the npm registry around the same time. The fix isn't a faster internet connection. It's not re-downloading things that didn't change.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why Not Just Keep the Runner Around?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Okay, obvious question — why not just reuse the same VM? Keep &lt;code&gt;node_modules&lt;/code&gt; sitting there between runs?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; You're rediscovering self-hosted runners, actually. That's exactly what a warm self-hosted runner gives you — the machine survives between jobs, so &lt;code&gt;node_modules&lt;/code&gt; could technically survive too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So problem solved, use self-hosted runners?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Two problems with that as a general answer. First — most teams use GitHub-hosted runners, and those die after every job on purpose, for the isolation guarantees we talked about last time. You don't get to keep them warm.&lt;/p&gt;

&lt;p&gt;Second — even on a self-hosted runner, just leaving &lt;code&gt;node_modules&lt;/code&gt; sitting there is risky. What if the lockfile changed since the last run? Now you've got half-old, half-new dependencies on disk, and a bug that only exists because of stale files nobody remembers installing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So you need something that survives between runs, but &lt;em&gt;knows&lt;/em&gt; when it's no longer valid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Now you've built the concept before I've even said the word.&lt;/p&gt;




&lt;h3&gt;
  
  
  What Is a Cache?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; A &lt;strong&gt;cache&lt;/strong&gt;, in CI, is a saved snapshot of something expensive to produce — stored outside the runner, restored into a &lt;em&gt;new&lt;/em&gt; runner if it's still valid.&lt;/p&gt;

&lt;p&gt;Not "keep the VM alive." Save the &lt;em&gt;result&lt;/em&gt; of the expensive step somewhere durable — a storage bucket, basically — and pull it back down at the start of the next run, before you even try to install anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So it's not the runner remembering. It's GitHub remembering, and handing it to whichever runner shows up next.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Exactly. The runner is still dumb and disposable, same as before. The cache lives independently of any one machine.&lt;/p&gt;




&lt;h3&gt;
  
  
  📒 Senior Engineer's Notebook
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;A cache doesn't make your runner smarter. It makes your runner start further ahead. The runner is still starting from zero every time — you're just handing it a head start before it asks for one.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  The Restaurant Analogy
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Can you make this concrete? I'm picturing it fuzzily.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Think of a restaurant kitchen. Every order, in theory, could start from raw ingredients — grind the spices, make the stock, chop everything fresh. Technically correct. Also, nobody's eating in under two hours.&lt;/p&gt;

&lt;p&gt;So kitchens &lt;strong&gt;prep&lt;/strong&gt; — chop vegetables ahead of time, pre-make stock, portion things out. When an order comes in, the cook isn't starting from a raw onion. They're starting from prepped ingredients, and just assembling and cooking the parts that actually change per order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; And the cache is the prep station.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; The cache is the prep station. &lt;code&gt;npm ci&lt;/code&gt; is still going to run — that's the cooking. But it's not starting from a raw onion every time. It's starting from something already prepped, as long as the prep is still valid for this order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; "Still valid for this order" — that's doing a lot of work in that sentence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Good catch. That's the entire hard part of caching. Let's go there.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Cache Key
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; How does GitHub know whether yesterday's cached &lt;code&gt;node_modules&lt;/code&gt; is safe to hand me today? What if I added a package this morning?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; This is the actual engineering problem in caching — not storing things, &lt;em&gt;invalidating&lt;/em&gt; them correctly. Get this wrong and you either waste all your savings, or worse, silently serve stale dependencies.&lt;/p&gt;

&lt;p&gt;The mechanism: a &lt;strong&gt;cache key&lt;/strong&gt;. When you save a cache, you tag it with a string. When you ask for a cache back, you ask for that same string. Match — you get the cache. No match — you get nothing, and install from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So what's the string?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Almost always — a hash of your lockfile. &lt;code&gt;package-lock.json&lt;/code&gt;, hashed into one short string. Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;node-modules-a1b2c3d4e5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Why the lockfile specifically, not the whole &lt;code&gt;package.json&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Because the lockfile is the thing that actually determines what gets installed — exact versions, exact dependency tree. If the lockfile hasn't changed, &lt;code&gt;npm ci&lt;/code&gt; would install the &lt;em&gt;exact same thing&lt;/em&gt; it installed last time. Same input, same output. Nothing to redo.&lt;/p&gt;

&lt;p&gt;If the lockfile &lt;em&gt;has&lt;/em&gt; changed — even one dependency bumped a patch version — the hash changes. New key. No match. Cache miss. Fresh install, exactly like before caching existed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So the cache can never accidentally hand me the wrong dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; That's the whole design goal. The cache key isn't a convenience — it's the safety mechanism. If it's wrong, the cache doesn't just slow you down, it lies to you.&lt;/p&gt;




&lt;h3&gt;
  
  
  Cache Hit vs Cache Miss
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Walk me through both paths.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Every run, before &lt;code&gt;npm ci&lt;/code&gt;, an early step asks: "does a cache exist for this exact key?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache hit&lt;/strong&gt; — the lockfile hash matches something already saved. GitHub downloads the saved cache into the runner. &lt;code&gt;npm ci&lt;/code&gt; still runs, but npm sees the packages are already sitting there and mostly just verifies and links them. Seconds, not minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cache miss&lt;/strong&gt; — no match. Nothing gets restored. &lt;code&gt;npm ci&lt;/code&gt; downloads everything from the registry, same as a runner with no caching at all. Slow, like before. But now, at the &lt;em&gt;end&lt;/em&gt; of the job, the freshly installed dependencies get saved under this new key — so tomorrow, this becomes a hit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So the first run after any dependency change is always slow, no matter what.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Always. There's no way around paying for the very first install of a new dependency set. Caching doesn't eliminate that cost — it stops you from paying it &lt;em&gt;again and again&lt;/em&gt; for something that didn't change.&lt;/p&gt;




&lt;h3&gt;
  
  
  🪞 If I asked you this in an interview
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;"How does dependency caching work in CI, and why is the cache key based on the lockfile?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The pipeline hashes the lockfile into a cache key and checks whether a saved cache exists for that key. If it does — a cache hit — dependencies are restored instead of downloaded fresh, saving significant time. If not — a cache miss — dependencies install normally, and the result is saved under the new key for future runs. The lockfile is used because it fully determines the dependency tree; if it hasn't changed, the previous install is still valid.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  🎤 Explain It In One Minute
&lt;/h3&gt;

&lt;p&gt;Imagine explaining this to a teammate — without using the words &lt;em&gt;cache&lt;/em&gt;, &lt;em&gt;key&lt;/em&gt;, or &lt;em&gt;hash&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Why does your pipeline sometimes install dependencies in ten seconds, and sometimes in ten minutes — for the exact same project?&lt;/p&gt;

&lt;p&gt;If you can answer that in plain language, you actually understand caching. Not just the YAML that enables it.&lt;/p&gt;




&lt;h3&gt;
  
  
  🚨 Beginner Mistakes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;"I'll just cache &lt;code&gt;node_modules&lt;/code&gt; and skip &lt;code&gt;npm ci&lt;/code&gt; entirely on a hit."&lt;/strong&gt; Don't. &lt;code&gt;npm ci&lt;/code&gt; on a cache hit is already fast — it's mostly verifying, not reinstalling. Skipping it outright risks a broken or partial &lt;code&gt;node_modules&lt;/code&gt; from an interrupted previous run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I'll use one cache key for everything, to maximize hits."&lt;/strong&gt; Now you've disabled the safety mechanism. A cache that never invalidates isn't a cache — it's a permanently stale snapshot waiting to bite someone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I'll cache my build secrets alongside dependencies for convenience."&lt;/strong&gt; Caches are stored outside the runner and can, depending on your setup, be restored into other jobs or branches. Secrets don't belong anywhere near a cache.&lt;/p&gt;




&lt;h3&gt;
  
  
  Caching Isn't Free
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; This sounds like a pure win. Why wouldn't every project cache everything?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Because "restore the cache" isn't instant either. Downloading a saved cache from storage takes time too — usually much less than a fresh install, but not zero. For a tiny project with twelve dependencies, the caching overhead might not even be worth it.&lt;/p&gt;

&lt;p&gt;There's also staleness risk. If your cache key isn't specific enough, or someone works around it, you can end up debugging a bug that only exists because of a subtly wrong cached artifact — and that's a genuinely confusing thing to debug, because your code is fine. Your cache is lying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; How would you even notice that's happening?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Usually: "works on a clean install, fails in CI" — and nobody suspects the cache first, because caches are supposed to be invisible plumbing. It's usually the last thing anyone checks. Which is exactly why it's worth understanding now, instead of at 11 PM during an incident.&lt;/p&gt;




&lt;h3&gt;
  
  
  🏢 Office Reality
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Cold cache."&lt;/strong&gt; No cache exists yet for this key — first run after a dependency change, or the very first run ever. Expect it to be slow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Warm the cache."&lt;/strong&gt; Deliberately running a job first — sometimes on a schedule — so the cache exists before someone's actual work needs it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Cache miss on main."&lt;/strong&gt; The lockfile changed and nobody expected it to — often a sign someone updated a dependency without realizing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Bust the cache."&lt;/strong&gt; Deliberately force a cache to be treated as invalid, usually by changing the key — used when a cache is suspected to be corrupted or wrong, and nobody wants to figure out exactly why.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  📒 Senior Engineer's Notebook
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Caching is a bet: the cost of occasionally getting it wrong is smaller than the cost of always starting from zero. Most of the time that bet is correct. The moment you stop understanding your own cache keys is the moment that bet quietly turns against you.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Whiteboard Moment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Runner starts (fresh VM)
    ↓
Hash package-lock.json → cache key
    ↓
Ask: does a cache exist for this key?
    ↓
   ┌─────────────┴─────────────┐
  HIT                          MISS
   ↓                            ↓
Restore cached deps      Download from registry
   ↓                            ↓
npm ci (fast, verifies)   npm ci (slow, installs)
   ↓                            ↓
   └─────────────┬─────────────┘
                  ↓
        Save cache under this key
        (only happens on a miss)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So the diagram I should really remember is just: hash the lockfile, ask if it's been seen before, and only do real work if the answer is no.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; That's caching. Everywhere, not just here. Same idea shows up in browsers, CDNs, databases. You'll meet this pattern for the rest of your career. Today you met it in the smallest, least dramatic place it lives — a &lt;code&gt;node_modules&lt;/code&gt; folder.&lt;/p&gt;




&lt;h3&gt;
  
  
  What You Should Be Able to Explain Now
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;(Without looking at Google)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Can you explain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why a fresh VM has to redownload dependencies every single run?&lt;/li&gt;
&lt;li&gt;Why "just keep the runner alive" isn't a general solution?&lt;/li&gt;
&lt;li&gt;What a cache key actually is, and why it's based on the lockfile?&lt;/li&gt;
&lt;li&gt;The difference between a cache hit and a cache miss — and what happens in each?&lt;/li&gt;
&lt;li&gt;Why a cache miss on the very first run after a dependency change is unavoidable?&lt;/li&gt;
&lt;li&gt;Why an overly broad cache key is more dangerous than no caching at all?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If yes — you understand caching the way it actually gets debugged, not just the way it gets configured.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Okay — so dependencies are handled. Cache hit, &lt;code&gt;npm ci&lt;/code&gt; finishes fast. What happens after that? My code still isn't running anywhere real yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Right. Installed dependencies just means the ingredients are prepped. Nobody's eaten anything yet. Next: how do we turn "code plus dependencies sitting on a runner" into something that can actually be handed off and deployed?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; That's the artifact you mentioned back in Episode 1, isn't it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; It is. Time to find out what that word actually means.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(End of Episode 3)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>productivity</category>
      <category>devops</category>
      <category>computerscience</category>
    </item>
    <item>
      <title>Episode 2 — Who Actually Runs My Pipeline?</title>
      <dc:creator>surajrkhonde</dc:creator>
      <pubDate>Mon, 03 Aug 2026 01:57:39 +0000</pubDate>
      <link>https://dev.to/surajrkhonde/episode-2-who-actually-runs-my-pipeline-aji</link>
      <guid>https://dev.to/surajrkhonde/episode-2-who-actually-runs-my-pipeline-aji</guid>
      <description>&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Last week you left me hanging. I push code, GitHub gets it — then what? Whose computer is actually running my tests?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Honestly, most engineers use this stuff for months without ever asking that question. Good that you did.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Push
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Let's slow it down. You type &lt;code&gt;git push&lt;/code&gt;. What actually leaves your machine?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; The code, I guess? My commits?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; More specific than that. Git doesn't upload your whole project every time — it sends only what's changed, packed into a compact Git-native format. Not a ZIP of your folder. Just the new commits, efficiently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; I've never thought about what the wire format looks like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; And you don't need to, not for this. Git internals are their own series — we'll get there. All you need today is: your commits arrive.&lt;/p&gt;

&lt;p&gt;Point is — your commits arrive. GitHub stores them. The branch pointer updates. Your PR, if there is one, now shows a new commit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; And then a pipeline starts?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; No. Not yet. Something has to tell it to start.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Webhook
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; GitHub doesn't inherently know that your repo cares about pushes. GitHub runs millions of repos. Most of them don't have CI. So after storing your commit, GitHub does one thing: it fires an &lt;strong&gt;event&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Internally, this looks like a notification — "hey, a push event just happened on this repo, on this branch, here are the commit details." GitHub then checks: does anyone care about this event?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Who's "anyone"?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Anyone who registered a &lt;strong&gt;webhook&lt;/strong&gt;. A webhook is just a URL that says "when X happens, send an HTTP POST to this address with the details." External services register webhooks on your repo all the time — Slack bots, monitoring tools, deployment services. And GitHub's own CI system, GitHub Actions, effectively registers one too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So GitHub Actions is... listening through a webhook like everything else?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Conceptually, yes. Under the hood it's more integrated than a third-party webhook — GitHub doesn't literally POST to itself over the internet — but the mental model holds. An event occurs, something is listening, and that something decides what to do next.&lt;/p&gt;




&lt;h3&gt;
  
  
  📝 Production Note
&lt;/h3&gt;

&lt;p&gt;Webhooks are how most modern systems communicate about "something happened." Stripe uses them to tell your server a payment succeeded. Twilio uses them to deliver incoming SMS. GitHub uses them to notify CI systems that a push occurred. The pattern is always the same: event source, HTTP POST, payload with details, receiver decides what to do.&lt;/p&gt;




&lt;h3&gt;
  
  
  🎤 Explain It In One Minute
&lt;/h3&gt;

&lt;p&gt;Imagine you're explaining GitHub Actions to a teammate — without using the words &lt;em&gt;webhook&lt;/em&gt;, &lt;em&gt;runner&lt;/em&gt;, or &lt;em&gt;workflow&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Can you explain what happens after &lt;code&gt;git push&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;If yes — you understand the idea, not just the terminology.&lt;/p&gt;




&lt;h3&gt;
  
  
  Workflow Discovery
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Okay, so the push event fires. What does GitHub Actions do with it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; It does something surprisingly simple. It looks at your repo.&lt;/p&gt;

&lt;p&gt;Specifically, it checks for a directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.github/workflows/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that directory doesn't exist — nothing happens. Your push lands, your branch updates, and that's the end of it. No pipeline. No runner. No tests. Silence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; What if the directory exists but is empty?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Same thing. GitHub looks for workflow files inside that directory — YAML files, specifically — and checks whether any of them are configured to trigger on a push event. If none match, silence again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So the workflow file is what actually connects the push to the pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Exactly. The event is just noise until a workflow file says "I care about this."&lt;/p&gt;




&lt;h3&gt;
  
  
  What Is a Workflow?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Before we go further — what actually is a workflow, conceptually? Not the YAML syntax, but the idea.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; A workflow is a description of: "when this event happens, run these steps, in this order, on this type of machine."&lt;/p&gt;

&lt;p&gt;That's it. Three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Trigger&lt;/strong&gt; — what event starts it? A push? A PR? A manual button click? A schedule?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jobs&lt;/strong&gt; — logical groupings of steps. A "test" job, a "build" job, a "deploy" job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Steps&lt;/strong&gt; — the actual commands. &lt;code&gt;npm ci&lt;/code&gt;, &lt;code&gt;npm test&lt;/code&gt;, &lt;code&gt;docker build&lt;/code&gt;, whatever your project needs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; And all of that lives in one YAML file?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; One YAML file per workflow. A repo can have many workflows — one for PRs, one for deploys, one for nightly cleanup, whatever you need. Each is independent.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why YAML?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Why YAML, though? Why not just a JavaScript file or a Python script that says what to run?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Few reasons, and they're all about trust and constraint.&lt;/p&gt;

&lt;p&gt;First — a JS or Python file is executable. It can do anything. Delete files, make network requests, mine crypto. You're handing GitHub a script and saying "run this on your machines." That's a hard sell at GitHub's scale — millions of repos, many of them public, many of them from strangers.&lt;/p&gt;

&lt;p&gt;Second — YAML is declarative. You describe what you want, not how to do it. "Run these steps in this order on this machine." The CI system decides how to interpret and execute that. You can't write a &lt;code&gt;while(true)&lt;/code&gt; loop in workflow YAML. You can't open a socket. The format itself constrains what's possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; But YAML can get complex.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; It can, and people complain about it constantly. But the alternative — "just let anyone run arbitrary code" — is worse. YAML is the compromise between "expressive enough to define a pipeline" and "restricted enough that GitHub can safely run it on shared infrastructure."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; You said "shared infrastructure." Is that where runners come in?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Almost — but one thing first, so you don't walk away with the wrong idea. YAML isn't what makes GitHub secure. GitHub still executes real commands — &lt;code&gt;npm test&lt;/code&gt;, &lt;code&gt;docker build&lt;/code&gt;, whatever your steps say. YAML just gives you a structured, constrained way to describe the workflow. The actual security comes later — from isolated runners, permissions, and execution controls. Don't confuse the format with the safety mechanism.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why Events Trigger Workflows
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Why not just run the workflow on every push to any branch? Why does the trigger matter?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Because you don't always want it to run.&lt;/p&gt;

&lt;p&gt;Your &lt;code&gt;main&lt;/code&gt; branch — yes, run everything. Tests, builds, maybe a deploy.&lt;/p&gt;

&lt;p&gt;Someone's feature branch named &lt;code&gt;experiment/delete-everything&lt;/code&gt; — maybe run tests, but absolutely do not deploy.&lt;/p&gt;

&lt;p&gt;A branch named &lt;code&gt;dependabot/chore-update-lodash&lt;/code&gt; — run tests, sure, but do you really need a full build and deploy preview for a patch version bump of a utility library?&lt;/p&gt;

&lt;p&gt;The trigger lets you say: "this workflow runs on pushes to &lt;code&gt;main&lt;/code&gt; only" or "this workflow runs on pull requests but not on pushes to branches." It's about matching effort to risk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; What happens if someone pushes to a branch and no workflow triggers?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Nothing. That's the correct behavior. Not every code change needs a pipeline.&lt;/p&gt;




&lt;h3&gt;
  
  
  🪞 If I asked you this in an interview
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;"What actually happens when you push code to a GitHub repo with GitHub Actions configured?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Git sends a pack file with the new commits to GitHub. GitHub stores them and fires a push event. GitHub Actions checks &lt;code&gt;.github/workflows/&lt;/code&gt; for workflow files that match that event. If it finds one, it schedules the workflow. If not, nothing happens — the push lands with no pipeline.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  The Runner Problem
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Okay. So the workflow matches. GitHub knows what to run. Now — whose machine actually executes it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Now we hit the real question. And it's one that surprises people.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub does not run your code on its own servers.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Wait — really?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Really. Think about it from GitHub's perspective. Your workflow says &lt;code&gt;npm install&lt;/code&gt;. Then &lt;code&gt;npm test&lt;/code&gt;. Your tests might spin up a database. They might download gigabytes of dependencies. They might run for forty-five minutes. They might have a memory leak that hogs CPU.&lt;/p&gt;

&lt;p&gt;Now multiply that by every push to every repo on GitHub, happening every second, all day, forever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; That would be absurdly expensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; It would also be a security nightmare. Your code could try to read other repos' data, probe internal network addresses, exploit the host kernel. Running arbitrary user code on shared servers is one of the hardest problems in computing. GitHub's core product is storing your code safely — not executing it unpredictably.&lt;/p&gt;

&lt;p&gt;So instead, GitHub does something smarter.&lt;/p&gt;




&lt;h3&gt;
  
  
  What Is a Runner?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; A &lt;strong&gt;runner&lt;/strong&gt; is a machine — a real or virtual computer — that exists to do one thing: receive workflow instructions from GitHub, check out your code, and execute your steps.&lt;/p&gt;

&lt;p&gt;It's not GitHub's server in the traditional sense. It's a separate machine, running a piece of software called the &lt;strong&gt;runner agent&lt;/strong&gt;, that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Listens for jobs assigned to it by GitHub&lt;/li&gt;
&lt;li&gt;Creates an isolated environment for each job&lt;/li&gt;
&lt;li&gt;Checks out your repo's code into that environment&lt;/li&gt;
&lt;li&gt;Runs each step in order&lt;/li&gt;
&lt;li&gt;Reports results — pass or fail — back to GitHub&lt;/li&gt;
&lt;li&gt;Cleans up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So the runner is basically a worker that does what the workflow tells it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Exactly. GitHub is the coordinator. The runner is the executor. Your workflow YAML is the instructions.&lt;/p&gt;




&lt;h3&gt;
  
  
  📒 Senior Engineer's Notebook
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;The runner doesn't think. It doesn't decide what to run. It receives a job definition from GitHub and executes it step by step, reporting each step's result. All the intelligence lives in GitHub's scheduling layer — the runner is muscles, not brain.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Hosted Runners
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Where does the runner machine come from?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Two options. First: &lt;strong&gt;GitHub-hosted runners&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;GitHub maintains a pool of virtual machines — thousands of them — across AWS and Azure. When your workflow triggers, GitHub picks an available machine from that pool, starts a fresh VM for your job, runs the runner agent on it, hands it your workflow, and tears the VM down when it's done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Fresh every time?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Fresh every time. You never get a machine that someone else just used. No leftover files, no cached state from a stranger's repo, no chance of cross-contamination. The VM is born, it runs your job, and it dies.&lt;/p&gt;

&lt;p&gt;Think of it like a hotel. Every guest gets a separate room. When they check out, housekeeping cleans it before the next guest ever sees it. You never inherit the last guest's mess — and the next guest never sees yours. Hosted runners work the same way, one job per "room," cleaned before the next one checks in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; That must be expensive for GitHub.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; It is. That's why GitHub puts limits on how many minutes you can use per month on the free tier, and charges for more. Every minute your workflow runs is a minute of a real VM costing real money.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; What does the machine look like? What's on it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; GitHub offers several runner images — pre-configured VM templates with different tools pre-installed. There's one for Ubuntu Linux with Node.js, Python, Java, Docker, and a bunch of CLI tools. There's one for macOS. There's one for Windows. You pick which one your job needs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So if I need Go installed, I either pick an image that has it or install it myself in a step?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Yes. The runner image is a starting point, not a straitjacket. Most real workflows have early steps that install anything the image is missing.&lt;/p&gt;




&lt;h3&gt;
  
  
  Self-Hosted Runners
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; You said there were two options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Second option: &lt;strong&gt;self-hosted runners&lt;/strong&gt;. You provide the machine. Your company buys a server, or spins up a VM in your own AWS account, installs the runner agent software on it, and registers it with GitHub. Now when a workflow triggers, GitHub can assign jobs to your machine instead of its own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Why would a company do that instead of using GitHub's?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Several reasons. Let me walk through the real ones, not the theoretical ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost at scale.&lt;/strong&gt; If you're running five hundred workflows a day, each taking ten minutes, that's eighty-three hundred minutes per day. GitHub charges for that. A dedicated server might be cheaper at that volume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom tooling.&lt;/strong&gt; Your company uses an internal VPN, a proprietary database, a custom compiler, an on-premises artifact registry that isn't accessible from the public internet. A GitHub-hosted runner can't reach any of that. Your machine, on your network, can.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compliance.&lt;/strong&gt; Remember the bank from last week? They can't run code on machines they don't control. Regulatory requirements might mandate that all execution happens on infrastructure the company owns and audits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed.&lt;/strong&gt; If your repo is enormous — think monorepo with gigabytes of history — just checking out the code takes time. A self-hosted runner with a local cache or a fast network connection to your Git server can shave minutes off every run.&lt;/p&gt;




&lt;h3&gt;
  
  
  🚨 Beginner Mistakes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;"Self-hosted runners are always better."&lt;/strong&gt; They're not. You now own patching, security updates, disk space, and availability. If your self-hosted runner goes down at 2 AM, your pipeline is broken until you wake up and fix it. GitHub-hosted runners have that problem instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I'll put a self-hosted runner on my personal laptop."&lt;/strong&gt; Technically possible. Terrible idea. Your laptop goes to sleep, your pipeline breaks. Your laptop joins a coffee shop WiFi, your pipeline is now running on a public network with your repo's code on disk. Don't.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why Runners Exist — The Real Reason
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; But why the runner concept at all? Why not just have GitHub SSH into a server and run commands directly?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Because that's what the developer in our Friday story did, and we spent an hour talking about why it was a problem.&lt;/p&gt;

&lt;p&gt;No audit trail. No isolation. No consistency between runs. No way to reproduce what happened. A runner fixes all of that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every step is logged&lt;/strong&gt; — GitHub receives a status update for each step as it runs. You can see exactly what command ran, what it output, and how long it took.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The environment is defined&lt;/strong&gt; — you specify the OS, the tools, the environment variables. Same workflow, same machine template, same result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolation is enforced&lt;/strong&gt; — your job can't see other jobs. It can't access the host system. It runs in a constrained environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lifecycle is managed&lt;/strong&gt; — the machine is created for your job and destroyed after. No leftover state, no drift.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The runner is the answer to "how do we run arbitrary code safely, repeatably, and observably?"&lt;/p&gt;




&lt;h3&gt;
  
  
  Runner Lifecycle
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Can you walk through the actual sequence? From trigger to first command?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Sure. Here's what happens, step by step, the moment your workflow matches a push event:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Queue.&lt;/strong&gt; GitHub places your workflow job in a queue. If you have three jobs and only two runners available, one waits.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Assignment.&lt;/strong&gt; A runner picks up the job. If it's GitHub-hosted, a fresh VM starts first. If it's self-hosted, the runner agent signals "I'm ready" and receives the job.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Environment setup.&lt;/strong&gt; The runner creates an isolated working directory. It sets up environment variables — including ones GitHub injects automatically, like &lt;code&gt;GITHUB_SHA&lt;/code&gt; for your commit hash and &lt;code&gt;GITHUB_REF&lt;/code&gt; for your branch name.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Checkout.&lt;/strong&gt; Usually the first real step in any workflow — the runner pulls your code from GitHub into its working directory. There's a standard action called &lt;code&gt;actions/checkout&lt;/code&gt; that almost every workflow uses as its first step.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Step execution.&lt;/strong&gt; The runner walks through each step in your workflow. For each one, it starts a shell — bash on Linux, PowerShell on Windows — runs the command, captures the output and exit code, and reports back to GitHub.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Result.&lt;/strong&gt; Every step either passes or fails. If any step fails, the job stops and is marked failed. If all steps pass, the job is marked passed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Teardown.&lt;/strong&gt; The runner reports final status to GitHub. On GitHub-hosted runners, the VM is destroyed. On self-hosted runners, the working directory is cleaned and the agent waits for the next job.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Security Isolation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; You mentioned isolation. What does that actually mean in practice?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; On GitHub-hosted runners, each job gets its own VM. Not just its own directory — its own virtual machine. Separate kernel, separate filesystem, separate network namespace. Your job literally cannot see another job running on the same physical host, because there is no shared anything at the VM level.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; And on self-hosted?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; That's where it gets interesting — and where companies get cautious.&lt;/p&gt;

&lt;p&gt;On a self-hosted runner, jobs run as a process on a machine you control. The runner agent does apply some isolation — each job gets its own working directory and environment — but it's not a VM boundary. Two jobs on the same self-hosted runner share the same kernel and the same filesystem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So one job could theoretically read another job's files?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; If someone wrote a step that did that, yes. Which is why most companies that take this seriously use one runner per job — spin up a fresh VM or container for each workflow run, run the agent inside it, destroy it after. You lose the speed advantage of reusing a warm machine, but you gain real isolation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; What about environment variables? If one workflow sets a secret, can another workflow on the same runner see it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; GitHub injects secrets differently than regular environment variables — they're masked in logs and scoped to the specific workflow run. But the fundamental trust boundary on self-hosted runners is your infrastructure, not GitHub's. If you don't trust the code running on your runner, you have a bigger problem than environment variable leaking.&lt;/p&gt;




&lt;h3&gt;
  
  
  🏢 Office Reality
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"The runner is flaky."&lt;/strong&gt; Means jobs on that runner fail intermittently — maybe the machine is underpowered, maybe the disk is full, maybe there's a network issue between the runner and GitHub. Not your code's fault, but your pipeline's problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"We need more runners."&lt;/strong&gt; Means the queue is backing up. Jobs are waiting too long for an available machine. Common during peak hours when everyone's pushing code before standup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"That runner is pinned to the main branch."&lt;/strong&gt; Means that specific runner only accepts jobs from workflows triggered on &lt;code&gt;main&lt;/code&gt;. Other branches use different runners — maybe cheaper or less powerful ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"The runner image is stale."&lt;/strong&gt; The pre-configured template hasn't been updated in months. It's missing security patches or new tool versions. Someone needs to rebuild it.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Startup vs Enterprise Runners
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; How does this look different at a five-person startup versus a bank?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; At the startup — GitHub-hosted runners, default Ubuntu image, maybe one self-hosted runner for a special internal tool. Nobody's thought about it much. It works until it doesn't.&lt;/p&gt;

&lt;p&gt;At the bank — self-hosted runners on infrastructure they control, inside a VPN, with custom runner images built and audited by a security team. Each image is versioned and tagged. There's a pipeline that builds the runner images. Yes — a pipeline that builds the machines that run the pipelines. That layer of meta is where enterprises live.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; A pipeline for the pipeline?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; You'll see that pattern more than you'd expect. Companies automate everything — including the automation itself.&lt;/p&gt;




&lt;h3&gt;
  
  
  🪞 If I asked you this in an interview
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;"What's the difference between a GitHub-hosted runner and a self-hosted runner?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;GitHub-hosted runners are temporary VMs managed by GitHub — fresh for each job, pre-configured with common tools, but on infrastructure you don't control. Self-hosted runners are machines you provide — they can access your internal network and custom tooling, but you're responsible for maintaining, securing, and patching them.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Why doesn't GitHub just run pipeline code on its own servers?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Because running arbitrary user code at scale is expensive and dangerous. User code could consume excessive resources, access other repos' data, or exploit the host. Runners provide isolation — each job gets its own environment — and GitHub can control, meter, and tear down that environment when the job finishes.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Whiteboard Moment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git push
    ↓
GitHub receives pack file, stores commits
    ↓
Push event fires
    ↓
GitHub checks .github/workflows/ for matching workflows
    ↓
Match found → job queued
    ↓
Runner picks up job (VM starts if hosted)
    ↓
Runner sets up environment, injects variables
    ↓
actions/checkout pulls your code
    ↓
Runner executes each step, reports results
    ↓
Job passes/fails → VM destroyed (hosted) or cleaned (self-hosted)
    ↓
GitHub updates workflow status in your repo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same journey, stripped to its bones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer
    ↓
Git Push
    ↓
GitHub
    ↓
Workflow Parser
    ↓
Queue
    ↓
Runner
    ↓
Shell
    ↓
npm ci
    ↓
npm test
    ↓
Result
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Okay. So the runner has my code. It has a fresh environment. It's ready to go. But — how does it know what commands to run? The workflow YAML is sitting on GitHub, not on the runner.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Is it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; ...it's not? The runner already has it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Remember step four. &lt;code&gt;actions/checkout&lt;/code&gt;. What do you think it checked out?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; My code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Your code — which lives in a repo that contains &lt;code&gt;.github/workflows/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So the workflow file is part of the repo that gets checked out onto the runner?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Exactly. But here's the thing — the runner doesn't read the YAML from the checkout. It already received the job definition &lt;em&gt;before&lt;/em&gt; checkout happened. GitHub parsed the workflow, broke it into steps, and sent the runner a structured job payload. The YAML is instructions for GitHub. The runner receives &lt;em&gt;executed&lt;/em&gt; instructions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; What's the difference?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; The difference is where the intelligence lives. The runner doesn't interpret YAML. It receives "run step 1: &lt;code&gt;npm ci&lt;/code&gt;, run step 2: &lt;code&gt;npm test&lt;/code&gt;" as a list of commands. The YAML parsing, the variable substitution, the conditional logic — all of that happened on GitHub's side before the runner ever saw it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So the runner is dumber than I thought.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; The runner is &lt;em&gt;designed&lt;/em&gt; to be dumb. Dumb is secure. Dumb is predictable. The runner is a shell executor. It receives a command, runs it, reports the result. That's the job.&lt;/p&gt;




&lt;h3&gt;
  
  
  What You Should Be Able to Explain Now
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;(Without looking at Google)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Can you explain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What actually leaves your machine when you run &lt;code&gt;git push&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;What a webhook is and why it matters for CI?&lt;/li&gt;
&lt;li&gt;How GitHub discovers whether a repo has any workflows?&lt;/li&gt;
&lt;li&gt;What a runner is, in plain language?&lt;/li&gt;
&lt;li&gt;Why GitHub doesn't execute your code directly on its own servers?&lt;/li&gt;
&lt;li&gt;The difference between hosted and self-hosted runners — and when you'd pick each?&lt;/li&gt;
&lt;li&gt;What "isolation" means for a runner, and why it matters?&lt;/li&gt;
&lt;li&gt;Why the runner doesn't parse your YAML file itself?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If yes — you now understand more about CI infrastructure than most engineers who've been using GitHub Actions for a year.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; The runner received its instructions. It's about to run &lt;code&gt;npm ci&lt;/code&gt; as its first real command. But wait — where do the dependencies come from? Does the runner already have them? Does it download them every time?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Now you're asking the right question. And the answer is where pipelines either get fast or get painful.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(End of Episode 2)&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>software</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Episode 1 — The Friday Deploy</title>
      <dc:creator>surajrkhonde</dc:creator>
      <pubDate>Sun, 02 Aug 2026 04:23:46 +0000</pubDate>
      <link>https://dev.to/surajrkhonde/episode-1-the-friday-deploy-2424</link>
      <guid>https://dev.to/surajrkhonde/episode-1-the-friday-deploy-2424</guid>
      <description>&lt;h2&gt;
  
  
  &lt;em&gt;Week 1. "Today you'll shadow me during a deployment."&lt;/em&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What You'll Learn
&lt;/h3&gt;

&lt;p&gt;By the end of this series, you'll be able to explain confidently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What actually happens after &lt;code&gt;git push&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;How GitHub Actions, Jenkins, GitLab CI, and other CI systems work conceptually&lt;/li&gt;
&lt;li&gt;Why runners exist and how they execute your code&lt;/li&gt;
&lt;li&gt;How production deployments differ from local deployments&lt;/li&gt;
&lt;li&gt;Why companies use artifacts, Docker images, registries, and deployment strategies&lt;/li&gt;
&lt;li&gt;How secrets, SSH keys, and environments are managed safely&lt;/li&gt;
&lt;li&gt;Why pipelines fail, how engineers debug them, and how rollbacks work&lt;/li&gt;
&lt;li&gt;The tradeoffs startups and large companies make in their CI/CD workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This series isn't about memorizing YAML syntax. It's about living through the production systems behind modern software delivery — one incident, one question, one term at a time.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Today you'll shadow me. Nothing to do yet — just watch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; I merged my first PR yesterday. It got approved, checks passed. How does it actually reach production from here?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Let me answer that with a story instead of a diagram.&lt;/p&gt;

&lt;p&gt;Three years ago, at a company I worked at — Friday evening, 6:45 PM. Everyone's packed up. One developer, still at his desk, says, "just one small fix, two minutes." SSHs straight into the production server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Wait — he SSHs &lt;em&gt;into production&lt;/em&gt;? I thought developers usually weren't allowed to touch that directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Good catch. Depends entirely on where you work.&lt;/p&gt;




&lt;h3&gt;
  
  
  🏙️ Startup vs Big Tech — Who Gets to Touch Production
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Small Startup&lt;/strong&gt; — Founder or one of the first few engineers deploys manually. SSH key lives on someone's laptop. Single VM. No process, because nobody's had time to build one yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Growing Startup&lt;/strong&gt; — GitHub Actions shows up. A Docker container or two. One staging server before production. SSH access still exists, but it's shrinking.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product Company&lt;/strong&gt; — Self-hosted runners, multiple environments (dev, staging, prod), an artifact registry, blue-green deployments, and someone has to approve before anything reaches real users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bank / Fintech&lt;/strong&gt; — No SSH to production, full stop. Every deployment is logged and audited. Change approvals required. Secrets are tightly restricted. Compliance isn't optional — it's the job.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; This story is from the first category. That matters — it's part of why what happens next was even possible.&lt;/p&gt;




&lt;h3&gt;
  
  
  📝 Production Note
&lt;/h3&gt;

&lt;p&gt;Most companies never run production deployments directly from a developer's laptop — even when someone technically still has SSH access. Mature teams prefer deployments initiated by a pipeline, because every action a pipeline takes becomes logged, repeatable, and auditable. A human typing commands from memory is none of those things.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; He runs three commands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git pull
npm &lt;span class="nb"&gt;install
&lt;/span&gt;pm2 restart app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Why PM2? Why not just &lt;code&gt;node app.js&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Because &lt;code&gt;node app.js&lt;/code&gt; dies the moment the process crashes, or the terminal closes, or the server reboots. PM2 is a process manager — it keeps your app running, restarts it automatically if it crashes, and lets you do things like &lt;code&gt;pm2 restart app&lt;/code&gt; without killing and re-launching by hand. Full conversation on process managers, later. For now — it's the thing making sure your app doesn't quietly die at 3 AM with nobody watching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; And &lt;code&gt;npm install&lt;/code&gt; — I've also seen &lt;code&gt;npm ci&lt;/code&gt;. What's the difference?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; &lt;code&gt;npm install&lt;/code&gt; can update your lockfile and pull slightly different versions if something upstream changed. &lt;code&gt;npm ci&lt;/code&gt; installs &lt;em&gt;exactly&lt;/em&gt; what the lockfile says, nothing more, nothing less — and it's faster, since it skips the resolution step. In production, you almost always want &lt;code&gt;npm ci&lt;/code&gt;. This developer didn't know that yet either.&lt;/p&gt;

&lt;p&gt;He runs it. Checks the homepage. Loads fine. Shuts his laptop. Leaves for the weekend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; That's... basically what I assumed deployment &lt;em&gt;was&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; That's how thousands of companies started. It's also, almost word for word, why thousands of companies eventually stopped doing it that way.&lt;/p&gt;

&lt;p&gt;7:02 PM. PagerDuty goes off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Sorry — what's PagerDuty?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Fair question, you've probably never had to use one.&lt;/p&gt;

&lt;p&gt;Imagine production breaks at 2 AM. Who wakes up? Does AWS just know to call someone? Does Slack ring your phone?&lt;/p&gt;

&lt;p&gt;No. Companies use an incident management platform — PagerDuty is one of the most common, there's also Opsgenie, VictorOps. When monitoring detects something serious — payment failures, API errors, CPU spikes, a service going unreachable — it doesn't send an email. It calls whoever's on-call. Loudly, on purpose. A sleeping engineer doesn't read email at 2 AM, but they'll pick up a phone that won't stop ringing.&lt;/p&gt;

&lt;p&gt;Someone opens the incident Slack channel — already moving. Support is typing "getting calls about failed payments, is something down??"&lt;/p&gt;




&lt;h3&gt;
  
  
  🪞 If I asked you this in an interview
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;"Why do companies use incident platforms like PagerDuty instead of just emailing the on-call engineer?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Because production failures need immediate attention. Email is asynchronous and easy to miss for hours — an on-call system escalates repeatedly, through calls and pages, until someone actually acknowledges it.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Someone opens GitHub.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Why GitHub? The problem's happening in production, not in the repo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Good question — and it's the first question every experienced engineer asks in an incident.&lt;/p&gt;

&lt;p&gt;Production was fine ten minutes ago. It isn't now. So before anyone blames the database, or the cloud provider, or Kubernetes, the first thing to check is always: &lt;em&gt;what changed?&lt;/em&gt; Did somebody deploy? Which PRs merged? Which commit actually reached production? Who approved it?&lt;/p&gt;




&lt;h3&gt;
  
  
  📒 Senior Engineer's Notebook
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;The first question during an outage is rarely "who wrote the bug?" It's almost always "what changed recently?"&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; What happens while they're checking that?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; In parallel — someone starts tailing logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; What does "tailing logs" actually mean? I've heard the phrase, never really understood it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Worth stopping on, you'll do this constantly once you're on-call.&lt;/p&gt;

&lt;p&gt;Simplest version — on a single server, there's a literal command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; app.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tail&lt;/code&gt; shows the end of a file. &lt;code&gt;-f&lt;/code&gt; means "follow" — keep showing new lines live, instead of a one-time snapshot. So instead of scrolling a huge file, you watch it happen in real time.&lt;/p&gt;

&lt;p&gt;That works with one server. It falls apart the moment you have twenty, each writing its own log, and the failed request could've hit any one of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So what do bigger companies use?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Centralized logging — the app ships every log line somewhere central instead of leaving it on the machine that wrote it. Common names: &lt;strong&gt;CloudWatch&lt;/strong&gt; (AWS's native option), the &lt;strong&gt;ELK stack&lt;/strong&gt; — Elasticsearch, Logstash, Kibana — &lt;strong&gt;Datadog&lt;/strong&gt;, and &lt;strong&gt;Loki&lt;/strong&gt;, Grafana's logging system. Different products, same idea: collect logs from everywhere, put them somewhere searchable.&lt;/p&gt;

&lt;p&gt;Two things matter once you're searching. First — &lt;strong&gt;structured logs&lt;/strong&gt;. Instead of a line that just says &lt;code&gt;payment failed for user&lt;/code&gt;, a structured log looks more like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"payment_failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"8213"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"orderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A991"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you filter by field instead of grepping sentences and hoping the wording matches.&lt;/p&gt;

&lt;p&gt;Second — &lt;strong&gt;correlation IDs&lt;/strong&gt;. One request might touch five services on its way through — gateway, payment service, inventory service, and so on. A correlation ID is one unique value attached at the very start, logged by every service it passes through. Search that one ID, and you see the entire journey of that request across every system — instead of five unrelated log streams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; That's a lot more than "check the logs" sounded like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; It always is, once you actually need it under pressure. Back to Friday — someone's tailing logs, someone else pulls up metrics, and the room is still just asking: &lt;em&gt;did anyone deploy recently?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Nobody knows yet. That's the part people never picture. For the first several minutes of a real incident, nobody knows anything. Just noise, and a clock running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; But he only changed one line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; I've sat through incidents shaped almost exactly like this more than once. Different companies, different bugs, same shape. Someone saves five minutes during a deploy, spends the next hour trying to earn it back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Couldn't they just undo it? Roll back?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Depends. Can you always?&lt;/p&gt;

&lt;p&gt;What if that deploy already ran a database migration — already changed a column, already wrote data in a new shape? Rolling back &lt;em&gt;code&lt;/em&gt; is one git command. Rolling back everything the code already &lt;em&gt;did&lt;/em&gt; to your data is a different problem, and it doesn't have a one-line answer. Deserves its own full conversation — we'll get there.&lt;/p&gt;




&lt;h3&gt;
  
  
  🚨 Beginner Mistakes
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;"I'll just SSH and fix production directly."&lt;/strong&gt; Even if it's faster tonight, it creates configuration drift — the running server no longer matches what's in the repo, and nobody else knows what actually changed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I passed tests locally."&lt;/strong&gt; That tells you your machine is fine. It says nothing about production's environment, data, or scale.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"I'll use &lt;code&gt;npm install&lt;/code&gt; in CI."&lt;/strong&gt; Now your build isn't reproducible — two runs of the same commit could quietly pull different dependency versions.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; That Friday, they found the line, reverted it, restarted the service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So — they just fixed the bug?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Fixing it took eight minutes.&lt;/p&gt;

&lt;p&gt;Finding it took fifty-two.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; That's a huge gap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; The code is rarely the expensive part. The investigation is. Five people, staring at dashboards and logs, figuring out which of that day's changes actually mattered.&lt;/p&gt;




&lt;h3&gt;
  
  
  🏢 Office Reality
&lt;/h3&gt;

&lt;p&gt;You'll hear phrases like these constantly once you're in these conversations for real — worth decoding early, even before you've met all of them properly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"The deployment is green."&lt;/strong&gt; Means the deploy process finished without errors. Does &lt;strong&gt;not&lt;/strong&gt; mean production is healthy — those are separate claims, and this Friday is the perfect proof.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"The pipeline is red."&lt;/strong&gt; A step failed — could be a test, a build, a lint check. Doesn't yet tell you which.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"The runner is stuck."&lt;/strong&gt; The machine executing your pipeline steps hasn't responded — we haven't met runners properly yet, but we will.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Promote staging to production."&lt;/strong&gt; Take a build that's already been running safely in a test-like environment, and push that exact same build to real users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"The artifact is corrupted."&lt;/strong&gt; The packaged, built version of your app — the thing the pipeline produces and hands off to deployment — got damaged or built incorrectly. More on artifacts soon.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Whiteboard moment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Production
    ↓
Deployment
    ↓
Monitoring
    ↓
Incident
    ↓
Rollback
    ↓
Postmortem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything in this series lives somewhere on this line. Today we've walked from Deployment into Incident. Rollback and Postmortem are still ahead of us.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why Not Just Hire More People?
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Why do you think companies eventually stopped deploying by hand like this?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Because... it's slow?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Partly. What else?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; People make mistakes doing repetitive stuff manually?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Exactly — now let's go one layer deeper than "mistakes happen," because that's not quite the real reason either.&lt;/p&gt;

&lt;p&gt;Think about what it'd take to never have that fifty-two-minute scramble again. Suppose the company says: fine, before anything reaches production, someone runs through this, no exceptions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;□ Pull latest code
□ Install dependencies (npm ci, not npm install)
□ Build
□ Run unit tests
□ Run integration tests
□ Run database migration
□ Restart services
□ Verify health endpoint
□ Check metrics
□ Notify support
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten steps. None hard. A junior engineer could run this correctly on day one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So just... follow the list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; I used to think I could hold something like this in my head — careful, attentive, no checklist needed. Then one Friday, I forgot to run a database migration before restarting the service. I never trusted memory again after that. Mine or anyone else's.&lt;/p&gt;

&lt;p&gt;Now imagine running that list correctly. Thirty times a day. Every day. For years. Never tired, never rushing because it's 6:45 on a Friday.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Nobody does that forever without slipping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Right. The problem was never intelligence — it's repetition. Some companies tried hiring someone whose whole job is running this checklist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Did that work?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; For a while. Then someone asked — if the entire job is "ten fixed steps, same order, zero judgment required" — is that really work for a person?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; ...that sounds like something a machine should do.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; So that's what got built — something that runs the exact same checklist every time. No memory to trust. No "it worked yesterday."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Does it have a name?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; It does. We'll live inside it for several conversations. Most engineers call it a &lt;strong&gt;pipeline&lt;/strong&gt; — GitHub Actions, GitLab CI, Jenkins, depending where you work. Name changes, idea doesn't.&lt;/p&gt;




&lt;h3&gt;
  
  
  🪞 If I asked you this in an interview
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;"Why don't manual production deployments scale, even with careful, experienced engineers?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Because the failure mode isn't a lack of skill — it's repetition. The same fixed checklist run correctly a thousand times eventually gets run incorrectly once, and at 30 deploys a day, "eventually" arrives fast. Automating the checklist removes reliance on memory and attention, not intelligence.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  🎯 Interview Perspective
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Interviewer:&lt;/strong&gt; Why do companies use CI/CD?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weak answer:&lt;/strong&gt; Because it automates deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strong answer:&lt;/strong&gt; CI/CD standardizes repetitive deployment steps so every deployment follows the same validated process. It reduces human error, guarantees consistent environments between runs, creates an audit trail of exactly what shipped and when, and lets teams deploy frequently &lt;em&gt;because&lt;/em&gt; they trust the process — not despite it.&lt;/p&gt;




&lt;h3&gt;
  
  
  "My pipeline passed. Can I deploy?"
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; If the checklist runs every time, doesn't that slow people down for tiny fixes?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Check the math on Friday instead of guessing. Skipping the checklist saved maybe four minutes. The fix plus the investigation together cost sixty. So the real question was never "was the fix small" — it's what four minutes of skipped checking actually cost, once you count what came after.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Fifty-six extra minutes. Plus every payment that failed in that window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; That trade never looks expensive in the moment. Only in the postmortem, once someone adds it up.&lt;/p&gt;

&lt;p&gt;Here's a version of this question I get from almost every junior engineer eventually — "my pipeline passed, can I deploy?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; I mean... that was going to be my next question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; You're asking the wrong question, gently. A passing pipeline doesn't mean production is safe. It means the tests &lt;em&gt;you chose to write&lt;/em&gt; happened to pass. Different sentences entirely.&lt;/p&gt;




&lt;h3&gt;
  
  
  📒 Senior Engineer's Notebook
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;A deployment isn't finished when the pipeline turns green. It's finished when users are successfully using the feature.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Three Changes, One Broken Night
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Here's a harder one — the kind with no obvious villain.&lt;/p&gt;

&lt;p&gt;Same day, same repo, three developers. A updates the payment library. B rotates an environment variable. C upgrades the base Docker image. Each opens a PR. Each pipeline, individually, goes green. All three merge that afternoon. That evening, production breaks. Which one caused it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; The payment library. A.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; The env variable, then — B. Feels like the sneaky one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; ...the Docker image, C?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Also wrong, on its own.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So none of them?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; All three.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; How, if each one passed alone?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Because none were ever tested &lt;em&gt;together&lt;/em&gt;. The new Docker image changes how environment variables load — harmless alone. B renamed a variable the same day — also harmless alone, since B's pipeline never touched the new image. A's payment code was still reading that variable under its old name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So once all three landed the same evening, the variable A's code needed just... wasn't there anymore.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Exactly. Three individually reasonable decisions that never met each other until they were already live. That's most real outages — not one bad call, three good ones that collided.&lt;/p&gt;




&lt;h3&gt;
  
  
  🪞 If I asked you this in an interview
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;"How can three separate pull requests each pass their own tests, yet still break production together?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Because a passing pipeline only proves a change is safe in isolation. It says nothing about how that change interacts with other changes landing the same day. Integration risk lives between PRs, not inside any single one of them.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  What You Should Be Able to Explain Now
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;(Without looking at Google)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Can you explain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why manual deployments don't scale?&lt;/li&gt;
&lt;li&gt;Why investigation usually costs more than fixing?&lt;/li&gt;
&lt;li&gt;Why companies automate deployment checklists instead of hiring people to run them?&lt;/li&gt;
&lt;li&gt;Why "deployment succeeded" isn't the same as "production is healthy"?&lt;/li&gt;
&lt;li&gt;Why CI/CD exists as more than just "automation" for its own sake?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If yes — you're thinking like an engineer, not someone memorizing tool names.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; So when I push code — who actually presses the button that kicks all this off?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; Nobody. Not directly.&lt;/p&gt;




&lt;h3&gt;
  
  
  🔧 What Happens Behind the Scenes
&lt;/h3&gt;

&lt;p&gt;You type&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Does GitHub immediately start running your tests?&lt;/p&gt;

&lt;p&gt;No. First, GitHub receives a &lt;strong&gt;webhook&lt;/strong&gt; — a notification that something happened in the repo. Then it checks whether &lt;code&gt;.github/workflows/&lt;/code&gt; contains any workflow file matching this event.&lt;/p&gt;

&lt;p&gt;If no matching workflow exists — nothing happens. No pipeline. No runner. Nothing. Silence.&lt;/p&gt;

&lt;p&gt;If one &lt;em&gt;does&lt;/em&gt; match — only then does anything wake up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Junior Engineer:&lt;/strong&gt; Wake up where, though? Whose computer is actually running my tests?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior Engineer:&lt;/strong&gt; That's the real question. And why can't GitHub just run the code itself, on its own servers, without any of this ceremony?&lt;/p&gt;

&lt;p&gt;That's exactly where we're going next.&lt;/p&gt;

&lt;p&gt;So — where did that machine come from?&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>productivity</category>
      <category>development</category>
      <category>computerscience</category>
    </item>
    <item>
      <title>Episode 8: Git Workflow</title>
      <dc:creator>surajrkhonde</dc:creator>
      <pubDate>Thu, 30 Jul 2026 16:12:03 +0000</pubDate>
      <link>https://dev.to/surajrkhonde/episode-8-git-workflow-1o37</link>
      <guid>https://dev.to/surajrkhonde/episode-8-git-workflow-1o37</guid>
      <description>&lt;p&gt;&lt;em&gt;This series follows a fictional conversation between an experienced engineer and his nephew. Every episode explores one stage of how software moves from an idea to production.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Uncle, code's ready. &lt;code&gt;git add .&lt;/code&gt;, &lt;code&gt;git commit&lt;/code&gt;, &lt;code&gt;git push&lt;/code&gt; to main, done?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Stop. Two problems already — "code's ready" and "push to main." Let's go all the way back to the beginning, because you skipped the part that actually matters most.&lt;/p&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; The beginning of what?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Of this branch's entire life. Before we get there — do you know what Git actually changed, when it showed up?&lt;/p&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Version control, right?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Before Git, most teams worked off a shared folder, or a system where a file was locked while one person edited it — nobody else could touch it until they were done. Git changed the whole model: everyone works on a full copy of their own, completely independently, and merging intelligently became possible instead of "wait your turn." That's why, decades later, it's still how nearly every team on earth builds software together. But that same freedom is exactly why a team needs real discipline around it — a tool that lets everyone work independently can also let everyone quietly step on each other, if nobody agrees on the rules. Let me show you the whole journey of one feature, properly, from the ticket to the merge.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step One: The Branch Starts From an Updated Main
&lt;/h2&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; WISH-24 — the Wishlist ticket from Episode 1 — just got assigned to you. What's the very first Git command you run?&lt;/p&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; &lt;code&gt;git checkout -b feature/wishlist&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Straight from whatever your local &lt;code&gt;main&lt;/code&gt; happened to be, possibly days old? No.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout main
git pull origin main
git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; feature/WISH-24-add-wishlist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Why does it matter if my &lt;code&gt;main&lt;/code&gt; is a few days old before branching?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Because every commit that's landed on &lt;code&gt;main&lt;/code&gt; since you last pulled is a commit you're &lt;em&gt;not&lt;/em&gt; building on top of. Branch from stale code, and you're setting yourself up for a bigger conflict later, for no reason. And notice the branch name — &lt;code&gt;WISH-24&lt;/code&gt; isn't decoration. It's how anyone — a teammate, a reviewer, you in three months — can trace this branch straight back to Episode 1's ticket without asking.&lt;/p&gt;

&lt;p&gt;One more thing, right now, before you write a single line: you will never push this branch directly to &lt;code&gt;main&lt;/code&gt;. &lt;code&gt;main&lt;/code&gt; is protected — everything you're about to do happens here, on &lt;code&gt;feature/WISH-24-add-wishlist&lt;/code&gt;, until it's earned its way onto &lt;code&gt;main&lt;/code&gt; through review. Keep that in the back of your mind for the rest of this journey; we'll come back to exactly why it's enforced, not just agreed upon.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step Two: Small Commits, Not One Giant One
&lt;/h2&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Now I write the whole feature, and commit once at the end?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; That's how you end up with a commit history that looks like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Final changes
more fixes
again
final final
asdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useless to anyone reading it later, including you. Instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Add wishlist endpoint
Handle duplicate item validation
Add unit tests for duplicate case
Update API docs for /wishlist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Why does it actually matter, if it all ends up in the same PR anyway?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Because six months from now, someone runs &lt;code&gt;git blame&lt;/code&gt; on a line that's misbehaving, and finds either "Handle duplicate item validation" — instantly useful — or "asdf" — useless. Small, honest commits are a gift to whoever debugs this after you, and that person is often you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step Three: Open a Draft PR Early, Not at the End
&lt;/h2&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; So I keep working, and only open a PR once everything's finished?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; On a lot of teams, no — you open a &lt;strong&gt;Draft PR&lt;/strong&gt; almost immediately, after the first meaningful commit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create branch
   ↓
Write first commit
   ↓
Open Draft PR
   ↓
Keep pushing commits
   ↓
Mark "Ready for Review"
   ↓
Merge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Why open it before it's even done?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; A draft PR is visible to the whole team, but explicitly marked "not ready yet" — nobody's expected to review it seriously. It lets a teammate glance at your direction early and flag a problem before you've built three more days on top of it. It also means your CI starts running immediately, on every push, instead of surprising you all at once at the end. And a pull request isn't only for finding bugs — it's for making sure two engineers never spend three days independently building different solutions to the same problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step Four: Sync With Main While You Work
&lt;/h2&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; And while I'm still working?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; You rebase against &lt;code&gt;main&lt;/code&gt; regularly — not once, right before merging.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git fetch origin
git rebase origin/main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember Episode 3 — we drew boundaries so one feature wouldn't force you to reopen a box someone else owns. If those boundaries hold, you and a teammate rarely touch the same file at the same time, and syncing often keeps whatever overlap remains small enough to resolve in minutes, not hours.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step Five: CI Has to Pass — Nobody Merges on "Looks Good"
&lt;/h2&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Reviewer left a comment: "Looks good, approving." Can I merge now?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Look at the bottom of the PR first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ Unit Tests
✅ Build
✅ Lint
❌ Integration Tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One red cross. No merge — not because I said so, because the repository itself won't allow it.&lt;/p&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Even with a human approval already on it?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Even then. A real production pipeline runs this automatically, on every single push, before a human's opinion even matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PR opens
   ↓
Lint
   ↓
Unit tests
   ↓
Security scan
   ↓
Build
   ↓
Coverage check
   ↓
Reviewer approval
   ↓
Merge enabled
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Human approval and CI passing are two separate gates. Both have to open before the merge button even becomes clickable. That's the single most important production Git concept most juniors never see, because on a personal project, there's no CI watching over your shoulder at all.&lt;/p&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; What if someone else's PR merges into &lt;code&gt;main&lt;/code&gt; while mine is still being reviewed?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Then your branch is now behind &lt;code&gt;main&lt;/code&gt; again, even though your CI passed a moment ago. You sync — rebase against the latest &lt;code&gt;main&lt;/code&gt; — and CI runs again from scratch. Only once it's green &lt;em&gt;against the current &lt;code&gt;main&lt;/code&gt;&lt;/em&gt;, not the &lt;code&gt;main&lt;/code&gt; from an hour ago, does merge become possible. A green check next to an outdated branch means nothing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step Six: Who Actually Reviews This?
&lt;/h2&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Who gets assigned as reviewer — whoever's online?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; On a real repo, it's usually decided before anyone even opens the PR, through something like a &lt;code&gt;CODEOWNERS&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight codeowners"&gt;&lt;code&gt;&lt;span class="n"&gt;/services/wishlist/**&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;@backend-team&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;/infra/**&lt;/span&gt;&lt;span class="w"&gt;                  &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;@platform-team&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;/auth/**&lt;/span&gt;&lt;span class="w"&gt;                   &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;@security-team&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Touch a file under &lt;code&gt;/auth/&lt;/code&gt;, even accidentally, and the security team is automatically requested — whether you meant to touch auth or not. That's not bureaucracy for its own sake; it's how a large company makes sure the person reviewing actually knows the part of the system you touched.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step Seven: Merge — but Which Kind?
&lt;/h2&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; CI's green, review's approved. Now &lt;code&gt;git merge&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Now you find out which merge strategy this specific team has chosen — because companies genuinely differ here, and none of them is universally "correct."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Squash Merge   → all your commits become one clean commit on main
Merge Commit   → your full commit history is preserved, plus a merge commit
Rebase Merge   → your commits are replayed onto main individually, no merge commit at all
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Which one's right?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; The one your team already agreed on and uses consistently. Squash keeps &lt;code&gt;main&lt;/code&gt;'s history short and readable, at the cost of losing your intermediate commits. Merge commits preserve everything, at the cost of a noisier history. What actually matters isn't which one you pick — it's that the whole team uses the same one, so the history means the same thing everywhere in the repo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step Eight: The Branch Dies, on Purpose
&lt;/h2&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; And after it merges?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; The branch gets deleted. Its job is done — it existed only to carry this one change safely from your machine to &lt;code&gt;main&lt;/code&gt;. Keeping it around just clutters the repo with branches nobody will ever look at again.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Two Rules That Protect All of This
&lt;/h2&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Underneath this whole journey, two rules make it safe. First — &lt;code&gt;main&lt;/code&gt; is protected. Nobody, including the most senior engineer in the building, can push to it directly. Every change goes through this exact path. Second — force-push is banned on shared branches.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Teammate pushes commit A
You force-push, based on an older history
→ commit A is now gone from the remote branch
→ nobody gets an error
→ someone just discovers their work missing, later
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On your own branch, &lt;code&gt;git push --force-with-lease&lt;/code&gt; is fine — it checks nobody else's work would be erased before it lets you push. On &lt;code&gt;main&lt;/code&gt;, or any shared branch, it's blocked outright, by the repository itself, not just by convention.&lt;/p&gt;




&lt;h2&gt;
  
  
  Branch → Sync → Review
&lt;/h2&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; The framework — does it still hold, after all of this?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; It holds, it just carries more now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Branch
  ↓
Sync
  ↓
Review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Branch&lt;/strong&gt; — from an updated &lt;code&gt;main&lt;/code&gt;, named after the ticket, opened as a draft the moment there's something worth seeing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sync&lt;/strong&gt; — small, honest commits, rebased against &lt;code&gt;main&lt;/code&gt; often, so conflicts stay small and rare.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Review&lt;/strong&gt; — two independent gates before merge: CI passing, and a human who actually owns that part of the codebase approving it — then a merge strategy the whole team already agreed on, applied the same way every time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Uncle's Line
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"Git doesn't protect your code. Team discipline does. Git is just the notebook."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; So the whole journey — ticket, branch, small commits, syncing, draft PR, CI, review, merge, branch deleted — that's what actually happens every time, on a real team.&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Every single time, on every serious team you'll ever work on. Now your code is on &lt;code&gt;main&lt;/code&gt;. That feels like the finish line. It isn't — it's the starting gun. What happens to it next is a different lesson entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 Think Like an Engineer — Homework
&lt;/h2&gt;

&lt;p&gt;Take the same feature you've been designing since Episode 3. Walk it through the entire journey yourself.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Branch&lt;/strong&gt; — write the branch name, tied to a ticket ID, and the two commands you'd run before creating it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sync&lt;/strong&gt; — write three small, honest commit messages you'd realistically make while building it, instead of one giant one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review&lt;/strong&gt; — name one CI check you'd want gating this PR beyond tests (lint, security scan, coverage), and one file path that should automatically pull in a specific team via something like &lt;code&gt;CODEOWNERS&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't worry about the exact commands. The goal is to practice seeing your code as one small piece moving through a pipeline the whole team relies on — not as something finished the moment you stop typing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;— End of Episode 8 —&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>softwareengineering</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Episode 7: Local Development</title>
      <dc:creator>surajrkhonde</dc:creator>
      <pubDate>Thu, 30 Jul 2026 05:43:42 +0000</pubDate>
      <link>https://dev.to/surajrkhonde/episode-7-local-development-15ba</link>
      <guid>https://dev.to/surajrkhonde/episode-7-local-development-15ba</guid>
      <description>&lt;p&gt;&lt;em&gt;This series follows a fictional conversation between an experienced engineer and his nephew. Every episode explores one stage of how software moves from an idea to production.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Uncle. Finally. VS Code is open. &lt;code&gt;npm start&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Wait. Before you run that — where is this about to connect? What database, whose credentials, which environment?&lt;/p&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; ...I don't actually know. It just works when I run it.&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; That's exactly the problem. "It just works" is not the same as "I know what it's doing." Open the config and show me.&lt;/p&gt;




&lt;h2&gt;
  
  
  Configure: Secrets Don't Belong in Code
&lt;/h2&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Look at this line.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dbUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postgres://admin:P@ssw0rd@prod-db.internal:5432/wishlist&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; ...that's a real password. Hardcoded. In a file that's about to get committed to Git.&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; And the moment it's committed, it's in the repository's history forever, even if you delete the line in the next commit. Anyone with repo access — or anyone the repo is ever accidentally made public to — has your production database password.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;.&lt;span class="n"&gt;env&lt;/span&gt;  (&lt;span class="n"&gt;never&lt;/span&gt; &lt;span class="n"&gt;committed&lt;/span&gt; — &lt;span class="n"&gt;listed&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt; .&lt;span class="n"&gt;gitignore&lt;/span&gt;)

&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt;=&lt;span class="n"&gt;postgres&lt;/span&gt;://&lt;span class="n"&gt;localhost&lt;/span&gt;:&lt;span class="m"&gt;5432&lt;/span&gt;/&lt;span class="n"&gt;wishlist_dev&lt;/span&gt;
&lt;span class="n"&gt;NODE_ENV&lt;/span&gt;=&lt;span class="n"&gt;development&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dbUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; So the code stays identical, but what it connects to changes depending on which &lt;code&gt;.env&lt;/code&gt; is loaded?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Exactly. Same code, different config, per environment. Local uses &lt;code&gt;wishlist_dev&lt;/code&gt;, staging uses a staging URL, production uses production — none of them ever written into the code itself. This is the same principle from Episode 4 — a box shouldn't know things it doesn't need to. Your service code doesn't need to know it's "in production." It just needs a URL, handed to it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mirror: Don't Fake the Database
&lt;/h2&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; For local development, could I just use something simple, like SQLite, instead of setting up real Postgres on my machine?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; You could. And it will quietly lie to you. Remember Episode 5 — the &lt;code&gt;UNIQUE (user_id, product_id)&lt;/code&gt; constraint we designed? SQLite and Postgres don't always enforce constraints, types, or even certain query behaviors identically. You could write code that works perfectly against SQLite locally, pass every test on your machine, and then hit a completely different error the moment it touches real Postgres in staging.&lt;/p&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; So local has to be the exact same database engine as production.&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; As close as realistically possible — same engine, same major version where you can manage it. It doesn't have to be pixel-perfect identical; production might run a slightly newer minor version, or sit inside Kubernetes while local runs in a single container. What matters is behavior, not literal sameness. This is what Docker is for — one common way to get a consistent, disposable copy of that engine running locally. Some teams reach for Dev Containers, Podman, or a remote cloud workspace instead — the tool matters less than the principle: the same engine your production environment uses, not a lighter substitute that behaves differently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;docker-compose.yml&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;wishlist_dev&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432:5432"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; One command, and I have the same database engine my company actually runs in production, just running locally with test data.&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; That's the goal. Not a perfect clone of production — you don't need production's scale or its real user data locally — but behavior close enough that what passes locally has a real chance of passing in staging too.&lt;/p&gt;




&lt;h2&gt;
  
  
  Seed Data That Actually Tests Something
&lt;/h2&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; The database is running, but it's empty. How do I test the duplicate check without any users or products to test it against?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; You seed it — deliberately, not randomly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;js&lt;/span&gt;

&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;u1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Test User&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
&lt;span class="nx"&gt;products&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;p1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Running Shoes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;p2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Discontinued Jacket&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Why seed an inactive product on purpose?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Because you need to actually exercise Episode 5's decision — what happens when someone wishlists, or already has wishlisted, a product that's no longer active. If your seed data is just "five random happy-path products," you'll never locally trigger the exact edge cases you spent two episodes designing for. Seed data should reflect your hidden questions from Episode 1, not just the obvious case.&lt;/p&gt;




&lt;h2&gt;
  
  
  "It Works On My Machine"
&lt;/h2&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Uncle — did the "it just works" trap ever actually happen to you?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Once, badly. Everything passed on my laptop. Perfectly. Then CI failed, over and over, on a check that made no sense — a config value my code claimed didn't exist. Took most of an afternoon before we found it: my machine had an old environment variable left over from a project I'd worked on six months earlier. It was quietly filling in a value nobody else's setup had. My laptop wasn't more correct — it was just differently broken, in a way that happened to look like success.&lt;/p&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; So the code was never actually right.&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; It was right by accident, on exactly one machine. Since that day, if a new laptop can't run my project cleanly, I assume the &lt;em&gt;setup&lt;/em&gt; is wrong — not the teammate. That single habit has saved my team more time than almost any other discipline in this episode.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mock: What Doesn't Exist Yet
&lt;/h2&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Episode 3 mentioned a Notification Service — price-drop alerts. It doesn't exist yet. Do I need it running locally to develop Wishlist?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; No, and this is where a lot of local setups quietly become painful. If every engineer needs every other team's service running locally just to develop their own feature, onboarding becomes a nightmare, and half the team ends up debugging Docker instead of writing code. Instead — mock it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;notificationService&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;development&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;MockNotificationService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;// logs to console, does nothing real&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NotificationService&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;      &lt;span class="c1"&gt;// real network call&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; So Wishlist Service still calls something named &lt;code&gt;notificationService&lt;/code&gt;. It just doesn't matter locally what's actually behind that name.&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Exactly — and notice this only works cleanly &lt;em&gt;because&lt;/em&gt; of Episode 4's design. We decided Wishlist Service shouldn't know the details of how notifications get sent. That same boundary is what lets you swap in a mock without touching a single line of business logic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Not Just Point at Production?
&lt;/h2&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Honestly — could I skip all of this, and just connect my laptop directly to the real production database? It would definitely match production, since it &lt;em&gt;is&lt;/em&gt; production.&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Not unless you enjoy explaining outages. Production data is real and valuable. Production systems are fragile in ways you can't always see from outside. One accidental&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wishlist&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;without a &lt;code&gt;WHERE&lt;/code&gt; clause, run while you're "just testing something quickly" — and your afternoon becomes a company-wide incident review. That's the entire reason local development exists as its own discipline: to give you a space where mistakes are cheap, reversible, and entirely yours to make.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Test That Actually Matters: Can a New Teammate Run This?
&lt;/h2&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Here's how you know your local setup is actually good, not just working for you personally — hand a laptop to a new teammate who's never seen this repo, and see what happens.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone ...
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
docker-compose up
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run seed
npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; If that doesn't work start to finish, on a machine that isn't mine...&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Then it's not actually reproducible — it's just "works on my machine," and that phrase has cost more engineering hours industry-wide than almost any bug.&lt;/p&gt;




&lt;h2&gt;
  
  
  Configure → Mirror → Mock
&lt;/h2&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; The framework.&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Configure
  ↓
Mirror
  ↓
Mock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Configure&lt;/strong&gt; — every secret and every environment-specific value lives outside the code, loaded at runtime. The same code should run in every environment without a single line changing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mirror&lt;/strong&gt; — your local environment should use the same core technology as production — same database engine, same major versions — not a lighter substitute that behaves differently under the exact conditions you're trying to test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mock&lt;/strong&gt; — anything genuinely outside this feature's boundary — another team's service, a dependency that doesn't exist yet — gets a stand-in locally, so your setup stays small and every engineer can run it without needing the entire company's infrastructure on their laptop.&lt;/p&gt;




&lt;h2&gt;
  
  
  Uncle's Line
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"A local environment that lies to you quietly is worse than one that doesn't work at all — at least a broken setup tells you it's broken."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; So all of this, before I even write the actual &lt;code&gt;addItem&lt;/code&gt; logic.&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; All of it. Now you can actually write code, knowing exactly what it's talking to, and trusting that what works on your machine will behave the same way in staging. But writing the code is only half the discipline — how you save it, share it, and merge it without stepping on your teammates is the next lesson.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 Think Like an Engineer — Homework
&lt;/h2&gt;

&lt;p&gt;Take the same feature you've been designing since Episode 3.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Configure&lt;/strong&gt; — list at least three values this feature would need that should never be hardcoded (a URL, a key, a flag). Sketch what your &lt;code&gt;.env&lt;/code&gt; file would contain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mirror&lt;/strong&gt; — name one place where using a "simpler" local substitute (a different database, an in-memory fake) could hide a bug that only appears in staging or production.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mock&lt;/strong&gt; — name one dependency this feature would have on something outside its own boundary, and sketch how you'd stub it out locally without needing the real thing running.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't worry about getting the exact tooling right. The goal is to practice noticing every place your feature secretly depends on its environment, before that environment changes underneath you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;— End of Episode 7 —&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>programming</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>Episode 6: API Design</title>
      <dc:creator>surajrkhonde</dc:creator>
      <pubDate>Wed, 29 Jul 2026 08:34:50 +0000</pubDate>
      <link>https://dev.to/surajrkhonde/episode-6-api-design-4i64</link>
      <guid>https://dev.to/surajrkhonde/episode-6-api-design-4i64</guid>
      <description>&lt;p&gt;&lt;em&gt;This series follows a fictional conversation between an experienced engineer and his nephew. Every episode explores one stage of how software moves from an idea to production.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Uncle, the service is designed, the table is designed. Now surely the API is just... &lt;code&gt;POST /api/wishlist&lt;/code&gt;. We basically wrote that in Episode 4 already.&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; You wrote &lt;em&gt;an&lt;/em&gt; example. You never asked whether REST was even the right way for two systems to talk here. You just assumed it, because it's what you've always seen.&lt;/p&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Isn't REST just... how APIs work?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; REST is &lt;em&gt;one&lt;/em&gt; way two systems agree to talk. There are several. Before you design the contract, you should know why you picked this particular language, instead of a different one that was sitting right there.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Ways Two Systems Talk
&lt;/h2&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Let me show you four, quickly, and what each one is actually good at.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;REST        — resources over HTTP, usually JSON, stateless
gRPC        — binary protocol, strongly typed contracts, built for speed
SOAP        — XML-based, strict formal contracts, heavy but rigorous
WebSocket   — a connection that stays open, either side can push, anytime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; So why doesn't everyone just use whichever one is "best"?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Because none of them is best. Each one trades something for something else. Watch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;REST&lt;/strong&gt; — a browser or any human-readable client can call it and debug it in a browser tab. Stateless, so every request stands alone — simple to reason about, easy to cache. The cost: chattier than the alternatives, and JSON is heavier on the wire.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;gRPC&lt;/strong&gt; — built for backend services talking to each other at high volume, where speed and byte-size matter. Both sides agree on a strict, typed contract before any business logic runs. The cost: not something a browser calls directly without extra tooling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SOAP&lt;/strong&gt; — older and heavier, but with a formality REST doesn't enforce: a machine-readable contract for every operation, checked by tooling, not just convention. Still common in banking and government systems, where that guarantee matters more than developer convenience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebSocket&lt;/strong&gt; — for the moment your feature genuinely needs the &lt;em&gt;server&lt;/em&gt; to speak first, without being asked. Live chat, real-time price updates, a live viewer counter.&lt;/p&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; So which one is Wishlist?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Walk through it with me instead of guessing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing for Wishlist, Not in General
&lt;/h2&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Who calls this API?&lt;/p&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; The Flipkart mobile app and website. So... a real browser and a mobile client, not another backend service.&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; That already rules out gRPC as the &lt;em&gt;outward-facing&lt;/em&gt; choice — browsers don't speak it natively. Does the client need the server to push anything to it, without asking?&lt;/p&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Not for adding or removing an item — the user taps a button, waits for a response. That's a request, then a reply.&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; That rules out WebSocket, for this action specifically. Does this need the rigid, contract-first formality SOAP demands — heavy governance, strict versioned schemas, likely a regulator somewhere caring about the exact contract?&lt;/p&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; For a wishlist button? No.&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Then you've actually arrived at REST — not because it's the default, but because you eliminated the others for real reasons. That's the difference between picking a tool and inheriting one.&lt;/p&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Wait — you said "for this action specifically" about WebSocket. Is there a part of Wishlist where it &lt;em&gt;would&lt;/em&gt; fit?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Good catch. Remember Episode 3's tease — price-drop notifications. If you want that price drop to appear on a user's screen &lt;em&gt;while they're already looking at their wishlist&lt;/em&gt;, without them refreshing the page, that's exactly a WebSocket problem. But notice — that's a different action, with a different requirement, deserving its own decision. That's an entire lesson by itself, later in this series. Adding, removing, listing — those stay REST.&lt;/p&gt;




&lt;h2&gt;
  
  
  Specify: The Exact Contract
&lt;/h2&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Now that REST is chosen deliberately, we specify it properly — not one example endpoint, but the full shape.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;POST    /api/v1/wishlist          → addItem
GET     /api/v1/wishlist          → listItems
DELETE  /api/v1/wishlist/{itemId} → removeItem
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Why &lt;code&gt;/api/v1/&lt;/code&gt; — we only have one version. Isn't that premature?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; It feels premature until the day it isn't. The moment you need to change the response shape in a way that breaks existing mobile app clients still installed on someone's phone — and you will, eventually — you need &lt;code&gt;v2&lt;/code&gt; to exist &lt;em&gt;next to&lt;/em&gt; &lt;code&gt;v1&lt;/code&gt;, not instead of it. Adding the version prefix on day one costs nothing. Retrofitting it onto a live API that thousands of clients already call is a much harder migration.&lt;/p&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Why is &lt;code&gt;removeItem&lt;/code&gt; a &lt;code&gt;DELETE&lt;/code&gt; on &lt;code&gt;/wishlist/{itemId}&lt;/code&gt;, and not &lt;code&gt;POST /wishlist/remove&lt;/code&gt; with the ID in the body?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Because REST resources should be nouns, and HTTP methods should be the verbs. &lt;code&gt;/wishlist/{itemId}&lt;/code&gt; names a specific thing. &lt;code&gt;DELETE&lt;/code&gt; says what you're doing to it. &lt;code&gt;POST /wishlist/remove&lt;/code&gt; invents a fake verb-shaped URL, which means every client and every piece of tooling that expects REST conventions — caching layers, API gateways, generated documentation — now has to special-case your API instead of just understanding it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Status Codes Are Part of the Contract Too
&lt;/h2&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; In Episode 4, &lt;code&gt;DuplicateItemError&lt;/code&gt; mapped to 409. Is that part of API design, or was that already decided?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; It was decided in principle back then. Today you write it down as part of the actual contract, consistently, for every case — not invented per-endpoint.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;201 Created       — addItem succeeded
200 OK            — listItems succeeded
204 No Content    — removeItem succeeded
400 Bad Request   — invalid productId format
401 Unauthorized  — not logged in
404 Not Found     — product doesn't exist
409 Conflict      — item already in wishlist
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Why does this matter so much? Couldn't everything just return &lt;code&gt;200&lt;/code&gt; with a &lt;code&gt;success: true/false&lt;/code&gt; field?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; It could — and plenty of real APIs do. But then every single client has to parse your response body just to know if something failed, instead of being able to check the status code alone, which every HTTP tool already understands natively. You'd be throwing away information the protocol gives you for free.&lt;/p&gt;




&lt;h2&gt;
  
  
  One Error Shape, Everywhere
&lt;/h2&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Last piece — when something does fail, what does the client actually receive?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DUPLICATE_ITEM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This product is already in your wishlist."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Why a &lt;code&gt;code&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; a &lt;code&gt;message&lt;/code&gt;? Isn't the message enough?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; The message is for a human reading logs, or a developer debugging. The &lt;code&gt;code&lt;/code&gt; is for the &lt;em&gt;client's code&lt;/em&gt; to branch on — &lt;code&gt;if (error.code === "DUPLICATE_ITEM")&lt;/code&gt;. If the client had to match against the message text instead, changing a single word to make it clearer for humans would silently break every app relying on that exact string. And this shape should be identical across every endpoint in this API — not just Wishlist. The moment error format varies endpoint to endpoint, every client has to write custom handling for each one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Survey → Select → Specify
&lt;/h2&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; The framework.&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dot"&gt;&lt;code&gt;&lt;span class="nv"&gt;Survey&lt;/span&gt;
  &lt;span class="err"&gt;↓&lt;/span&gt;
&lt;span class="nv"&gt;Select&lt;/span&gt;
  &lt;span class="err"&gt;↓&lt;/span&gt;
&lt;span class="nv"&gt;Specify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Survey&lt;/strong&gt; — know what your real options are before defaulting to the familiar one. REST, gRPC, SOAP, WebSocket — each is genuinely the right answer somewhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Select&lt;/strong&gt; — eliminate options for real reasons tied to who's calling, whether the server needs to push, and how much formal contract enforcement the situation actually demands. Don't default — arrive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Specify&lt;/strong&gt; — once chosen, write the &lt;em&gt;whole&lt;/em&gt; contract: every method, every status code, one consistent error shape, and a version prefix from day one, even when day one only has one version.&lt;/p&gt;




&lt;h2&gt;
  
  
  Uncle's Line
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"The protocol isn't a technical detail you pick last. It's a promise about who can talk to your system, and how — and promises are expensive to break once someone else is depending on them."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; So now I actually know everything — what to build, what to reuse, how the boxes connect, what's inside each box, how the data is stored, and now how the outside world talks to it. Finally, VS Code?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Finally. But even sitting down to code has its own discipline — how you set up your machine, your branch, your environment, before the first line even exists. That's next.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 Think Like an Engineer — Homework
&lt;/h2&gt;

&lt;p&gt;Take the same feature you've been designing since Episode 3.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Survey&lt;/strong&gt; — briefly consider all four: would REST, gRPC, SOAP, or WebSocket ever genuinely fit part of this feature? Don't dismiss any without a real reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Select&lt;/strong&gt; — pick one for the main action, and write one sentence defending why you eliminated each of the other three.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Specify&lt;/strong&gt; — write out the endpoint(s), the HTTP methods, at least three realistic status codes, and one consistent error shape you'd use across all of them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't worry about picking the fashionable choice. The goal is to practice arriving at a protocol on purpose, instead of inheriting one by habit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;— End of Episode 6 —&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>restapi</category>
      <category>distributedsystems</category>
      <category>api</category>
    </item>
    <item>
      <title>Episode 5: Database Decisions</title>
      <dc:creator>surajrkhonde</dc:creator>
      <pubDate>Wed, 29 Jul 2026 03:21:54 +0000</pubDate>
      <link>https://dev.to/surajrkhonde/episode-5-database-decisions-5fj6</link>
      <guid>https://dev.to/surajrkhonde/episode-5-database-decisions-5fj6</guid>
      <description>&lt;p&gt;&lt;em&gt;This series follows a fictional conversation between an experienced engineer and his nephew. Every episode explores one stage of how software moves from an idea to production.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Uncle, the service is designed. Functions, flow, the split between Service and Repository. Now, finally, the database?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Finally, the database. And I want you to unlearn something before we start — a table is not "just where data sits." Every choice you make here outlives almost everything else we've designed. Code gets refactored constantly. A production table with real user data in it gets refactored rarely, carefully, and painfully. Get this wrong, and you're not fixing a bug — you're running a migration on a table with a million live rows, at 2 AM, hoping nothing breaks.&lt;/p&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; That sounds dramatic for a table with four columns.&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Let's find out if it's dramatic. Look at what we sketched loosely back in Episode 4.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wishlist_items
  id
  user_id
  product_id
  created_at
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every single one of those four things — I want you to be able to defend, not guess.&lt;/p&gt;




&lt;h2&gt;
  
  
  Model: What Are We Actually Storing?
&lt;/h2&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; First question, before any column names — should this even be a relational table? Or does Wishlist belong in a document store, key-value store, something else?&lt;/p&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; I don't know. When do you &lt;em&gt;not&lt;/em&gt; use a relational database?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; When your data doesn't have real relationships worth enforcing, or when your access pattern is almost entirely "fetch this one document by its key," with no need to join across entities. Think about what we're actually building here — a wishlist item only makes sense if both the user and the product it points to actually exist. If someone deletes a product tomorrow, you immediately have to ask what happens to every wishlist entry pointing at it. That question — what happens on one side when the other side changes — is exactly what a relationship is. A relational database is built to protect that. Wishlist fundamentally connects two other things that already exist as rows: a user and a product. Don't choose a database because it's fashionable. Choose it because it matches your data.&lt;/p&gt;




&lt;h2&gt;
  
  
  Constrain: The Keys That Protect You
&lt;/h2&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Now the primary key — &lt;code&gt;id&lt;/code&gt;. In Episode 4 I mentioned UUID versus auto-increment as a real trade-off. Let's actually decide it this time, properly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Option A: id BIGINT AUTO_INCREMENT
Option B: id UUID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Auto-increment is smaller, faster to index. UUID doesn't leak how many rows exist, or let someone guess other IDs by counting.&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Good, you remembered. Now here's the piece that actually decides it for Wishlist specifically — where does this ID get created? If it's created by the database, on a single server, auto-increment works fine. But the moment you have multiple services, or you ever need to create a wishlist item &lt;em&gt;before&lt;/em&gt; it reaches the database — say, generating it client-side for an offline-first mobile experience — auto-increment can't help you, because only the database knows the next number. A UUID can be generated anywhere, safely, with no coordination. For most CRUD tables, either works. But knowing &lt;em&gt;why&lt;/em&gt; you're picking one, not just which is more popular, is the actual skill.&lt;/p&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; For Wishlist specifically?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Honestly — for this feature, either choice is defensible. It's user-facing data, referenced in API responses, no real need for offline generation yet. Auto-increment would work fine, and plenty of high-scale systems deliberately use auto-increment internally, for good reasons. The important part isn't picking the "correct" one — it's understanding the trade-off, because different teams make different calls here depending on scale, tooling, and operational needs. If I had to choose today, I'd lean UUID, mainly so nobody downstream ever builds a feature assuming IDs are sequential and countable — but that's a judgment call, not a rule.&lt;/p&gt;




&lt;h2&gt;
  
  
  Foreign Keys, and the Question From Episode 1
&lt;/h2&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Now &lt;code&gt;user_id&lt;/code&gt; and &lt;code&gt;product_id&lt;/code&gt;. Both should be foreign keys — but a foreign key is more than a reference. It's a &lt;em&gt;rule&lt;/em&gt; about what happens when the thing it points to disappears.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;product_id UUID
  FOREIGN KEY → products.id
  ON DELETE ???
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Wait — this is Episode 1's question. "What if the product gets deleted by the admin tomorrow?"&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; The exact same question. Except back then it was a product decision. Now it's a database decision, and the database forces you to actually pick an answer — it won't let the column stay ambiguous.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ON DELETE CASCADE   → deleting the product silently deletes the wishlist row too
ON DELETE SET NULL  → wishlist row stays, product_id becomes null
ON DELETE RESTRICT  → deleting the product is blocked while it's still wishlisted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Which one is right?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Depends entirely on what Episode 1's business answer was. If the PM said "show it as unavailable," &lt;code&gt;CASCADE&lt;/code&gt; is wrong — it would erase the user's wishlist entry entirely instead of just marking it unavailable. &lt;code&gt;SET NULL&lt;/code&gt; breaks a &lt;code&gt;NOT NULL&lt;/code&gt; foreign key — so we'd either have to allow &lt;code&gt;product_id&lt;/code&gt; to be nullable, or choose a different strategy entirely. Realistically, you'd keep &lt;code&gt;product_id NOT NULL&lt;/code&gt;, skip a hard foreign key delete rule, and instead soft-delete products — mark them inactive rather than actually removing the row. The database decision and the product decision have to agree with each other, or one of them is silently wrong.&lt;/p&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; So I can't design this table without going back and rereading the requirements from Episode 1.&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; That's exactly the point. Every stage of this series has been building toward this moment — a database schema is where every earlier decision either gets honored, or quietly contradicted.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Constraint That Replaces a Function
&lt;/h2&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; What about the duplicate check? In Episode 4, &lt;code&gt;checkDuplicate()&lt;/code&gt; lived in the service.&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; It still does — but remember what we said about race conditions. The service check is your first, fast rejection. The real guarantee lives here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; One line, and it's actually enforced no matter what the application code does.&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; That's the difference between a rule you &lt;em&gt;hope&lt;/em&gt; holds, and a rule that &lt;em&gt;cannot&lt;/em&gt; be violated, even by a bug, even by two requests landing at the same microsecond.&lt;/p&gt;




&lt;h2&gt;
  
  
  Normalize, or Snapshot?
&lt;/h2&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Now the trickiest decision. Should this table store just &lt;code&gt;product_id&lt;/code&gt; — or also a copy of the product's price and name at the moment it was wishlisted?&lt;/p&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Why would we duplicate data that already lives in the products table?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Remember Episode 3 — the price-drop notification feature we teased? To know a price &lt;em&gt;dropped&lt;/em&gt;, you need to know what the price &lt;em&gt;was&lt;/em&gt; when the user wishlisted it. If you only store &lt;code&gt;product_id&lt;/code&gt;, that historical price is gone the moment the live price changes — you're always looking at "now," never "then."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Normalized:
  wishlist_items(user_id, product_id)
  → always join to products for current data
  → simple, never goes stale, but no memory of the past

Snapshot:
  wishlist_items(user_id, product_id, price_at_add)
  → remembers what mattered at the time
  → but now this table itself needs to stay meaningfully accurate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; So it's not "which one is correct." It's "which one does the feature actually need."&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Exactly. Design the database for the requirement in front of you, not for every requirement you can imagine. If price-drop notifications are still just a tease, not a committed feature, don't add &lt;code&gt;price_at_add&lt;/code&gt; today speculatively — that's a column you'll maintain forever for a feature that might never ship. Add it when the requirement is real, the same way we'd handle any hidden question in Episode 1 — ask, don't assume.&lt;/p&gt;




&lt;h2&gt;
  
  
  Scale: The Index Nobody Adds Until It's Too Late
&lt;/h2&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Last question. &lt;code&gt;listItems(userId)&lt;/code&gt; — what does that query actually do to this table?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;wishlist_items&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Looks up rows matching a user. Simple.&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Simple, until this table has ten million rows across all users, and there's no index on &lt;code&gt;user_id&lt;/code&gt;. Then that query scans the entire table, every single time, for every single user opening their wishlist page.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_wishlist_user&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;wishlist_items&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; Doesn't the &lt;code&gt;UNIQUE (user_id, product_id)&lt;/code&gt; constraint already index &lt;code&gt;user_id&lt;/code&gt;, since it's the first column?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Good — that's actually correct for most databases; a composite unique constraint typically also serves lookups on its leading column. Which means for this specific table, you may not need a second index at all. That's worth checking, not assuming — because an index you don't need still costs you something. Before adding any index, always ask: which specific query is this actually speeding up? If you can't name the query, you don't need the index yet.&lt;/p&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; What does an index cost?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; Every index speeds up reads on that column, and slows down every single write, because the database has to update the index too. A table with ten unnecessary indexes writes slower for no reason. Indexing isn't "add more, be safe." It's "add exactly what your real query patterns need, and nothing extra."&lt;/p&gt;




&lt;h2&gt;
  
  
  Model → Constrain → Scale
&lt;/h2&gt;

&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; The framework?&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model
  ↓
Constrain
  ↓
Scale
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Model&lt;/strong&gt; — decide what kind of storage the data's actual shape calls for, and whether to normalize or store a snapshot, based on what the feature genuinely needs — not on every future feature you can imagine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Constrain&lt;/strong&gt; — every key, foreign key, and unique constraint is a rule, not decoration. Each one should trace back to a real answer from your requirements, not a guess made while writing the schema.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scale&lt;/strong&gt; — index for the queries you actually run, verify what you already get for free from existing constraints, and remember every index you add also has a cost on every write.&lt;/p&gt;




&lt;h2&gt;
  
  
  Uncle's Line
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"A wrong function gets refactored in an afternoon. A wrong column gets migrated in a maintenance window, on a table full of real users, with everyone hoping nothing breaks."&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;👦 &lt;strong&gt;Nephew:&lt;/strong&gt; So the database isn't just storage. It's where every earlier decision either gets enforced, or exposed as incomplete.&lt;/p&gt;

&lt;p&gt;👨‍🦳 &lt;strong&gt;Uncle:&lt;/strong&gt; That's exactly it. Now — you have the service designed, and the table designed. You still haven't decided the exact shape of what the &lt;em&gt;outside world&lt;/em&gt; sends and receives to trigger all of this. That's next.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 Think Like an Engineer — Homework
&lt;/h2&gt;

&lt;p&gt;Take the same feature you've been designing since Episode 3.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Model&lt;/strong&gt; — is its core data relational, or does it genuinely fit a different kind of store? Would you normalize it, or does some part of it need a snapshot in time?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constrain&lt;/strong&gt; — pick one foreign key it would need. Decide what should happen when the row it points to gets deleted, and defend your answer using a requirement from Episode 1's kind of thinking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale&lt;/strong&gt; — write the one query this feature will run most often. Does it need an index that doesn't already exist as a side effect of another constraint?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't worry about picking the "textbook correct" answer. The goal is to practice noticing that a database schema is a set of promises, not just a set of columns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;— End of Episode 5 —&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>productivity</category>
      <category>beginners</category>
      <category>database</category>
    </item>
  </channel>
</rss>
