<?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: Adam - The Developer</title>
    <description>The latest articles on DEV Community by Adam - The Developer (@adamthedeveloper).</description>
    <link>https://dev.to/adamthedeveloper</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%2F1002243%2F84fa5f44-c4e1-4fec-934c-9fa687161e10.webp</url>
      <title>DEV Community: Adam - The Developer</title>
      <link>https://dev.to/adamthedeveloper</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/adamthedeveloper"/>
    <language>en</language>
    <item>
      <title>When Software Started Writing Software: A Developer’s History of AI</title>
      <dc:creator>Adam - The Developer</dc:creator>
      <pubDate>Mon, 22 Jun 2026 08:06:29 +0000</pubDate>
      <link>https://dev.to/adamthedeveloper/when-software-started-writing-software-a-developers-history-of-ai-4p9n</link>
      <guid>https://dev.to/adamthedeveloper/when-software-started-writing-software-a-developers-history-of-ai-4p9n</guid>
      <description>&lt;p&gt;If you've shipped software in the last three years, you've probably watched your job description quietly rewrite itself. You went from writing code, to writing code &lt;em&gt;with&lt;/em&gt; an autocomplete, to writing code &lt;em&gt;with&lt;/em&gt; a collaborator, to increasingly writing a spec and watching an agent write, test, and ship the code for you.&lt;/p&gt;

&lt;p&gt;That didn't happen overnight. It's the latest chapter in a 70-year story that started with researchers trying to teach machines to play checkers. Let's walk through it, not as a dry timeline, but as the story of how "intelligence" kept getting redefined every time machines got good at the last definition.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;1. The Symbolic Era: Intelligence as Logic (1950s–1980s)&lt;/li&gt;
&lt;li&gt;2. Statistics Quietly Eats Symbols (1990s–2000s)&lt;/li&gt;
&lt;li&gt;3. Deep Learning Breaks the Ceiling (2012–2017)&lt;/li&gt;
&lt;li&gt;4. The Transformer and the Birth of "General-ish" Intelligence (2017–2022)&lt;/li&gt;
&lt;li&gt;5. From Chatbot to Coworker: The Agentic Turn (2023–Today)&lt;/li&gt;
&lt;li&gt;So What Changed, Really?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. The Symbolic Era: Intelligence as Logic (1950s–1980s)
&lt;/h2&gt;

&lt;p&gt;The founding bet of AI, made official at the 1956 Dartmouth Workshop, was simple and audacious: thought is computation. If you could represent knowledge as symbols and rules, and manipulate those symbols correctly, you'd get intelligence.&lt;/p&gt;

&lt;p&gt;This gave us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Logic Theorist (1956)&lt;/strong&gt;: proved mathematical theorems by searching through logical statements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ELIZA (1966)&lt;/strong&gt;: pattern-matched your sentences back at you and convinced people it understood them. The first chatbot, and the first time humans projected understanding onto a system that had none.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expert systems (1970s–80s)&lt;/strong&gt;: programs like &lt;a href="https://en.wikipedia.org/wiki/MYCIN" rel="noopener noreferrer"&gt;MYCIN&lt;/a&gt; encoded a domain expert's rules as &lt;code&gt;IF-THEN&lt;/code&gt; statements. MYCIN could diagnose bacterial infections about as well as a human specialist, using a few hundred hand-written rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was &lt;strong&gt;weak, narrow intelligence&lt;/strong&gt; in the most literal sense: a system that was a genius in one box and knew nothing outside it. The fatal flaw was scale, every rule had to be written by hand by a human expert. Knowledge didn't generalize, and it didn't learn from data. When funding agencies realized these systems couldn't handle the messiness of the real world, the money dried up. This was the &lt;strong&gt;first AI winter&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Statistics Quietly Eats Symbols (1990s–2000s)
&lt;/h2&gt;

&lt;p&gt;While "AI" was a dirty word in grant applications, a different idea was gaining ground: instead of &lt;em&gt;telling&lt;/em&gt; a machine the rules, show it examples and let it find the rules itself.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Neural networks&lt;/strong&gt; had existed since the 1950s (the perceptron) but were widely written off after Minsky and Papert detailed their mathematical limits in &lt;a href="https://en.wikipedia.org/wiki/Perceptrons_(book)" rel="noopener noreferrer"&gt;Perceptrons (1969)&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backpropagation&lt;/strong&gt;, popularized in 1986, gave multi-layer networks a real way to learn and yet networks still mostly lost to simpler methods for another two decades. The algorithm existing wasn't enough; there wasn't enough labeled data or compute to let it show what it could do. It sat half-revived, a promising idea nobody could afford to run at scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Support Vector Machines, decision trees, and Bayesian methods&lt;/strong&gt; dominated practical machine learning, spam filters, recommendation engines, fraud detection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IBM's Deep Blue beat Garry Kasparov in 1997&lt;/strong&gt;, but largely through brute-force search over chess positions, not learning. Still narrow, still impressive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This era's intelligence was &lt;em&gt;statistical&lt;/em&gt; rather than &lt;em&gt;logical&lt;/em&gt;: pattern recognition over labeled data. It worked well on narrow, well-defined tasks but needed mountains of hand-labeled examples and feature engineering done by humans. The "intelligence" was still mostly in the human designing the features the model was just fitting a curve.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Deep Learning Breaks the Ceiling (2012–2017)
&lt;/h2&gt;

&lt;p&gt;The ingredients for the next leap had been sitting around for a decade: more data (the internet), more compute (GPUs originally built for video games), and algorithmic tricks for training deeper networks without them collapsing into noise. None of this looked inevitable at the time. Most of the field had moved on from neural networks; deep nets were a fringe bet kept alive by a small number of labs who kept getting told they were wasting their careers.&lt;/p&gt;

&lt;p&gt;The spark was &lt;strong&gt;&lt;a href="https://papers.nips.cc/paper_files/paper/2012/hash/c399862d3b9d6b76c8436e924a68c45b-Abstract.html" rel="noopener noreferrer"&gt;AlexNet (2012)&lt;/a&gt;&lt;/strong&gt;, a deep convolutional neural network that crushed the ImageNet image-classification competition, slashing the error rate compared to the next-best approach. That one result told the field something important: stack enough layers, feed them enough data, and the network finds its own features, no human feature engineering required. It wasn't a smooth continuation of the field's direction; it was closer to a coup. Within a couple of years, techniques that had been a punchline became the default starting point for almost every computer vision paper.&lt;/p&gt;

&lt;p&gt;What followed was a five-year sprint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;**&lt;a href="https://arxiv.org/abs/1301.3781" rel="noopener noreferrer"&gt;Word2Vec (2013)&lt;/a&gt; / &lt;a href="https://aclanthology.org/D14-1162/" rel="noopener noreferrer"&gt;GloVe (2014)**&lt;/a&gt;: words became vectors, and "meaning" became something you could do arithmetic on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://arxiv.org/abs/1406.2661" rel="noopener noreferrer"&gt;Generative Adversarial Networks (2014)&lt;/a&gt;&lt;/strong&gt;: two networks competing, one generating fakes, one detecting them, together learning to produce eerily convincing images.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.nature.com/articles/nature16961" rel="noopener noreferrer"&gt;AlphaGo (2016)&lt;/a&gt;&lt;/strong&gt;: beat the world's best Go player (the board game, not &lt;code&gt;go run main.go&lt;/code&gt; energy) using deep learning plus reinforcement learning plus tree search, on a game once thought too intuitive for machines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of these was still task-bound, a vision model couldn't write a sentence, a Go-playing model couldn't recognize a cat — but the skill inside each one was no longer handed to it by a human; the model discovered its own representation of the problem. That shift in mechanism, more than any single result, is what made the next jump possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The Transformer and the Birth of "General-ish" Intelligence (2017–2022)
&lt;/h2&gt;

&lt;p&gt;In 2017, a Google paper titled &lt;a href="https://arxiv.org/abs/1706.03762" rel="noopener noreferrer"&gt;"Attention Is All You Need"&lt;/a&gt; introduced the &lt;strong&gt;transformer architecture&lt;/strong&gt;. Instead of processing text sequentially like older recurrent networks, transformers let every word attend to every other word at once. It was a better way to model sequences and it turned out to scale beautifully.&lt;/p&gt;

&lt;p&gt;That architectural choice, combined with the realization that you could pretrain a single giant model on a huge slice of the internet and then adapt it to almost any language task, produced the GPT lineage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GPT (2018) → GPT-2 (2019) → GPT-3 (2020)&lt;/strong&gt;: each generation showed that scaling up parameters and data kept producing qualitatively new abilities, not just marginal accuracy gains. GPT-3 could write code, translate, summarize, and hold a conversation, despite never being explicitly trained to do any of those things.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instruction tuning and RLHF (2021–2022)&lt;/strong&gt;: raw language models predict the next token; they don't inherently want to be &lt;em&gt;helpful&lt;/em&gt;. Techniques like reinforcement learning from human feedback turned raw next-token predictors into assistants that follow instructions and refuse harmful ones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://chat.openai.com/" rel="noopener noreferrer"&gt;ChatGPT (November 2022)&lt;/a&gt;&lt;/strong&gt;: &lt;a href="https://twitter.com/sama/status/1598038815599661056" rel="noopener noreferrer"&gt;Sam Altman announced it on X&lt;/a&gt; with the energy of a minor release: "today we launched ChatGPT. try talking with it here." Understated copy for the moment the old era ended and a new history began. Research left the lab; your non-technical relatives showed up. A hundred million users in two months. The Turing Test stopped being a thought experiment and became something people ran into accidentally over breakfast.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the point where AI stopped meaning "deep in one box, blank everywhere else" and started meaning &lt;em&gt;compositional&lt;/em&gt;: a single set of weights that could combine skills it was never explicitly trained to combine. One model could write a sonnet, debug Python, and explain the sonnet's meter not because it was three different systems, but because language turned out to be a surprisingly good universal interface to a huge range of human tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. From Chatbot to Coworker: The Agentic Turn (2023–Today)
&lt;/h2&gt;

&lt;p&gt;A language model that just answers questions is a powerful autocomplete. The next phase of the story is about giving that model &lt;strong&gt;hands&lt;/strong&gt;: the ability to call tools, write and execute code, browse the web, remember state across steps, and chain its own reasoning into multi-step plans.&lt;/p&gt;

&lt;p&gt;A few threads converged here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tool use / function calling&lt;/strong&gt; let models stop just describing actions and start taking them, querying a database, hitting an API, running a calculation, instead of guessing at the answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval-augmented generation (RAG)&lt;/strong&gt; gave models access to information beyond their training data, grounding answers in real documents instead of frozen memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chain-of-thought and reasoning models&lt;/strong&gt; showed that letting a model "think out loud" before answering and eventually training it specifically to reason longer on hard problems, produced dramatically better results on math, logic, and multi-step planning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agentic frameworks&lt;/strong&gt; stitched these into loops: plan → act → observe → revise. Wrapped in orchestration code, retry logic, and well-designed tools, a model could chase a goal across many steps instead of answering once and stopping. Left alone long enough, it still drifts or takes wrong turns, the scaffolding exists to catch that. "Agent" describes a system, not a self-sufficient mind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-agent orchestration&lt;/strong&gt;: models that spin up &lt;em&gt;other&lt;/em&gt; model instances to parallelize work, each with a narrower role, then combine results. The specialist of the symbolic era is back, except now it's a transformer playing a role inside a swarm coordinated by another transformer, instead of a human-written rule.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the world a developer in 2026 actually lives in. Code isn't just suggested line-by-line; it's planned, written across multiple files, tested, and debugged in a loop that needs less hands-on steering than it used to, though still real review, real guardrails, and real human judgment about when to trust the output. The same pattern shows up outside coding: research agents that browse, synthesize, and cite sources across dozens of pages; operations agents that read a ticket, check a calendar, and draft a response; design agents that take a brief and return a working prototype. None of this is autonomy in the strong sense yet. It's interactive capability, a model in a loop with tools and a feedback signal — and it's powerful precisely because of how tightly that loop is engineered, not in spite of it.&lt;/p&gt;

&lt;p&gt;Easy to miss when you're staring at capability charts: every jump here was also an economics story. Expert systems died because expert time didn't scale. Statistical ML rode cheap labeled data and storage. Deep learning rode gaming GPUs. LLMs rode internet-scale data and transformer parallelism. Agents are having their moment because inference got cheap enough to run a model in a loop hundreds of times per task without laughing. The recurring question isn't "can we build it?" but it's "can we afford to run it enough times to be useful?" That bottleneck moved; it didn't disappear.&lt;/p&gt;

&lt;h2&gt;
  
  
  So What Changed, Really?
&lt;/h2&gt;

&lt;p&gt;If you zoom out, the history of AI is a story about &lt;em&gt;where the intelligence lives&lt;/em&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Era&lt;/th&gt;
&lt;th&gt;Where the "smarts" lived&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Symbolic AI&lt;/td&gt;
&lt;td&gt;In rules a human expert wrote by hand&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Statistical ML&lt;/td&gt;
&lt;td&gt;In features a human engineer chose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deep learning&lt;/td&gt;
&lt;td&gt;In representations the model learned itself&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large language models&lt;/td&gt;
&lt;td&gt;In patterns learned from most of the public internet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agentic systems&lt;/td&gt;
&lt;td&gt;In the model's own planning, tool use, and self-correction across time&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each era didn't replace the last so much as absorb it. Today's agents still do statistical pattern matching under the hood; they still occasionally fail in the brittle, overconfident ways the old expert systems did, just less often and less predictably.&lt;/p&gt;

&lt;p&gt;If you compress the five eras above, there are really only three discontinuities that mattered:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Hand-coded intelligence&lt;/strong&gt;: rules a human wrote (symbolic AI).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learned representations&lt;/strong&gt;: patterns a model found in data, with steadily less human-chosen structure (statistical ML → deep learning → LLMs).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive systems&lt;/strong&gt;: models that act, observe consequences, and revise, instead of just outputting a single answer (agents).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The five-era version tells a better story; the three-version compression is what actually changed underneath.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shift one: who writes the rules — human or data. &lt;/li&gt;
&lt;li&gt;Shift two: passive answer vs. active loop. Most of "AI got so much better" traces to one of those.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't a straight march toward AGI. It's researchers repeatedly asking "what if the machine decided this part?" and finding that worked, once the economics finally allowed it.&lt;/p&gt;

&lt;p&gt;Whether the next chapter is "agents that reliably run entire businesses" or "another winter while the hype outpaces the engineering" is genuinely an open question and depending on who you ask, both are already happening at once.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>There Is No Perfect Solution in Software Development: Every Decision is a Tradeoff</title>
      <dc:creator>Adam - The Developer</dc:creator>
      <pubDate>Mon, 15 Jun 2026 06:22:48 +0000</pubDate>
      <link>https://dev.to/adamthedeveloper/there-is-no-perfect-solution-in-software-development-every-decision-is-a-tradeoff-136l</link>
      <guid>https://dev.to/adamthedeveloper/there-is-no-perfect-solution-in-software-development-every-decision-is-a-tradeoff-136l</guid>
      <description>&lt;p&gt;Most bad decisions in software engineering aren't made because the engineer chose wrong between two clear options. They're made because the engineer didn't realize they were making a choice at all.&lt;/p&gt;

&lt;p&gt;You optimized for readability without noticing that particular loop runs a million times per second. You built for perfect flexibility without realizing you'd never actually need it. You shipped fast and acknowledged the technical debt—until the debt became someone else's problem, and by then it was too late to refactor.&lt;/p&gt;

&lt;p&gt;The pattern: teams usually aren't choosing between two good options. They're choosing between one safe default and one speculative optimization, and calling it a tradeoff to feel better about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What Every Engineer Needs to Know About Tradeoffs&lt;/li&gt;
&lt;li&gt;
Classic Tradeoffs You'll Face

&lt;ul&gt;
&lt;li&gt;Performance vs. Readability&lt;/li&gt;
&lt;li&gt;Flexibility vs. Simplicity&lt;/li&gt;
&lt;li&gt;Speed to Market vs. Technical Debt&lt;/li&gt;
&lt;li&gt;Scalability vs. Cost&lt;/li&gt;
&lt;li&gt;Security vs. Convenience&lt;/li&gt;
&lt;li&gt;The CAP Theorem: What It Actually Means&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The Pattern: How Good Decisions Actually Get Made&lt;/li&gt;
&lt;li&gt;How to Make Better Tradeoff Decisions&lt;/li&gt;
&lt;li&gt;The Mark of Experience&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Every Engineer Needs to Know About Tradeoffs
&lt;/h2&gt;

&lt;p&gt;There is no perfect solution. Every architecture, every algorithm, every design decision buys you something at the cost of something else. This isn't pessimism, it's the foundation of good engineering judgment.&lt;/p&gt;

&lt;p&gt;But here's the catch: not all tradeoffs are 50/50. In practice, experienced engineers often default heavily one way and only deviate with evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Classic Tradeoffs You'll Face
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Performance vs. Readability
&lt;/h3&gt;

&lt;p&gt;Here's what usually wins: readability. Unless you have a proven bottleneck backed by profiling data, optimize for clarity.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Faster but harder to maintain&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Slower but immediately clear&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;source&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second version is probably fine. Premature optimization is still the root of all evil. The first example should only exist if: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;you've profiled&lt;/li&gt;
&lt;li&gt;you found this function is actually slow&lt;/li&gt;
&lt;li&gt;you've measured that the optimization makes a meaningful difference&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;All three conditions are rarer than you'd think.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Flexibility vs. Simplicity
&lt;/h3&gt;

&lt;p&gt;"Flexibility" often looks like smart future-proofing until you realize you'll never actually need it.&lt;/p&gt;

&lt;p&gt;You build a generic plugin system because "we might need it." You abstract everything into interfaces because "we'll probably want to swap implementations." You create configuration options for scenarios that never materialize. Meanwhile, your simple code that handles exactly one thing is getting buried under layers of generality.&lt;/p&gt;

&lt;p&gt;Ship the simple thing. If you actually need multiple use cases later, refactoring from concrete to generic is almost always easier than refactoring from over-engineered to usable. The exception: if you're building a library or platform that multiple teams depend on, flexibility becomes a real requirement, not speculation.&lt;/p&gt;

&lt;p&gt;Most over-engineering is just ego disguised as foresight.&lt;/p&gt;

&lt;h3&gt;
  
  
  Speed to Market vs. Technical Debt
&lt;/h3&gt;

&lt;p&gt;Ship in two weeks with known compromises? Or spend two months building something maintainable? &lt;/p&gt;

&lt;p&gt;Both answers are right in different contexts. A startup with three months of runway and a saturated market needs speed. A fintech system handling billions in transactions needs stability. There's no universal answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scalability vs. Cost
&lt;/h3&gt;

&lt;p&gt;Premature scalability is usually just waste unless you have strong signals of growth.&lt;/p&gt;

&lt;p&gt;You can architect for 100x your current traffic and be right. You can also architect for today's load and be right. The difference is that one cost money now, and the other might cost money later. Most teams choose wrong because they're optimizing for an imagined future instead of the constraints they actually face.&lt;/p&gt;

&lt;p&gt;The right call: scale when you have evidence that growth is coming, not because it &lt;em&gt;might&lt;/em&gt; happen. Growth that doesn't materialize? You've spent months and money on infrastructure that will never be used. Growth that does materialize and catches you off-guard? That's painful, but you'll fix it. The fix is usually cheaper than over-engineering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you are unsure whether you need scalability, you don't.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Security vs. Convenience
&lt;/h3&gt;

&lt;p&gt;Require 2FA, complex passwords, and proof of identity? That's more secure. Users will also hate you.&lt;/p&gt;

&lt;p&gt;Frictionless auth is delightful for users and a security nightmare. You're balancing two legitimate concerns.&lt;/p&gt;

&lt;h3&gt;
  
  
  The CAP Theorem: What It Actually Means
&lt;/h3&gt;

&lt;p&gt;Everything above applies to a single application you control end to end. This one is different — it belongs to &lt;strong&gt;distributed systems&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A distributed system is software spread across multiple machines that coordinate over a network. Your API talking to one database is distributed in the loose sense, but CAP really starts to matter when the same data lives in more than one place: a primary with read replicas, a Redis cluster, a multi-region deployment. The moment you have copies of data on separate nodes, connected by a network that can fail, you inherit tradeoffs you do not get on a single server.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;CAP theorem&lt;/strong&gt; names the central one. The textbook version says: "Pick any two of Consistency, Availability, and Partition Tolerance."&lt;/p&gt;

&lt;p&gt;Here's what actually happens: In any real distributed system, &lt;strong&gt;partition tolerance is not optional&lt;/strong&gt;. Network failures will happen. So the real choice is between &lt;strong&gt;Consistency and Availability when the network breaks&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CP systems&lt;/strong&gt; fail safely when they can't guarantee consistency. A banking app might do this, it's better to be down than serve wrong balances.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AP systems&lt;/strong&gt; stay up and serve whatever data they have. A social feed might do this, slightly stale likes are better than a 503.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The code doesn't need to be complicated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CP: fail rather than serve wrong data&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getBalance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT balance FROM accounts WHERE id = $1&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="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// If database is unreachable, the request fails. That's the design.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// AP: serve cached data with a staleness flag&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getBalance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;stale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT balance FROM accounts WHERE id = $1&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="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stale&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;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Partition: return whatever we have cached&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;stale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Choose based on what breaks is worse: being wrong (CP) or being unavailable (AP).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pattern: How Good Decisions Actually Get Made
&lt;/h2&gt;

&lt;p&gt;Once you recognize that a tradeoff exists, you're already halfway to making a good decision. You can be intentional about what you're trading and why. You can explain it to your team. You can revisit it later if your constraints change.&lt;/p&gt;

&lt;p&gt;Without that awareness? You end up optimizing for readability on a loop that runs a million times per second. You build flexibility you'll never need. You ship fast and defer the cost to future-you.&lt;/p&gt;

&lt;p&gt;Here's what separates functional teams from dysfunctional ones: functional teams argue about &lt;em&gt;which&lt;/em&gt; tradeoff they're making. Dysfunctional teams don't realize there's a choice at all, and they compound the costs by pretending it was inevitable.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Make Better Tradeoff Decisions
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Name the tradeoff explicitly&lt;/strong&gt;&lt;br&gt;
Don't say "should we use Redis?" Say "are we optimizing for speed or operational simplicity?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Understand your constraints&lt;/strong&gt;&lt;br&gt;
What actually matters in your context? A library used by millions needs different tradeoffs than an internal tool used by five people.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Make it reversible if possible (but know the real limits)&lt;/strong&gt;&lt;br&gt;
Refactoring from concrete to generic is usually easier than the reverse. Local code changes are easy to undo. But reversibility has hard boundaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;True reversibility&lt;/strong&gt;: Internal implementation details, local code scope, nothing that touches user-facing behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;False reversibility&lt;/strong&gt;: Anything that becomes business-critical, anything customers build workflows around, anything that spreads to multiple teams. A quick hack that ships and immediately gets embedded in product behavior doesn't refactor cleanly six months later. The team has built processes around it. Customers depend on it. Other engineers have written code that relies on it. Reversibility was an illusion from the moment the code touched production.&lt;/p&gt;

&lt;p&gt;Know which category your decision falls into before you ship it as temporary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Document your reasoning&lt;/strong&gt;&lt;br&gt;
Future you (and your teammates) will thank you. "We chose simple over performant here because X" is gold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Revisit your assumptions&lt;/strong&gt;&lt;br&gt;
Your constraints change. What was the right tradeoff six months ago might be wrong now. That's not failure—that's growth.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mark of Experience
&lt;/h2&gt;

&lt;p&gt;Here's what changes as you get better at this:&lt;/p&gt;

&lt;p&gt;Seniors aren't less stressed, they're stressed about the right things. They don't waste energy trying to eliminate uncertainty. They can't. Uncertainty is part of the job. Instead, they focus on &lt;strong&gt;reducing risk and keeping decisions reversible where possible&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;They ask good questions: "What happens if we're wrong about this?" "What would make us want to undo this decision?" "Do we have evidence this is actually a bottleneck?" They make explicit choices, document the tradeoffs, and move forward without second-guessing.&lt;/p&gt;

&lt;p&gt;The skill isn't knowing everything. It's &lt;strong&gt;making conscious tradeoffs and living with the consequences&lt;/strong&gt; without pretending there was ever a perfect choice.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>software</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
    <item>
      <title>The Hidden Machinery Behind Background Jobs</title>
      <dc:creator>Adam - The Developer</dc:creator>
      <pubDate>Tue, 09 Jun 2026 07:27:48 +0000</pubDate>
      <link>https://dev.to/adamthedeveloper/the-hidden-machinery-behind-background-jobs-1g5a</link>
      <guid>https://dev.to/adamthedeveloper/the-hidden-machinery-behind-background-jobs-1g5a</guid>
      <description>&lt;p&gt;This will be a boring article today, but I wanted to write it after talking to a developer who genuinely believed background jobs are the key to faster software.&lt;/p&gt;

&lt;p&gt;They’re not hard to add. They’re easy to write, easy to deploy, and deceptively minimal, depending on how much complexity you’re willing to ignore elsewhere.&lt;/p&gt;

&lt;p&gt;Let’s talk about something most of us use every day but rarely think about.&lt;/p&gt;

&lt;p&gt;You enqueue a job (or, in my language, “drop something into a queue”). Your API returns &lt;code&gt;202 Accepted&lt;/code&gt; in 3 milliseconds. It feels good, responsive, non-blocking, event-driven. All the right words.&lt;/p&gt;

&lt;p&gt;But between &lt;code&gt;queue.enqueue()&lt;/code&gt; and a worker picking it up, a lot happens across your app, the OS, the network stack, and the broker. Most of it is invisible. All of it matters when something breaks at 2am.&lt;/p&gt;

&lt;p&gt;This is that journey, layer by layer. By the end, you’ll have a clearer picture of what “fast” and “non-blocking” actually mean when a queue is involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;What We Think Is Happening&lt;/li&gt;
&lt;li&gt;Layer 1: Your Application Code&lt;/li&gt;
&lt;li&gt;Layer 2: The Network Stack and the Kernel&lt;/li&gt;
&lt;li&gt;
Layer 3: Inside the Broker

&lt;ul&gt;
&lt;li&gt;Redis&lt;/li&gt;
&lt;li&gt;RabbitMQ&lt;/li&gt;
&lt;li&gt;Kafka&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
Layer 4: The Event Loop, and What Is Actually Powering It

&lt;ul&gt;
&lt;li&gt;How It Evolved&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Layer 5: The Worker, Acknowledgments, and What "At Least Once" Actually Means&lt;/li&gt;
&lt;li&gt;Layer 6: The Things That Quietly Go Wrong&lt;/li&gt;
&lt;li&gt;What "Fast" Actually Means&lt;/li&gt;
&lt;li&gt;The Full Journey, Summarized&lt;/li&gt;
&lt;li&gt;Closing Thoughts&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What We Think Is Happening
&lt;/h2&gt;

&lt;p&gt;Most of us carry a mental model that looks roughly 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;Producer  →  [ Queue ]  →  Worker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean. Simple. And not wrong, exactly, just kinda incomplete. The full picture looks more like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Producer
  → serialize payload to bytes
  → write bytes to TCP socket
  → kernel buffers → NIC → wire
  → broker receives bytes
  → broker deserializes, routes, stores message
  → broker sends ACK to producer
  → worker connects and subscribes
  → broker reads message from storage
  → broker sends to worker over TCP
  → worker deserializes → your function runs
  → worker sends ACK back to broker
  → broker marks message delivered, removes it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of those arrows is real work. Let's walk through each one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 1: Your Application Code
&lt;/h2&gt;

&lt;p&gt;Here is a fairly typical job enqueue in TypeScript using BullMQ:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Queue&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bullmq&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;emailQueue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emails&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;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;localhost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6379&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;emailQueue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sendWelcome&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;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;usr_abc123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;welcome-v2&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Job enqueued&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Returns in ~3ms&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;await&lt;/code&gt; resolves fast. But a few things happened before it did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Serialization.&lt;/strong&gt; Your payload object got converted to a flat sequence of bytes. In BullMQ's case, this is JSON. Every nested field, every string, every number, reduced to a string that can cross a process boundary. Your clean TypeScript object does not survive the trip; its data does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Message wrapping.&lt;/strong&gt; The payload gets wrapped in an envelope before it goes anywhere. Job ID, timestamp, retry count, delay, priority, queue name. Depending on the broker, this metadata overhead can be comparable in size to your actual payload for small messages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connection reuse.&lt;/strong&gt; BullMQ does not open a new TCP connection to Redis for every job. It keeps a pool of connections alive. Enqueuing borrows one of those connections, writes the message bytes, and returns it. If every connection in the pool is busy, your &lt;code&gt;await&lt;/code&gt; waits here. Not in some clean async queue, but blocked, waiting for a connection to free up.&lt;/p&gt;

&lt;p&gt;So even before a byte has left your machine, your "fast, non-blocking enqueue" has already done serialization, object allocation, pool management, and potentially some waiting. Still fast. Just not free.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 2: The Network Stack and the Kernel
&lt;/h2&gt;

&lt;p&gt;When the library writes to the socket, your application hands control to the OS kernel. This boundary has a name: a &lt;strong&gt;syscall&lt;/strong&gt;. Roughly speaking, it is your code saying "I need to do something I am not allowed to do myself" and the kernel taking over.&lt;/p&gt;

&lt;p&gt;On Linux, that write looks like this at the system level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// What your queue library is ultimately triggering&lt;/span&gt;
&lt;span class="kt"&gt;ssize_t&lt;/span&gt; &lt;span class="n"&gt;bytes_written&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket_fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message_length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things happen in sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your bytes are &lt;strong&gt;copied from your process's memory into the kernel's TCP send buffer&lt;/strong&gt;. This is a separate memory region. Once copied, your code cannot touch those bytes anymore.&lt;/li&gt;
&lt;li&gt;The kernel's TCP stack breaks the data into segments, adds sequence numbers and checksums, and hands them to the network driver.&lt;/li&gt;
&lt;li&gt;The NIC DMA-transfers the data to its own buffer and puts it on the wire.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;the important part here: &lt;code&gt;write()&lt;/code&gt; returns before the data is confirmed received by the broker. It returns when the kernel has accepted the bytes. The actual network transfer happens asynchronously behind your process's back.&lt;/p&gt;

&lt;p&gt;When your enqueue call returns quickly, you have confirmed the kernel accepted your bytes. Not that the broker received them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 3: Inside the Broker
&lt;/h2&gt;

&lt;p&gt;This is where things differ meaningfully depending on which broker you are using. Let's look at three common ones.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redis
&lt;/h3&gt;

&lt;p&gt;Redis works entirely in memory. When the message arrives over TCP, Redis's event loop reads the bytes from the socket, deserializes the command, and modifies an in-memory data structure. For a BullMQ job, this is an entry in a Redis sorted set or list, depending on job type.&lt;/p&gt;

&lt;p&gt;No disk is touched in the default path. This is why Redis is fast. It is also the catch: a crash or restart with default settings means everything in memory is gone. Your enqueued jobs, vanished, with no error and no log entry.&lt;/p&gt;

&lt;p&gt;Redis does support persistence (RDB snapshots, AOF logging), but these are asynchronous by default. There is always a small window between when you enqueue and when that data is safely written to disk. That window is your risk to size and accept consciously.&lt;/p&gt;

&lt;h3&gt;
  
  
  RabbitMQ
&lt;/h3&gt;

&lt;p&gt;RabbitMQ has a more layered approach to storage. A message arrives via AMQP, gets deserialized, passes through the exchange's routing logic (which queue does this message belong to, based on binding keys and patterns), and lands in the target queue.&lt;/p&gt;

&lt;p&gt;Each message carries more weight than it looks. A payload of 1KB actually occupies roughly 2KB in RabbitMQ's memory once internal metadata is factored in. The internal database keeps an in-memory copy of all data, even on disk nodes.&lt;/p&gt;

&lt;p&gt;For persistent messages, RabbitMQ writes to disk before sending the ACK back to your producer. If memory pressure climbs above a configurable threshold, RabbitMQ starts paging messages to disk. If it keeps climbing, RabbitMQ does something that surprises most people who have not read the docs: it &lt;strong&gt;blocks publishers&lt;/strong&gt;. Stops reading from their TCP sockets entirely. Your producer's &lt;code&gt;write()&lt;/code&gt; call hangs. Your enqueue appears to freeze. The queue is not broken. It is telling you it is full and it cannot accept more until workers catch up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kafka
&lt;/h3&gt;

&lt;p&gt;Kafka treats the problem differently at a fundamental level. A message is not stored in a data structure in memory. It is &lt;strong&gt;appended sequentially to a log file on disk&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Sequential disk writes on modern SSDs and NVMe drives are extremely fast because they avoid seek times entirely. This is where much of Kafka's throughput comes from. Consumer progress is tracked as a simple integer, an offset into that log file. To read the next message, a consumer increments its offset and fetches. To replay messages, it resets the offset.&lt;/p&gt;

&lt;p&gt;This model means multiple independent consumers can read the same data, message replay is trivially possible, and retention is time-based rather than acknowledgment-based. It also means Kafka is disk-heavy and its throughput degrades with larger messages. NVMe storage is not optional for serious Kafka deployments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 4: The Event Loop, and What Is Actually Powering It
&lt;/h2&gt;

&lt;p&gt;This is the part that ties everything together. Your broker, your async worker, your web framework: all of them are built on the same foundational mechanism.&lt;/p&gt;

&lt;p&gt;To understand it, we need to understand the problem it exists to solve.&lt;/p&gt;

&lt;p&gt;A server that handles connections by blocking on a socket read can only handle one connection per thread. To handle 10,000 concurrent connections, you would need 10,000 threads. A thread typically needs 1 to 8 MB of stack space. That is a lot of RAM spent on processes that are almost entirely idle, just waiting for data.&lt;/p&gt;

&lt;p&gt;The solution is &lt;strong&gt;I/O multiplexing&lt;/strong&gt;: a single thread monitors thousands of file descriptors and reacts only when one actually has data.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It Evolved
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;select()&lt;/code&gt; was Unix's first answer. You pass the kernel a bitmask of file descriptors to watch. The kernel scans every one of them on every call to see which are ready:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// kernel scans ALL of these on every single call&lt;/span&gt;
&lt;span class="n"&gt;fd_set&lt;/span&gt; &lt;span class="n"&gt;read_fds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;FD_ZERO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;read_fds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;FD_SET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sock1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;read_fds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;FD_SET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sock2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;read_fds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// ... up to 1024 max&lt;/span&gt;

&lt;span class="n"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_fd&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;read_fds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// returns, then you scan the whole bitmask yourself to find who is ready&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is O(n) per call, where n is your total number of watched file descriptors. With thousands of connections, this burns meaningful CPU just on scanning. Plus there is a hard ceiling of 1,024 descriptors. Fine for an early-nineties Unix workstation. Not fine for anything built in the last twenty years.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;poll()&lt;/code&gt; removed the 1,024 limit but kept the linear scan. A step forward, not a solution.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;epoll()&lt;/code&gt; is the modern Linux answer. The philosophy shifts: instead of asking the kernel "which of these are ready right now?", you register your file descriptors once and say "notify me when something changes."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// create an epoll instance&lt;/span&gt;
&lt;span class="c1"&gt;// the kernel allocates a red-black tree and a ready list internally&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;epfd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;epoll_create1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// register a file descriptor once&lt;/span&gt;
&lt;span class="c1"&gt;// kernel inserts it into the red-black tree — O(log n)&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;epoll_event&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EPOLLIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// wake me when data is available to read&lt;/span&gt;
&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;socket_fd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;epoll_ctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EPOLL_CTL_ADD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket_fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// The event loop — this is what Node.js, Redis, Nginx all do at their core&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;epoll_event&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;MAX_EVENTS&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// process sleeps here, consuming zero CPU&lt;/span&gt;
    &lt;span class="c1"&gt;// Kernel wakes it when at least one FD has data&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;epoll_wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MAX_EVENTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// n is only the count of *ready* file descriptors, not total registered ones&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;handle_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;epoll_create1()&lt;/code&gt; is called, the kernel allocates two internal structures: a &lt;strong&gt;red-black tree&lt;/strong&gt; for all registered file descriptors (insertion and lookup in O(log n)), and a &lt;strong&gt;linked list&lt;/strong&gt; for descriptors that currently have data ready.&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;epoll_wait()&lt;/code&gt; is called, the process is suspended and consumes no CPU.&lt;/p&gt;

&lt;p&gt;When a network packet arrives for one of your registered sockets, the NIC fires an interrupt. The kernel processes the incoming data and a &lt;strong&gt;callback&lt;/strong&gt; moves that socket's file descriptor from the red-black tree to the ready list. &lt;code&gt;epoll_wait()&lt;/code&gt; wakes up and returns only the descriptors that are ready.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;select&lt;/code&gt;, the kernel scans your entire list on every call regardless of how many are actually ready. With &lt;code&gt;epoll&lt;/code&gt;, the kernel does the bookkeeping internally and only tells you about the ones you care about. For a server with 10,000 connected clients where 3 of them sent data right now, &lt;code&gt;select&lt;/code&gt; scans 10,000, &lt;code&gt;epoll&lt;/code&gt; returns 3.&lt;/p&gt;

&lt;p&gt;This is why:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A single-threaded Node.js process can handle tens of thousands of concurrent connections.&lt;/li&gt;
&lt;li&gt;Redis processes millions of operations per second on one thread.&lt;/li&gt;
&lt;li&gt;Nginx serves enormous traffic without spawning thousands of OS threads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your event-driven architecture is, at its foundation, a process sleeping in the kernel, waiting to be woken by a network interrupt.&lt;/p&gt;




&lt;h2&gt;
  
  
  Layer 5: The Worker, Acknowledgments, and What "At Least Once" Actually Means
&lt;/h2&gt;

&lt;p&gt;Below is a TypeScript worker consuming from the same queue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Worker&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bullmq&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;emails&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="c1"&gt;// Ii we reach here without throwing, BullMQ sends the ACK automatically&lt;/span&gt;
  &lt;span class="c1"&gt;// if we throw, BullMQ sends a NACK and schedules a retry&lt;/span&gt;

&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;localhost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6379&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;concurrency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// how many jobs this worker handles in parallel&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple from the outside. Under the hood, the worker process opened a TCP connection to Redis, subscribed to the queue, and entered its own event loop. From the OS's perspective, it is a sleeping process blocked on a socket read, parked in the kernel's wait queue, consuming no CPU.&lt;/p&gt;

&lt;p&gt;When Redis has a job ready, it sends data over TCP. The kernel receives the bytes, the socket becomes readable, the wait queue callback fires, the process wakes, BullMQ deserializes the job data, and your function is called.&lt;/p&gt;

&lt;p&gt;When your function returns without throwing, BullMQ sends an ACK to Redis. Redis marks the job complete. If your function throws before that ACK is sent, the job stays in an active state. After a configurable timeout, it gets requeued.&lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;at-least-once delivery&lt;/strong&gt;. Your function will be called at minimum once. It may be called more than once. Here is why that matters in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Unsafe: if this crashes after the charge but before the receipt,&lt;/span&gt;
&lt;span class="c1"&gt;// the job retries and the customer gets charged twice&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processPaymentJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;chargeCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendReceipt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Safer: the idempotency key ensures a duplicate call&lt;/span&gt;
&lt;span class="c1"&gt;// is treated as the same operation, not a new one&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processPaymentJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;chargeCustomer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;customerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Idempotency is not a nice-to-have in a queue-based system. &lt;/span&gt;
    &lt;span class="c1"&gt;// It is a correctness requirement for any handler that makes side effects in the outside world.&lt;/span&gt;
    &lt;span class="na"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendReceipt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&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;h2&gt;
  
  
  Layer 6: The Things That Quietly Go Wrong
&lt;/h2&gt;

&lt;p&gt;Now that we can see the full path, these failure modes are a lot less surprising.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The queue fills up and nobody is watching.&lt;/strong&gt; Workers fall behind. Messages pile up. RabbitMQ reaches its memory threshold and stops accepting new messages. Your producers' TCP writes start hanging. Your API response times climb. Engineers look for a slow database query. Add queue depth and consumer lag to your dashboards and save everyone a confusing hour.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data loss because persistence was not configured.&lt;/strong&gt; Redis with default settings holds everything in memory. A clean deployment restart flushes the queue. No error is thrown because nothing went wrong from the broker's perspective. The data simply stops existing. Enable AOF persistence and understand the durability tradeoff you are making before your first production incident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The dead letter queue fills up unnoticed.&lt;/strong&gt; A dead letter queue is where failed jobs end up after repeated retries. It’s easy to ignore because nothing actively lives there by default. But that’s exactly the problem. If you don’t build a consumer for it, and don’t monitor its depth, you can quietly accumulate months of failed work without realizing it. By the time you notice, you’re just gonna be digging through history of dead queues.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Duplicate processing because the handler is not idempotent.&lt;/strong&gt; A worker completes its work, then crashes before ACKing. The broker redelivers. The external side effect runs twice. This has caused real double-charges, duplicate emails, and inconsistent records at real companies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A blocking call inside an async worker.&lt;/strong&gt; If your handler makes a synchronous blocking call, it stalls the event loop for the duration. No other messages are processed. Throughput drops to the speed of the slowest synchronous operation. Use async drivers for databases, HTTP clients, and file I/O inside workers.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "Fast" Actually Means
&lt;/h2&gt;

&lt;p&gt;When we say a queue makes an endpoint faster, we mean something specific: &lt;strong&gt;the HTTP response no longer waits for the work to finish&lt;/strong&gt;. The work itself takes the same amount of time. Often more, when you factor in serialization, network round trips to the broker, storage, retrieval, deserialization, and worker execution.&lt;/p&gt;

&lt;p&gt;What changed is when the user gets their response. Not how long the work takes.&lt;/p&gt;

&lt;p&gt;This is genuinely valuable. Decoupling user-facing latency from background work is a real improvement. But it is a different thing than making work faster, and conflating the two leads to queues in places that add complexity without benefit, and missing queues in places where the decoupling would genuinely help.&lt;/p&gt;

&lt;p&gt;A queue earns its place when the work is not needed to form the response, the work is slow enough that waiting feels bad, the work is safe to retry, and the producer and consumer need to scale independently.&lt;/p&gt;

&lt;p&gt;A queue adds complexity without clear payoff when you need the result to respond, your latency budget is too tight for broker round trips, or the operation is fast enough that doing it inline is simpler and easier to observe.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Journey, Summarized
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your TypeScript code
  ↓  JSON.stringify → bytes allocated in your process memory
  ↓  write() syscall → bytes copied to kernel TCP send buffer
  ↓  kernel TCP stack → NIC DMA transfer → wire

Broker process (Redis / RabbitMQ / Kafka)
  ↓  epoll_wait() wakes on incoming socket data
  ↓  deserialize bytes → route → store (RAM / disk / log file)
  ↓  ACK sent back to producer over TCP

Worker process
  ↓  sleeping in kernel wait queue, zero CPU consumption
  ↓  broker pushes job data over TCP
  ↓  kernel wakes worker via socket callback
  ↓  deserialize → your async handler function runs
  ↓  ACK on success → broker removes job
  ↓  throw on failure → broker requeues after timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every arrow is real work with real cost and real failure modes. None of it is magic. All of it is understandable, and understanding it makes you a meaningfully better engineer of the systems that depend on it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;Queues and background jobs are genuinely good tools. The resilience they add, the ability to absorb traffic spikes, retry failures gracefully, and let producers and consumers scale independently — all of that is real and worth the added complexity.&lt;/p&gt;

&lt;p&gt;The point is not to be wary of them. It is to understand what you are working with so you can configure them thoughtfully, monitor the right things, and know where to look when something behaves unexpectedly.&lt;/p&gt;

&lt;p&gt;The queue depth is a metric worth watching. The dead letter queue is an inbox worth reading. The ACK timeout is a contract with the broker worth understanding.&lt;/p&gt;

&lt;p&gt;Keep an eye on those three and your queues will mostly be a quiet, reliable part of your system doing exactly what they promised.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>architecture</category>
      <category>typescript</category>
      <category>backend</category>
    </item>
    <item>
      <title>When Duplicate Code Is the Better Design</title>
      <dc:creator>Adam - The Developer</dc:creator>
      <pubDate>Mon, 01 Jun 2026 06:09:57 +0000</pubDate>
      <link>https://dev.to/adamthedeveloper/when-duplicate-code-is-the-better-design-1idk</link>
      <guid>https://dev.to/adamthedeveloper/when-duplicate-code-is-the-better-design-1idk</guid>
      <description>&lt;p&gt;You've seen the developer. Maybe you &lt;em&gt;are&lt;/em&gt; the developer.&lt;/p&gt;

&lt;p&gt;They discover DRY — ✨ &lt;strong&gt;Don't Repeat Yourself&lt;/strong&gt; ✨ — and something switches in their brain. A primal need awakens. Every duplicated string, every similar-looking function, every pair of lines that rhyme in the wrong light becomes a &lt;strong&gt;personal affront&lt;/strong&gt;. An itch. A moral failing.&lt;/p&gt;

&lt;p&gt;Two weeks later, their codebase looks like a game of Jenga where every piece is also load-bearing, also abstract, also parametrized six ways to Sunday, and also, crucially, &lt;strong&gt;completely impossible to understand&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Congratulations. You've achieved ✨ DRY ✨. You've also achieved a codebase that will ruin your next three Fridays and everyone's.&lt;/p&gt;




&lt;h2&gt;
  
  
  What DRY Actually Says (and Doesn't)
&lt;/h2&gt;

&lt;p&gt;DRY comes from &lt;em&gt;The Pragmatic Programmer&lt;/em&gt; by Andy Hunt and Dave Thomas. The actual rule is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Every piece of &lt;strong&gt;knowledge&lt;/strong&gt; must have a single, unambiguous, authoritative representation within a system."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Notice what it says: &lt;strong&gt;knowledge&lt;/strong&gt;. it's not &lt;em&gt;code&lt;/em&gt;. it's not &lt;em&gt;characters&lt;/em&gt;. it's not &lt;em&gt;"things that look similar when squinted at from across the room."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It says knowledge.&lt;/p&gt;

&lt;p&gt;The principle is about avoiding duplicated &lt;em&gt;intent and logic&lt;/em&gt; — not scrubbing every repeated character like you're laundering evidence. But somewhere between the book and the keyboard, people stopped reading and started pattern-matching like raccoons sorting shiny garbage:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If two lines of code look alike, merge them immediately or you're morally bankrupt, professionally suspect, and probably the reason standups run long."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's not DRY. That's &lt;strong&gt;aesthetic OCD with a philosophy degree and a GitHub contribution graph to protect&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Taxonomy of DRY Crimes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Crime #1: Abstracting Coincidental Similarity
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Two functions that happen to look the same TODAY&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatUserName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatAuthorName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;author&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The classic DRY-brain response: &lt;em&gt;"These are identical! Extract!"&lt;/em&gt;&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="c1"&gt;// The abstraction&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;formatName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firstName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lastName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fine. Harmless, even. Like a campfire in dry grass. Six months later, user names want a salutation, author names want a pen name fallback, and your cute little &lt;code&gt;formatName&lt;/code&gt; has metastasized into a fifteen-parameter horror show with an enum called &lt;code&gt;NameFormattingStrategy&lt;/code&gt; — the kind of function that needs its own onboarding doc and makes junior devs reconsider their career choices.&lt;/p&gt;

&lt;p&gt;The duplication wasn't a bug. It was &lt;strong&gt;two different things that happened to share a shape for one Tuesday&lt;/strong&gt;. A user is not an author. Their names evolve on different timelines. The duplicate code was whispering that — you heard "refactor opportunity" and built a cage instead.&lt;/p&gt;




&lt;h3&gt;
  
  
  Crime #2: The Abstraction That Needs a Manual
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: readable in 3 seconds&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getActiveAdminUsers&lt;/span&gt;&lt;span class="p"&gt;(&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isActive&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&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="c1"&gt;// After: DRY'd into a puzzle box&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;filterEntities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;predicates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sortBy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;transform&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;filtered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;predicates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pred&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;pred&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sorted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sortBy&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;filtered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sortBy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;filtered&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;limited&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;transform&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;limited&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;limited&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage (good luck, new hire)&lt;/span&gt;
&lt;span class="nf"&gt;filterEntities&lt;/span&gt;&lt;span class="p"&gt;(&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="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isActive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&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;You saved four lines. You created a &lt;code&gt;filterEntities&lt;/code&gt; function that now silently accumulates every filtering need in your entire app, grows to 200 lines, and gets passed to new developers with the haunted look of someone handing off a cursed object.&lt;/p&gt;

&lt;p&gt;The original function had a &lt;strong&gt;name&lt;/strong&gt;. It was easy to understand, it told a simple story, &lt;code&gt;getActiveAdminUsers&lt;/code&gt; is self-documenting. Your generalized thing is a puzzle.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Code is read far more than it's written. Abstractions are not free — they are paid for in comprehension, every single time someone opens that file.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Crime #3: DRY Across Wrong Boundaries
&lt;/h3&gt;

&lt;p&gt;The most insidious form. And unlike the toy examples above, this one has a body count.&lt;/p&gt;

&lt;p&gt;Every backend dev has seen this happen. You're building a notification system. You have email, SMS, push, and in-app notifications. They all take a user, a type, and some options. They look identical at the call site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// v1 — reasonable. clean. you feel good about yourself.&lt;/span&gt;
&lt;span class="nf"&gt;sendNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order_confirmed&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="nx"&gt;orderId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The abstraction makes sense. You write one function. You route internally. Ship it.&lt;/p&gt;

&lt;p&gt;Then marketing wants to send a promotional email blast. Different thing, same function — but now you need an &lt;code&gt;isMarketing&lt;/code&gt; flag because marketing emails have unsubscribe footers and transactional ones don't.&lt;/p&gt;

&lt;p&gt;Then legal needs CAN-SPAM compliance on marketing sends. Different opt-out logic per locale. Now you need &lt;code&gt;locale&lt;/code&gt; and &lt;code&gt;complianceRules&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then mobile push notifications need to respect iOS quiet hours, which email doesn't care about. Add &lt;code&gt;respectQuietHours&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then in-app notifications don't go through any external service at all — they're just a database write — but they're still "notifications" so they stay in the function.&lt;/p&gt;

&lt;p&gt;Then SMS gets a character limit and needs message chunking logic that email will never need.&lt;/p&gt;

&lt;p&gt;Eighteen months after that clean &lt;code&gt;sendNotification(user, type, options)&lt;/code&gt;, you have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// v∞ — the function that consumed itself&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NotificationType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NotificationOptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sms&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;push&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;in_app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isMarketing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;isTransactional&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;complianceRules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ComplianceConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;respectQuietHours&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;chunkIfOverLimit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;bypassOptOut&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// "just this once, for the launch"&lt;/span&gt;
  &lt;span class="nx"&gt;trackingPixel&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;email&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isMarketing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// 40 lines of compliance logic&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// 30 lines of transactional logic&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sms&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="c1"&gt;// 50 lines that have nothing to do with email&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;push&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="c1"&gt;// 45 lines that have nothing to do with SMS&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;channel&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;in_app&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="c1"&gt;// just writes to a table but must pass through this gauntlet anyway&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;This function is now 300 lines. It has no tests that actually cover all the flag combinations — there are 2⁷ of them, good luck. Every engineer who touches it adds one parameter at the top and one &lt;code&gt;if&lt;/code&gt; branch somewhere in the middle, and prays they didn't break the Malaysian SMS path.&lt;/p&gt;

&lt;p&gt;You have not DRY'd your code. You have built a &lt;strong&gt;load-bearing monolith disguised as a helper function&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The actual fix is humbling: email, SMS, push, and in-app were never the same thing. They shared a surface-level interface and nothing else. They have different rate limits, different compliance regimes, different retry logic, different failure modes, and different delivery guarantees. The right design was always:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;sendSms&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;sendPushNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;createInAppNotification&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, it's four functions. Yes, some options are repeated across them. That's fine — the repetition is &lt;em&gt;honest&lt;/em&gt;. Each function can evolve independently. The SMS team can add chunking without touching the email function. Legal can update the CAN-SPAM logic without a regression on push. A new engineer can read &lt;code&gt;sendEmail&lt;/code&gt; and understand &lt;code&gt;sendEmail&lt;/code&gt; without holding the entire notification universe in their head.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The shared abstraction didn't eliminate complexity. It hid it, then grew it, then held it hostage.&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Crime #4: The Generic Repository That Ate Your Domain
&lt;/h3&gt;

&lt;p&gt;Every backend dev hits this phase. You have &lt;code&gt;UserService&lt;/code&gt;, &lt;code&gt;OrderService&lt;/code&gt;, &lt;code&gt;ProductService&lt;/code&gt;. They all need to fetch things from the database. The queries look suspiciously similar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Three services, three nearly identical queries. Your DRY alarm is screaming.&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;findAllUsers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;deletedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;findAllOrders&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;not&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft&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="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;findAllProducts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&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="nf"&gt;findMany&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;isPublished&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix writes itself. You extract a base class. You add generics. You feel like you're finally doing &lt;em&gt;real&lt;/em&gt; architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The abstraction that will haunt three teams&lt;/span&gt;
&lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;abstract&lt;/span&gt; &lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;findAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;filters&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;findOne&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="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&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="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;findPaginated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
      &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;filters&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserRepository&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;users&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;orders&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="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductRepository&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;BaseRepository&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;products&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Beautiful. Reusable. You deleted forty lines and posted about it in Slack. Life is good.&lt;/p&gt;

&lt;p&gt;Then product management asks for order search with customer name, date range, and payment status — but only for admins, and only orders that haven't been refunded unless the refund was partial. Your &lt;code&gt;findPaginated&lt;/code&gt; now accepts a &lt;code&gt;QueryOptions&lt;/code&gt; object with twelve optional fields and a &lt;code&gt;include&lt;/code&gt; array that half the team misconfigures.&lt;/p&gt;

&lt;p&gt;Then users need soft-delete scoping, role-based visibility, and a "last active" sort that requires a join. You override &lt;code&gt;findAll&lt;/code&gt; in &lt;code&gt;UserRepository&lt;/code&gt;. Then you override &lt;code&gt;findPaginated&lt;/code&gt; too. The base class is now mostly dead code that new hires still have to read.&lt;/p&gt;

&lt;p&gt;Then products need full-text search, category trees, and inventory counts from a warehouse table in another service. Someone adds &lt;code&gt;findPaginatedWithSearch&lt;/code&gt;. Someone else adds &lt;code&gt;findPaginatedWithJoins&lt;/code&gt;. The base class grows a &lt;code&gt;buildQuery&lt;/code&gt; hook, then a &lt;code&gt;QueryBuilder&lt;/code&gt; parameter, then an escape hatch called &lt;code&gt;rawQueryOverride&lt;/code&gt; that three repositories use and nobody documents.&lt;/p&gt;

&lt;p&gt;Eighteen months later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// v∞ — BaseRepository, but make it suffer&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;findPaginated&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Entity&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;FilterMap&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;include&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;IncludeMap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;joins&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;JoinConfig&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="nl"&gt;search&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;SearchConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;SortConfig&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;SortConfig&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="nl"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;internal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;softDelete&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;bypassTenantScope&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// "temporary, for the migration"&lt;/span&gt;
    &lt;span class="nl"&gt;aggregate&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;AggregateConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;unknown&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PaginatedResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;TransformedRow&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 180 lines of conditional query assembly&lt;/span&gt;
  &lt;span class="c1"&gt;// every repository passes a different options shape&lt;/span&gt;
  &lt;span class="c1"&gt;// the type signature is longer than most functions it replaces&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You have not eliminated duplication. You have built a &lt;strong&gt;generic pagination framework&lt;/strong&gt; that every entity in your system must awkwardly fit into, like forcing every piece of furniture through the same IKEA allen wrench.&lt;/p&gt;

&lt;p&gt;The honest version was always:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// UserRepository — boring, explicit, correct&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;findActiveUsersForAdmin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// OrderRepository — different domain, different query&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;findOrdersForDashboard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OrderDashboardFilters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ProductRepository — nobody else's problem&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;findPublishedProductsWithInventory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;categoryId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, they all paginate. Yes, they all query a database. That's &lt;strong&gt;infrastructure similarity&lt;/strong&gt;, not &lt;strong&gt;domain knowledge&lt;/strong&gt;. Users, orders, and products don't share a reason to change — they share a SQL dialect. Conflating those two is how you end up with a &lt;code&gt;BaseEntityManagerFactoryHelperUtil&lt;/code&gt; and a Jira ticket titled "refactor findPaginated (blocked: needs architect approval)."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pagination is not a domain concept. It's a transport detail. Your repository layer is not a place to build a query DSL because SELECT statements rhymed once.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Principle You Actually Need: AHA
&lt;/h2&gt;

&lt;p&gt;Sandi Metz — of &lt;em&gt;Practical Object-Oriented Design&lt;/em&gt; fame — offers the antidote:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AHA: Avoid Hasty Abstractions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The rule: &lt;strong&gt;prefer duplication over the wrong abstraction&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A wrong abstraction is worse than duplication because:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Duplication is visible and easy to fix&lt;/li&gt;
&lt;li&gt;A wrong abstraction is load-bearing, hard to see, and expensive to undo&lt;/li&gt;
&lt;li&gt;People are afraid to delete abstractions, so they pile parameters on top until it collapses&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The heuristic she and others suggest: &lt;strong&gt;wait for the third time&lt;/strong&gt;. Once — write it. Twice — note the repetition. Three times — &lt;em&gt;now&lt;/em&gt; think about what abstraction, if any, makes sense. By then you have enough examples to see the &lt;em&gt;actual&lt;/em&gt; shape of the knowledge, not just the accidental shape of the code.&lt;/p&gt;




&lt;h2&gt;
  
  
  When DRY Is Right
&lt;/h2&gt;

&lt;p&gt;To be fair to the principle: DRY is genuinely critical in the right places.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Business rules.&lt;/strong&gt; The formula for calculating interest, the rule for what makes a user "active," the threshold for triggering an alert — these must live in exactly one place. When the business logic changes, you want to change exactly one thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configuration.&lt;/strong&gt; A base URL hardcoded in twelve files is twelve places a typo can happen. That's rightfully DRY.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data schemas.&lt;/strong&gt; Your database schema and your validation schema should not diverge because you copy-pasted them. Derive one from the other.&lt;/p&gt;

&lt;p&gt;The test for whether something &lt;em&gt;should&lt;/em&gt; be DRY: &lt;strong&gt;if this thing needs to change, how many places need to change with it, and do those places share a reason to change?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If yes — DRY it.&lt;/p&gt;

&lt;p&gt;If they just look similar today — leave it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Real Anti-Pattern DRY Is Trying to Fight
&lt;/h2&gt;

&lt;p&gt;DRY's real enemy was never "duplicate code." It was &lt;strong&gt;duplicate knowledge&lt;/strong&gt; — the same business rule, scattered across the system, slowly drifting out of sync.&lt;/p&gt;

&lt;p&gt;The nightmare scenario: your pricing logic is in the database trigger, the API controller, the frontend calculation, and a comment in a Slack message from 2019. When pricing changes, you find three of them. The fourth one quietly disagrees for six months, occasionally giving users the wrong number, and you never know why.&lt;/p&gt;

&lt;p&gt;That's what DRY is for. Not for making your &lt;code&gt;formatName&lt;/code&gt; function 40% more reusable.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Checklist for Before You Abstract
&lt;/h2&gt;

&lt;p&gt;Before you extract that abstraction, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Is this duplicate &lt;em&gt;knowledge&lt;/em&gt;, or duplicate &lt;em&gt;shape&lt;/em&gt;?&lt;/strong&gt; Two things can look alike for different reasons.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can I name this abstraction clearly?&lt;/strong&gt; If you're reaching for &lt;code&gt;processEntityData&lt;/code&gt; or &lt;code&gt;handleThing&lt;/code&gt;, stop. The abstraction isn't real yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What happens when one usage changes?&lt;/strong&gt; If you can't change the abstraction without worrying about the other usages, it's too coupled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Am I doing this because it's right, or because repetition makes me uncomfortable?&lt;/strong&gt; Valid question. The answer matters.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;DRY is a heuristic, not a commandment. It was written to fight a specific enemy: knowledge duplication causing systems to rot. It was not written to make your codebase a shrine to abstraction.&lt;/p&gt;

&lt;p&gt;The best codebases I've read had a little repetition in them. Deliberate repetition. The kind where someone clearly decided "these two things look alike but they're not the same thing, and I'm going to leave them separate so they can evolve separately."&lt;/p&gt;

&lt;p&gt;That's not laziness. That's wisdom.&lt;/p&gt;

&lt;p&gt;Write the thing twice if you have to. Your future self, reading the code at 11pm with no context, will thank you for the clarity.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>programming</category>
      <category>productivity</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Minimal Code Doesn’t Mean Stable Code</title>
      <dc:creator>Adam - The Developer</dc:creator>
      <pubDate>Tue, 26 May 2026 04:15:08 +0000</pubDate>
      <link>https://dev.to/adamthedeveloper/minimal-code-doesnt-mean-stable-code-4mbd</link>
      <guid>https://dev.to/adamthedeveloper/minimal-code-doesnt-mean-stable-code-4mbd</guid>
      <description>&lt;p&gt;The argument sounds reasonable: fewer lines of code mean fewer bugs. Simpler to review, easier to reason about, less surface area for defects. Sounds great. It's true. But it's also incomplete.&lt;/p&gt;

&lt;p&gt;The problem starts when backend developers treat production systems like homework assignments. In a single-process app:&lt;/p&gt;

&lt;p&gt;you control execution. You know the order. Threads might race, but at least they share the same memory and clock. &lt;/p&gt;

&lt;p&gt;Once you have APIs talking to databases, webhooks firing at midnight, async jobs on a queue, and three replicas behind a load balancer, the failure modes multiply: connections drop, messages arrive out of order, clocks disagree, and partial failures show up at 3 AM on Tuesdays. &lt;/p&gt;

&lt;p&gt;Trimming code doesn't make any of that go away. It just hides the complexity until something breaks.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Minimal Code Meets Production
&lt;/h2&gt;

&lt;p&gt;Consider what happens when your minimalist masterpiece meets reality:&lt;/p&gt;

&lt;p&gt;Your service temporarily loses connection to the database for 30 seconds. Your code has no timeout logic. Requests hang. Users refresh. More requests queue up. Eventually something breaks.&lt;/p&gt;

&lt;p&gt;Two instances process the same webhook because you thought "that probably won't happen." No idempotency key, so the charge runs twice. Your balance sheet now has an extra $50,000 in it. Your accountant is confused. Your manager is less confused.&lt;/p&gt;

&lt;p&gt;A worker crashes mid-operation. There's no recovery mechanism. The transaction is abandoned in an inconsistent state. Your data is now in a state that violates every assumption you made about how it should look.&lt;/p&gt;

&lt;p&gt;A retry storm after a downstream blip hammers your API because nothing backs off or deduplicates. Rate limits trip. Legitimate traffic gets dropped. You're debugging an outage caused by code that "handled errors" by logging and returning.&lt;/p&gt;

&lt;p&gt;None of these are prevented by writing less. They're prevented by writing the boring safeguards you skipped because they looked redundant.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Production Actually Requires
&lt;/h2&gt;

&lt;p&gt;Modern backend systems need safeguards that simple applications never had to think about:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency.&lt;/strong&gt; Every operation must be safe to retry. A payment webhook redelivered, a queue message processed twice, a client that retries on timeout—all of these need a way to recognize "already done." Operation IDs, version numbers, dedupe keys. Not glamorous. Required.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeouts.&lt;/strong&gt; Requests to other services need deadlines. Without them, cascading failures happen silently and gradually consume all your resources. Your code will just sit there, waiting, like a phone call that never connects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compensation Logic.&lt;/strong&gt; When a multi-step operation fails partway through, something has to undo the work already committed. You can't abandon a half-finished saga and hope nobody notices. That's more code than assuming success. People skip it anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conflict Detection.&lt;/strong&gt; When two writers touch the same record—two API instances, a retry overlapping with the original request—you need version checks, timestamps, or optimistic locking. Pretending conflicts don't exist works until two updates land in the wrong order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observability.&lt;/strong&gt; Logging, metrics, and traces that let you reconstruct what happened when something fails. At 3 AM, you'll wish this existed. When something breaks and you have no logs, you'll understand why this matters.&lt;/p&gt;

&lt;p&gt;You can't delete these and call it simplification. You're just moving complexity from your editor into your on-call rotation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Less Code vs. Less Noise
&lt;/h2&gt;

&lt;p&gt;Kill redundant abstractions, dead logic, and speculative frameworks. That's good discipline.&lt;/p&gt;

&lt;p&gt;Deleting retry wrappers, validation, circuit breakers, or idempotency checks because they "add noise" is a different move. &lt;/p&gt;

&lt;p&gt;You're betting stability on dependencies you don't control. When the database hiccups, the partner API times out, or Kubernetes reschedules a pod mid-request, the system doesn't get simpler. It gets wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Test
&lt;/h2&gt;

&lt;p&gt;If your app runs more than one instance, talks to other services, or processes work asynchronously, these questions will eventually matter:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If a process dies mid-operation, can the system detect it and recover correctly?&lt;/li&gt;
&lt;li&gt;If a message is delayed several seconds, what actually happens?&lt;/li&gt;
&lt;li&gt;If two workers attempt the same operation at once, is the result deterministic or a coin flip?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you can't answer all three with specific mechanisms—not vibes, not "we'll fix it in prod"—the codebase isn't simple. It's fragile.&lt;/p&gt;

&lt;p&gt;Write the safeguards. Handle the failure modes. The goal isn't more lines for their own sake; it's making hidden complexity visible before production does it for you.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>productivity</category>
      <category>backend</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>The Mid-Dev Paradox: Why Juniors Are Chill and Mid-Levels Will Reorganize Your Code at 3 AM</title>
      <dc:creator>Adam - The Developer</dc:creator>
      <pubDate>Wed, 20 May 2026 09:58:51 +0000</pubDate>
      <link>https://dev.to/adamthedeveloper/the-mid-dev-paradox-why-juniors-are-chill-and-mid-levels-will-reorganize-your-code-at-3-am-2bh4</link>
      <guid>https://dev.to/adamthedeveloper/the-mid-dev-paradox-why-juniors-are-chill-and-mid-levels-will-reorganize-your-code-at-3-am-2bh4</guid>
      <description>&lt;p&gt;Here's a thing nobody talks about in the "building a healthy engineering culture" TED talks: &lt;strong&gt;junior developers are not the problem&lt;/strong&gt;. They're predictable. They ask questions. They listen. They know what they don't know, which is half the battle.&lt;/p&gt;

&lt;p&gt;The mid-level developer? Now &lt;em&gt;there's&lt;/em&gt; your chaos agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tier List (Controversial, I Know)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Juniors:&lt;/strong&gt; "I don't understand this. Can you explain it?"&lt;br&gt;
&lt;strong&gt;Translation:&lt;/strong&gt; &lt;em&gt;easy, good faith problem&lt;/em&gt;. You explain it. They learn. Maybe they code it a weird way, but at least they'll tell you before shipping. These people are trying not to sink the boat.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mids:&lt;/strong&gt; "Yeah, I read a Medium article about this architecture pattern once."&lt;br&gt;
&lt;strong&gt;Translation:&lt;/strong&gt; &lt;em&gt;oh no&lt;/em&gt;. They now know juuuust enough to be confident, not enough to see all the edges they're missing. They're about to refactor your entire codebase because TypeScript stricter settings are "table stakes for any serious project" (they read an article).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Seniors:&lt;/strong&gt; "We could do it that way, but let's think about what breaks in six months."&lt;br&gt;
&lt;strong&gt;Translation:&lt;/strong&gt; &lt;em&gt;calm, because we've seen it break in six months before&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Juniors Are Actually Great
&lt;/h2&gt;

&lt;p&gt;A junior asked why the onboarding docs were three years out of date. We laughed. Then realized: none of us had updated them because we all just knew how things worked now. New person had to learn from Slack messages and asking people questions. We'd optimized ourselves into a bottleneck.&lt;/p&gt;

&lt;p&gt;Here's the thing about juniors: they haven't been beaten down yet. They still think documentation should exist. They still think things should be findable. Revolutionary concepts, I know.&lt;/p&gt;

&lt;p&gt;You point them at a task, they get stuck, they ask where the docs are, and you realize there &lt;em&gt;aren't&lt;/em&gt; any. Or they're scattered across four different Slack threads from 2021. They don't know yet that this is normal. They think "onboarding" means something other than "asking random people questions until you figure it out."&lt;/p&gt;

&lt;p&gt;The risk with a junior is they'll point out all the gaps and make you feel bad about it. The upside is they're collaborative about fixing it. They're not trying to prove anything. They're just trying to do the job and not feel lost the whole time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mid-Dev Energy Crisis
&lt;/h2&gt;

&lt;p&gt;Mids are where things get spicy.&lt;/p&gt;

&lt;p&gt;They've shipped something. Maybe it worked, maybe it kind of worked, maybe it caught fire but they're not thinking about that right now. The point is: they have &lt;em&gt;success weight&lt;/em&gt;. They're no longer a question mark—they're a contributor with opinions.&lt;/p&gt;

&lt;p&gt;But here's where it gets fun. They've read enough to be dangerous, not enough to be wise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Mid-Dev's Greatest Hits:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Architecture Reorganization:&lt;/strong&gt; "I've been thinking... what if we moved everything to microservices? Distributed systems are the future. I watched a talk." No strategic reason. No pain point being solved. They just watched a talk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Framework Rewrite:&lt;/strong&gt; "Okay but what if we rewrote the whole thing in [new framework]?" Why? "Better DX." Whose DX? Their DX for writing new code while everyone else owns production incidents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Premature Abstraction:&lt;/strong&gt; Four months into the project, they've created a custom hook ecosystem so complex that only they understand it. Junior tries to use it, asks for help, gets a 40-minute explanation of prop-drilling alternatives nobody asked for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Aggressive Code Review:&lt;/strong&gt; They're reviewing a junior's PR. The code works. It's readable. It passes tests. They leave 12 comments asking why the developer "didn't consider using a custom hook" or "didn't memoize aggressively" or "didn't think about server rendering" even though it's a form in an admin panel that hits 30 users a month.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The mid-dev is learning that they can have opinions, and they're exercising that power like they just got access to a 2002 Honda Civic and need to test the engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Happens (And It's Not Their Fault)
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable truth: mids are in the danger zone of confidence versus competence. They're past the "I don't know anything" phase but not yet in the "I know what I don't know" phase.&lt;/p&gt;

&lt;p&gt;The Dunning-Kruger effect is real, but it's not really about stupidity. It's about &lt;em&gt;scale&lt;/em&gt;. A junior's worked on one thing. A mid's worked on three to five things. That &lt;em&gt;feels&lt;/em&gt; like a lot. It &lt;em&gt;feels&lt;/em&gt; like they've seen the patterns.&lt;/p&gt;

&lt;p&gt;Until they haven't. Until they learn (the hard way) that "best practice" depends on context. That premature optimization isn't just bad—it's actively hostile to shipping. That code that's boring and works beats code that's clever and breaks at 2 AM.&lt;/p&gt;

&lt;p&gt;The thing is, every senior developer alive was a mid once. Most of us pushed something completely unnecessary to production while congratulating ourselves on our architecture skills. Most of us spent a week optimizing something that didn't matter. That's not a failure—that's training.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Works
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;If you're a senior managing a mid:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Don't tell them they're wrong—that just makes them defensive. Instead, ask them questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What would break if we do this?" (Often: silence, then they realize something.)&lt;/li&gt;
&lt;li&gt;"How long will the refactor take? What's the payoff?" (This makes them do math. Math is humbling.)&lt;/li&gt;
&lt;li&gt;"Can we ship the simple version first and refactor if we hit actual problems?" (This teaches patience.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Give them ownership of something that can break &lt;em&gt;a little bit&lt;/em&gt; without destroying the business. Let them experience consequences without trauma. This is how you turn a mid into a senior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're a mid reading this:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Look, you're doing great. You're learning fast, you're getting stronger, you're building real things. The fact that you're in this uncomfortable middle zone means you're &lt;em&gt;growing&lt;/em&gt;. That's good.&lt;/p&gt;

&lt;p&gt;But here's the move: &lt;strong&gt;start asking seniors why instead of telling juniors what&lt;/strong&gt;. When you're about to suggest a rewrite, ask a senior if they've seen that problem before. When you're reviewing code, ask yourself if you're protecting the system or protecting your ego.&lt;/p&gt;

&lt;p&gt;The seniors you respect aren't the ones with the cleverest solutions. They're the ones who know when simple is better. You can get there faster than you think, but only if you stay curious instead of confident.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Insight
&lt;/h2&gt;

&lt;p&gt;Juniors are dangerous in ways we expect—they might not write optimal code, they might miss edge cases. We've built systems to catch that (code review, testing, etc). We &lt;em&gt;expect&lt;/em&gt; juniors to be junior.&lt;/p&gt;

&lt;p&gt;Mids are dangerous in ways we don't expect. They're good enough that we trust their judgment. They're confident enough that they don't ask for second opinions. And they've read just enough Medium articles to be genuinely convinced they're right.&lt;/p&gt;

&lt;p&gt;The thing that separates a mid from a senior isn't skill—it's humility. It's knowing that "I don't know" doesn't mean you're not good at this; it means you're paying attention.&lt;/p&gt;

&lt;p&gt;So yeah, keep your juniors close. Let them ask questions. Appreciate that they still think code should be boring and work-y instead of clever and break-y.&lt;/p&gt;

&lt;p&gt;And the mids? Keep them curious. Give them hard problems. Have them explain their solutions to someone who wasn't part of the decision. Let them fail small.&lt;/p&gt;

&lt;p&gt;The goal is to make juniors into mids and mids into people who remember what it felt like to be certain about everything.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>programming</category>
      <category>career</category>
      <category>architecture</category>
    </item>
    <item>
      <title>⚔️ Go vs Java: The Minimalist vs The Enterprise Veteran</title>
      <dc:creator>Adam - The Developer</dc:creator>
      <pubDate>Mon, 11 May 2026 06:12:47 +0000</pubDate>
      <link>https://dev.to/adamthedeveloper/go-vs-java-the-minimalist-vs-the-enterprise-veteran-1gg3</link>
      <guid>https://dev.to/adamthedeveloper/go-vs-java-the-minimalist-vs-the-enterprise-veteran-1gg3</guid>
      <description>&lt;p&gt;&lt;strong&gt;No sides. No agenda. Just two languages walking into a bar and us watching what happens.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The Setup&lt;/li&gt;
&lt;li&gt;The Contenders&lt;/li&gt;
&lt;li&gt;A Quick Origin Story&lt;/li&gt;
&lt;li&gt;Language Philosophy: Complexity vs. Simplicity&lt;/li&gt;
&lt;li&gt;Performance: JVM vs Native Binary&lt;/li&gt;
&lt;li&gt;Concurrency: Goroutines vs Virtual Threads&lt;/li&gt;
&lt;li&gt;Ecosystem &amp;amp; Libraries: The Forest vs The Toolshed&lt;/li&gt;
&lt;li&gt;Tooling: Go's Discipline vs Java's Buffet&lt;/li&gt;
&lt;li&gt;
Team Learning Curve: The First Month Matters

&lt;ul&gt;
&lt;li&gt;Go: Steep and Short&lt;/li&gt;
&lt;li&gt;Java: Gradual and Endless&lt;/li&gt;
&lt;li&gt;The Team Implication&lt;/li&gt;
&lt;li&gt;In Practice&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

Where Each One Shines

&lt;ul&gt;
&lt;li&gt;Go is great for:&lt;/li&gt;
&lt;li&gt;Java is great for:&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;The Honest Answer&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  ⚔️  The Setup
&lt;/h2&gt;

&lt;p&gt;I decided to write this because I've been drowning in content where people compare languages like they're picking a religion. "This is better than that, that is better than this," and they're mostly just optimized for outrage.&lt;/p&gt;

&lt;p&gt;I've been writing Java for a long time. Fell in love with it early—the way it handles threads, the concurrency model, the entire ecosystem around it just &lt;em&gt;clicked&lt;/em&gt;. For a while I genuinely thought Java was the language where threading finally became practical and not a complete nightmare.&lt;/p&gt;

&lt;p&gt;Full transparency: I also wanted to write Java because back then, knowing Java meant programmers would praise you at parties. There was definitely ego in it. (Okay, there was &lt;em&gt;a lot&lt;/em&gt; of ego in it.) But the reasons I still reach for it now are actually technical and only &lt;em&gt;somewhat&lt;/em&gt; ego-driven.&lt;/p&gt;

&lt;p&gt;Then Go came along. Started writing it about two years ago and I've been enjoying it. Built two distributed systems at work with it. But it makes you &lt;em&gt;do&lt;/em&gt; things manually, the syntax feels weirdly alien the first time you see it, and the ecosystem has this vibe of "I hope this library doesn't get abandoned next month."&lt;/p&gt;

&lt;p&gt;No hate on Go. I genuinely love using it.&lt;/p&gt;

&lt;p&gt;So here we are—I'm writing this (technically during a very boring class about a week ago) to actually break down what these languages are good at, so you can make a smarter choice than "everyone's using X so we should too."&lt;/p&gt;




&lt;h2&gt;
  
  
  🥊 The Contenders
&lt;/h2&gt;

&lt;p&gt;Two languages dominate a lot of backend conversations right now. One has been around since the disco era, survived the dot-com bust, and somehow became the backbone of half the world's enterprise software. The other was built by Google engineers who were tired of waiting for C++ to compile, and it became the quiet workhorse behind Docker, Kubernetes, and half of modern infrastructure.&lt;/p&gt;

&lt;p&gt;Java and Go. The veteran and the minimalist. The cathedral and the toolshed.&lt;/p&gt;

&lt;p&gt;Neither is objectively better. Both are genuinely excellent at different things. This post isn't here to crown a winner. It's here to help you understand what each one is &lt;em&gt;actually good at&lt;/em&gt;, so you can make a smarter call the next time someone says "so what stack are we using?"&lt;/p&gt;

&lt;p&gt;Let's get into it.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏁 A Quick Origin Story
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Java&lt;/strong&gt; was born in 1995 at Sun Microsystems, led by &lt;strong&gt;James Gosling&lt;/strong&gt;, with one promise: &lt;em&gt;Write Once, Run Anywhere&lt;/em&gt;. The JVM meant your compiled bytecode could run on any machine. This was revolutionary at the time. Java rode that wave into enterprise dominance and never really left.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Go&lt;/strong&gt; (or Golang) was created at Google in 2009 by &lt;strong&gt;Rob Pike&lt;/strong&gt;, &lt;strong&gt;Ken Thompson&lt;/strong&gt;, and &lt;strong&gt;Robert Griesemer&lt;/strong&gt;. These three people have more programming language credentials than most of us will ever accumulate. Their frustration? C++ build times were destroying their productivity. Their solution? A language that was fast to compile, fast to run, and simple enough that you couldn't shoot yourself in the foot too badly.&lt;/p&gt;

&lt;p&gt;Different eras. Different problems. Different philosophies.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 Language Philosophy: Complexity vs. Simplicity
&lt;/h2&gt;

&lt;p&gt;This is where the two languages diverge most dramatically. Not in syntax, but in &lt;em&gt;worldview&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Java&lt;/strong&gt; believes in giving you tools. Lots of tools. Generics, inheritance, abstract classes, interfaces, annotations, lambdas, streams, optional, records, sealed classes. Java has a solution for every pattern, and then a pattern for every solution. It trusts you to assemble them wisely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Go&lt;/strong&gt; believes in taking tools away. Go has no classes (just structs). No inheritance. No generics until recently (Go 1.18, 2022). No exceptions, just error values returned explicitly. The Go team's philosophy is almost aggressively minimalist. If a feature could be abused, they'd rather not include it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Go error handling: explicit, everywhere, always&lt;/span&gt;
&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"data.txt"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to open file: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Java: exceptions handle the unhappy path separately&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FileReader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"data.txt"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// do stuff&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;RuntimeException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to open file"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&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;Neither approach is wrong. Java's exception model keeps the happy path clean. Go's explicit errors mean you &lt;em&gt;cannot&lt;/em&gt; forget to handle failure. The compiler won't let you ignore an error value without being deliberate about it.&lt;/p&gt;

&lt;p&gt;Go's philosophy produces code that a new team member can read on day one. Java's philosophy produces code that can model genuinely complex domains with precision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dig deeper if:&lt;/strong&gt; You care about this. &lt;a href="https://go.dev/doc/faq" rel="noopener noreferrer"&gt;Go's design FAQ&lt;/a&gt; and &lt;a href="https://openjdk.org/projects/amber/" rel="noopener noreferrer"&gt;Java's language evolution&lt;/a&gt; tell very different stories about how languages grow.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ Performance: JVM vs Native Binary
&lt;/h2&gt;

&lt;p&gt;Go compiles to a &lt;strong&gt;native binary&lt;/strong&gt;. You run &lt;code&gt;go build&lt;/code&gt;, you get a self-contained executable that starts in milliseconds. It's like handing someone a knife—they just use it.&lt;/p&gt;

&lt;p&gt;Java runs on the &lt;strong&gt;JVM&lt;/strong&gt;, which is more like handing someone a full kitchen. There's setup time (the JVM initializes), there's a warm-up period where the JIT compiler figures out what code you're running a lot (and starts optimizing it), but once it knows what's happening, it can produce machine code that's genuinely competitive or sometimes better than Go for sustained workloads.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Go&lt;/th&gt;
&lt;th&gt;Java&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cold start&lt;/td&gt;
&lt;td&gt;🟢 Milliseconds&lt;/td&gt;
&lt;td&gt;🟡 Seconds (improving with GraalVM)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Peak throughput (warmed up)&lt;/td&gt;
&lt;td&gt;🟡 Very fast&lt;/td&gt;
&lt;td&gt;🟢 Can match or beat Go&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory footprint&lt;/td&gt;
&lt;td&gt;🟢 Small&lt;/td&gt;
&lt;td&gt;🟡 Larger baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Serverless / short-lived processes&lt;/td&gt;
&lt;td&gt;🟢 Natural fit&lt;/td&gt;
&lt;td&gt;🟡 JVM overhead hurts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long-running services&lt;/td&gt;
&lt;td&gt;🟡 Great&lt;/td&gt;
&lt;td&gt;🟢 JIT optimization pays off&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The tradeoff:&lt;/strong&gt; Go's predictable, instant startup is perfect for environments where things are constantly spinning up and down. Java's startup cost disappears if the process lives for weeks—the JIT warmup happens once, and then you get increasingly optimized code.&lt;/p&gt;

&lt;p&gt;GraalVM native images exist if you want Java's ecosystem with Go's startup speed, but you're adding complexity to your build. It's a bridge, not a solution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dig deeper if:&lt;/strong&gt; &lt;a href="https://www.techempower.com/benchmarks/" rel="noopener noreferrer"&gt;TechEmpower benchmarks&lt;/a&gt; if you like staring at numbers.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔀 Concurrency: Goroutines vs Virtual Threads
&lt;/h2&gt;

&lt;p&gt;Go's concurrency story used to be the unreachable dream for everyone else. &lt;strong&gt;Goroutines&lt;/strong&gt; are lightweight, greenthread-style concurrency that the Go runtime manages for you. You can spawn tens of thousands without breaking a sweat:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Launch 10,000 concurrent tasks. No ceremony.&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="n"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;doSomethingBlocking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}(&lt;/span&gt;&lt;span class="n"&gt;i&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;Channels are your communication layer—they're the part that makes goroutines actually &lt;em&gt;elegant&lt;/em&gt; instead of just fast:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="s"&gt;"hello from another goroutine"&lt;/span&gt;
&lt;span class="p"&gt;}()&lt;/span&gt;

&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This mental model (goroutines + channels) became foundational to Go. It made high-concurrency systems feel operationally approachable. That's why Docker, Kubernetes, Prometheus—all the infrastructure that had to handle millions of goroutines—are written in Go.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Java had a problem here.&lt;/strong&gt; For years, the answer to "how do I handle thousands of concurrent requests?" was "spawn a thread per request" or "use a thread pool and hope." It worked, but it didn't &lt;em&gt;feel&lt;/em&gt; right. You could feel the language fighting you.&lt;/p&gt;

&lt;p&gt;Then Java 21 brought &lt;strong&gt;Virtual Threads&lt;/strong&gt;. Same idea as goroutines—lightweight, JVM-managed concurrency. But here's the thing: they look exactly like regular Java threads. No new syntax, no new mental model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Java 21: 100,000 virtual threads. Same old executor API.&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Executors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;newVirtualThreadPerTaskExecutor&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;IntStream&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;range&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100_000&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;forEach&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
        &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;submit&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;doSomethingBlocking&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The real difference:&lt;/strong&gt; Go requires you to think in a new way. Goroutines and channels are a genuinely elegant paradigm, but they're different from how most languages do concurrency. Java's virtual threads let you keep thinking the old way—submit work, forget about it, let the runtime handle the threads.&lt;/p&gt;

&lt;p&gt;Go's approach produces more elegant concurrency code when you're building from scratch. Java's approach is pragmatic when you have existing blocking code or when you don't want to learn a new concurrency philosophy just to handle concurrent requests.&lt;/p&gt;

&lt;p&gt;Both solve the same problem. Go solved it first and more elegantly. Java solved it later and more "you don't have to change anything."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dig deeper if:&lt;/strong&gt; &lt;a href="https://go.dev/blog/goroutines-and-channels" rel="noopener noreferrer"&gt;The Go Blog on goroutines&lt;/a&gt; or &lt;a href="https://openjdk.org/jeps/444" rel="noopener noreferrer"&gt;JEP 444: Virtual Threads&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🌲 Ecosystem &amp;amp; Libraries: The Forest vs The Toolshed
&lt;/h2&gt;

&lt;p&gt;Java's ecosystem is &lt;em&gt;vast&lt;/em&gt;. I'm talking millions of artifacts in Maven Central. Whatever you need exists somewhere. Database drivers, HTTP clients, payment processors, ML frameworks—multiple mature options, probably more than you want to choose from. The Spring ecosystem alone is essentially its own platform. Spring Boot, Spring Data, Spring Cloud, Spring Security. Teams build entire careers knowing just that one thing deeply.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tradeoff:&lt;/strong&gt; Abundance creates paralysis. You're choosing between 47 JSON libraries and second-guessing yourself. A "simple" Spring Boot project pulls in hundreds of transitive dependencies. You're managing a forest, and sometimes you can't see the trees.&lt;/p&gt;

&lt;p&gt;Go's ecosystem is younger and more curated. The standard library is &lt;em&gt;actually good&lt;/em&gt;—HTTP servers, JSON encoding, crypto, testing are all production-quality and baked in. The community has filled in the gaps with solid packages: &lt;code&gt;gin&lt;/code&gt;, &lt;code&gt;echo&lt;/code&gt;, &lt;code&gt;gorm&lt;/code&gt;, &lt;code&gt;cobra&lt;/code&gt;. But sometimes you hit the edge. A niche domain where nothing exists, and now you're writing it yourself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tradeoff:&lt;/strong&gt; You make fewer decisions, have fewer dependencies to worry about, and your binaries are smaller. But occasionally you're building something the ecosystem hasn't solved yet.&lt;/p&gt;

&lt;p&gt;Here's where it matters: For &lt;strong&gt;small, bounded services&lt;/strong&gt; (webhooks, rate limiters, health checkers, internal tools), Go's minimal approach keeps things clean and understandable. You grab the standard library, maybe add one focused package, and you're done. For &lt;strong&gt;complex enterprise systems&lt;/strong&gt; (multi-tenant SaaS with user roles, audit trails, compliance logging, payment integration), Java's ecosystem saves you months of building. Spring Data handles the database complexity that would be a pain to build. Spring Security handles authentication scenarios that would take forever to get right.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Go: 8 lines, no dependencies&lt;/span&gt;
&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusOK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fprintln&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;`{"status": "ok"}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListenAndServe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;":8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Spring: more setup, but it's assuming you'll build an actual system on top&lt;/span&gt;
&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HealthController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/health"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;health&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"ok"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Go version is simpler when you actually need simplicity. The Spring version pays dividends when complexity is inevitable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dig deeper if:&lt;/strong&gt; &lt;a href="https://pkg.go.dev" rel="noopener noreferrer"&gt;pkg.go.dev&lt;/a&gt; or &lt;a href="https://mvnrepository.com" rel="noopener noreferrer"&gt;mvnrepository.com&lt;/a&gt; (warning: you will feel overwhelmed).&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ Tooling: Go's Discipline vs Java's Buffet
&lt;/h2&gt;

&lt;p&gt;Go ships with an &lt;em&gt;opinionated&lt;/em&gt; standard toolchain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;go fmt&lt;/code&gt;: formats your code. Non-negotiable. Everyone's Go code looks the same.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;go test&lt;/code&gt;: testing built in, no framework needed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;go vet&lt;/code&gt;: catches common mistakes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;go mod&lt;/code&gt;: dependency management, built in since Go 1.11&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;go build&lt;/code&gt;: one command, one binary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are no arguments about Go tooling. It's just &lt;em&gt;there&lt;/em&gt;, it works, and the whole Go community uses the same tools.&lt;/p&gt;

&lt;p&gt;Java's tooling is more of a choose-your-own-adventure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Build tools: Maven or Gradle (religious war ongoing since 2012)&lt;/li&gt;
&lt;li&gt;Testing: JUnit + Mockito + AssertJ + maybe Testcontainers + maybe Spock&lt;/li&gt;
&lt;li&gt;Formatting: Checkstyle? Google Java Format? Your lead's personal preferences from 2015?&lt;/li&gt;
&lt;li&gt;Dependency management: Maven Central or JitPack or that internal Nexus your company runs that nobody fully understands&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Java tooling ecosystem is powerful, flexible, and the source of at least 30% of new developer onboarding time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The tradeoff:&lt;/strong&gt; Go's rigid tooling means less time arguing about style and setup, but also less flexibility if you have unusual needs. Java's flexible tooling means you &lt;em&gt;can&lt;/em&gt; optimize for your exact situation, but you have to make more decisions upfront.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dig deeper if:&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go tooling: &lt;code&gt;go help&lt;/code&gt; in your terminal is genuinely great. &lt;/li&gt;
&lt;li&gt;Java: the &lt;a href="https://maven.apache.org/guides/" rel="noopener noreferrer"&gt;Maven docs&lt;/a&gt; or &lt;a href="https://docs.gradle.org" rel="noopener noreferrer"&gt;Gradle docs&lt;/a&gt; depending on which side of history you're on.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  👥 Team Learning Curve: The First Month Matters
&lt;/h2&gt;

&lt;p&gt;This is where language choice gets &lt;em&gt;practical&lt;/em&gt; in ways that benchmarks completely miss.&lt;/p&gt;

&lt;h3&gt;
  
  
  Go: Steep and Short
&lt;/h3&gt;

&lt;p&gt;Week one with Go is rough. The syntax looks wrong to you: &lt;code&gt;defer&lt;/code&gt;, &lt;code&gt;goroutines&lt;/code&gt;, &lt;code&gt;channels&lt;/code&gt;, &lt;code&gt;interfaces without explicit implementation&lt;/code&gt;. You'll write code that compiles but doesn't feel right. You'll stare at a pointer receiver and wonder why it exists.&lt;/p&gt;

&lt;p&gt;But then something shifts. By week three, you're productive. There's just not enough to learn. Go is &lt;em&gt;intentionally simple&lt;/em&gt;—it has fewer corners, fewer patterns, fewer ways to paint yourself into a corner. You get to the end of the learning curve faster because there &lt;em&gt;is&lt;/em&gt; an end.&lt;/p&gt;

&lt;p&gt;After a month, you can pick up any Go codebase and understand it. The style is consistent because &lt;code&gt;gofmt&lt;/code&gt; is non-negotiable. There's usually one way to do things, so debates are settled by the language itself.&lt;/p&gt;

&lt;h3&gt;
  
  
  Java: Gradual and Endless
&lt;/h3&gt;

&lt;p&gt;A new Java developer is productive &lt;em&gt;fast&lt;/em&gt;. Spring Boot handles so much boilerplate. IntelliJ is powerful enough that you can write working code without really knowing what you're doing. By week one, you've shipped something.&lt;/p&gt;

&lt;p&gt;But productive ≠ competent. The learning curve doesn't end, it just gets less steep.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Generics and wildcards: "What's a &lt;code&gt;? super T&lt;/code&gt;?"&lt;/li&gt;
&lt;li&gt;Inheritance hierarchies: "Why does this class extend AbstractSomething which implements Interface-Whatever?"&lt;/li&gt;
&lt;li&gt;Dependency injection: "How did this bean get instantiated?"&lt;/li&gt;
&lt;li&gt;Streams vs for loops: "Which should I use?"&lt;/li&gt;
&lt;li&gt;Checked vs unchecked exceptions: "Should I throw this or declare it?"&lt;/li&gt;
&lt;li&gt;Annotations: "Is this magic or is it explicit?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After a month, you ship features. But they're not idiomatic. You copy patterns without understanding them. You build things the complex way because Java &lt;em&gt;can&lt;/em&gt; do complexity, so you assume it &lt;em&gt;should&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;After 6 months, you start thinking in Java. After a year, you're actually dangerous.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Team Implication
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Go teams scale horizontally.&lt;/strong&gt; Hire three junior developers, and by week four they're all contributing meaningfully. Code reviews are fast because there's less to argue about. The language enforces consistency. New people can't accidentally introduce wildly different patterns because the language doesn't allow them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Java teams scale with depth.&lt;/strong&gt; Hire three senior engineers who know Spring inside-out, and they can architect complex systems. But hire three mid-level developers, and you'll spend months establishing patterns. The payoff is that once you have that shared understanding, you can build systems that would be awkward in Go.&lt;/p&gt;

&lt;h3&gt;
  
  
  In Practice
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Go:&lt;/strong&gt; New developer → valuable by day 3 → production-ready code by day 20&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Java:&lt;/strong&gt; New developer → visible output by day 5 → stops making seniors cringe by day 90&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your team is mostly junior and turns over frequently, Go reduces friction. People ramp up before they leave. If your team is senior and stable, Java's richness becomes an asset. You can mentor through the complexity, and the codebase can express sophisticated requirements.&lt;/p&gt;

&lt;p&gt;Neither is better. They're different onboarding curves with different endpoints.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏢 Where Each One Shines
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Go is great for:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cloud-native infrastructure&lt;/strong&gt;: Docker, Kubernetes, Terraform, Prometheus—all Go. They needed to handle the concurrency problem (goroutines managing millions of containers), and Go's lightweight concurrency made high-scale infrastructure feel operationally approachable. Few mainstream languages made this density practical at the time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Microservices and APIs&lt;/strong&gt;: Small binary, fast startup, low memory. When you're deploying dozens of services to containers that spin up and down constantly, Go's millisecond startup matters operationally. The JVM's seconds are a constant background friction in that scenario.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CLI tools&lt;/strong&gt;: Single binary, no runtime, just works. Ship a Go executable to users and they run it. That's it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Network-heavy services&lt;/strong&gt;: Goroutines handle tens of thousands of concurrent connections efficiently. If you're building something that lives at the edge (proxy, load balancer, API gateway), this becomes an operational advantage.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Teams with high turnover or a strong consistency preference&lt;/strong&gt;: The language enforces one way of doing things. New people ramp fast. Debates about style disappear because &lt;code&gt;gofmt&lt;/code&gt; is non-negotiable.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Java is great for:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Complex enterprise domains&lt;/strong&gt;: The type system (generics, sealed classes, records) lets you model intricate business logic precisely. When requirements change three years later, the compiler helps you find everything that needs updating. Java makes you be explicit about contracts, and that pays off over time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;High-throughput, long-running services&lt;/strong&gt;: Once the JVM warms up—which happens once, then stays warm for weeks—the JIT produces increasingly optimized code. In services running continuously and handling millions of requests, this optimization compounds. You get better performance the longer it runs, the opposite of microservices.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Large, stable teams&lt;/strong&gt;: Onboarding takes longer, but once your team knows the patterns, Java's explicitness becomes a feature. You can architect complex systems and have everyone understand them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data-heavy applications&lt;/strong&gt;: Hibernate, Spring Data, the JPA ecosystem—they've solved a lot of hard database problems. Complex queries, transactions, migrations, relationship management. Go's database story works, but Java's is more mature and battle-tested.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Existing Java investment&lt;/strong&gt;: You have code that works, people who know it, and switching costs are real. Modern Java (21+) is genuinely better to work with than it used to be. Virtual threads solved a real weakness. Stay and improve rather than rewrite.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Systems that need long-term evolution&lt;/strong&gt;: Java's type system helps you reason about changes years later. The language pushes you toward being explicit about constraints and contracts. That discipline pays off when requirements get complex.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🤷 The Honest Answer
&lt;/h2&gt;

&lt;p&gt;Pick Go if you're building infrastructure tooling, CLIs, or containerized microservices where startup time and memory footprint actually matter operationally. Pick it if you want to move fast without debating style guides. Pick it if your team is junior and you don't have time to babysit learning curves.&lt;/p&gt;

&lt;p&gt;Pick Java if you're building something genuinely complex and you need a type system that grows with your requirements. Pick it if you already have Java in your organization and switching would be a nightmare. Pick it if your team is senior and stable—the language rewards expertise and knowledge accumulation.&lt;/p&gt;

&lt;p&gt;The honest truth is that the language is rarely the bottleneck. Architecture, database design, team communication, deployment infrastructure—those matter more. But choosing the right tool for your constraints does save you from fighting friction that shouldn't exist.&lt;/p&gt;

&lt;p&gt;Both are genuinely good at what they're designed for. You're not picking between "good" and "bad." You're picking between "good for this" and "good for that."&lt;/p&gt;

</description>
      <category>programming</category>
      <category>java</category>
      <category>go</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Write Code That's Easy to Delete: The Art of Impermanent Software</title>
      <dc:creator>Adam - The Developer</dc:creator>
      <pubDate>Sat, 02 May 2026 08:22:42 +0000</pubDate>
      <link>https://dev.to/adamthedeveloper/write-code-thats-easy-to-delete-the-art-of-impermanent-software-19l1</link>
      <guid>https://dev.to/adamthedeveloper/write-code-thats-easy-to-delete-the-art-of-impermanent-software-19l1</guid>
      <description>&lt;p&gt;&lt;em&gt;We obsess over making code last. Maybe we should obsess over making it leave gracefully.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;There's a quote that's been living rent-free in my head for years:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"Write code that is easy to delete, not easy to extend."&lt;/strong&gt;&lt;br&gt;
— Tef, &lt;em&gt;programming is terrible&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The first time I read it, I pushed back. Isn't the whole point to write code that &lt;em&gt;survives&lt;/em&gt;? That scales? That you can build on top of?&lt;/p&gt;

&lt;p&gt;Then I spent a weekend trying to rip out a logging library from a three-year-old codebase. It had quietly spread into 40 files. Removing it felt like surgery on a patient who had grown bones around a sponge.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Lie We Tell Ourselves
&lt;/h2&gt;

&lt;p&gt;When we write code, we tell ourselves a flattering story: &lt;em&gt;this will be here in five years, so I should make it robust, reusable, and extensible.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;But the data doesn't support this story. Most features get changed within months. Many get cut entirely. The average production codebase has entire directories that haven't been touched in years — not because they're perfect, but because everyone is too afraid to delete them.&lt;/p&gt;

&lt;p&gt;We write code as if it's load-bearing. Usually, it isn't.&lt;/p&gt;

&lt;p&gt;The irony is that the more we try to make code "permanent" — wrapping it in abstractions, coupling it into shared utilities, weaving it through the system, the &lt;em&gt;harder&lt;/em&gt; it becomes to change. We've traded adaptability for the illusion of durability.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "Easy to Delete" Actually Means
&lt;/h2&gt;

&lt;p&gt;It doesn't mean write throwaway code. It doesn't mean skip tests or ignore structure.&lt;/p&gt;

&lt;p&gt;It means: &lt;strong&gt;design for reversibility.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you write a feature, ask yourself: &lt;em&gt;if this needed to go away tomorrow, what would that look like?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the answer is "a 400-line PR touching 20 files," something went wrong at the design stage — not the deletion stage.&lt;/p&gt;

&lt;p&gt;Easy-to-delete code tends to share a few traits:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. It lives in one place
&lt;/h3&gt;

&lt;p&gt;Duplication gets a bad reputation. The DRY principle is good advice, but taken to its extreme, it creates code that's deeply entangled. When the same function is reused in eight different contexts, you can't change it for one context without worrying about all the others.&lt;/p&gt;

&lt;p&gt;Sometimes, a little duplication is the price of independence. Two modules that both have a &lt;code&gt;formatDate&lt;/code&gt; function can each evolve or disappear without consequences.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. It has a clear boundary
&lt;/h3&gt;

&lt;p&gt;The hardest code to delete is the code that has leaked everywhere. The database client that got imported into UI components. The config object that got passed twelve layers deep. The utility function that became load-bearing infrastructure.&lt;/p&gt;

&lt;p&gt;Boundaries are what make deletion safe. An isolated module, a clean interface, a service behind a well-defined API... these are things you can remove, replace, or rewrite without holding or thinking through your breath.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. It doesn't know too much
&lt;/h3&gt;

&lt;p&gt;Code that's easy to delete tends to be ignorant but in the best way. It doesn't know about the rest of the system. It takes inputs, does its job, returns outputs. It doesn't reach out and grab global state. It doesn't mutate things it didn't create.&lt;/p&gt;

&lt;p&gt;Ignorant code is also testable code, which is no coincidence ( I actually didn't wanna add this part for some personal reasons )&lt;/p&gt;

&lt;h3&gt;
  
  
  4. It's hidden behind a seam
&lt;/h3&gt;

&lt;p&gt;Feature flags. Adapter layers. Interface abstractions. These aren't just engineering formalism — they're deletion handles. A feature behind a flag can be switched off in seconds. Code behind an interface can be swapped without the callers noticing.&lt;/p&gt;

&lt;p&gt;The strangler fig pattern exists precisely for this reason: wrap the old thing, build the new thing alongside it, then delete the old thing once it's isolated. The seam is what makes that possible.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Different Way to Think About Abstraction
&lt;/h2&gt;

&lt;p&gt;We often reach for abstraction to avoid repetition. But the best reason to abstract something is to &lt;em&gt;isolate&lt;/em&gt; it or to give it a name and a box so that you can change or remove it without touching everything else.&lt;/p&gt;

&lt;p&gt;Think about logging. You could scatter &lt;code&gt;console.log&lt;/code&gt; calls everywhere. That's easy to write and immediately painful to change. Or you could route all logging through a single &lt;code&gt;logger&lt;/code&gt; module. Now if you want to swap logging libraries, or add context, or silence it entirely — you only touch one file. ONE.&lt;/p&gt;

&lt;p&gt;The abstraction isn't there because logging is complex. It's there because logging is a thing that might &lt;em&gt;change or disappear&lt;/em&gt;, and you want that to be painless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Abstract at the seams, not in the middle.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Deletability as a Code Review Lens
&lt;/h2&gt;

&lt;p&gt;Here's something I've started doing in code review: asking not just "does this work?" but "what would it take to remove this?"&lt;/p&gt;

&lt;p&gt;It reframes things in a useful way.&lt;/p&gt;

&lt;p&gt;A PR that adds a new feature and touches 15 files is a warning sign — not necessarily because it's wrong, but because it's announcing a high cost of future change. A PR that adds the same feature through a single, well-bounded module is leaving a cleaner footprint.&lt;/p&gt;

&lt;p&gt;You can extend this to architecture decisions. Before adding a new dependency, ask: "what does removing this look like in two years?" Some dependencies are fine because they're small, stable, or isolated. Others are like introducing an invasive species. They grow into everything and become impossible to root out.&lt;/p&gt;




&lt;h2&gt;
  
  
  Impermanence Is Not Defeatism
&lt;/h2&gt;

&lt;p&gt;There's a Zen concept sometimes translated as &lt;em&gt;impermanence&lt;/em&gt; — the idea that things arise, exist for a time, and pass away. This isn't pessimism. It's just an accurate description of how things work.&lt;/p&gt;

&lt;p&gt;Software is the same. Features come and go. Products pivot. Requirements change. The code you're writing today will be partially or wholly replaced. That's not failure — that's how living software works.&lt;/p&gt;

&lt;p&gt;Writing for impermanence means accepting this, and designing accordingly. It means your goal isn't to write code that can never be removed. It's to write code whose removal is &lt;em&gt;cheap&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The engineers who built systems that are still running 30 years later didn't achieve that by making the code impossible to touch. They achieved it by writing code that was easy to reason about, easy to isolate, and when the time came, it's easy to replace piece by piece.&lt;/p&gt;




&lt;h2&gt;
  
  
  In Practice: A Checklist
&lt;/h2&gt;

&lt;p&gt;Before you commit something, it's worth a quick gut-check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Could I delete this feature with a single PR?&lt;/strong&gt; If not, why not?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How many files does this touch?&lt;/strong&gt; More isn't always worse, but it should feel intentional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is this module aware of things it shouldn't be?&lt;/strong&gt; Imports, globals, side effects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If this dependency disappeared tomorrow, how bad would it be?&lt;/strong&gt; Could you swap it in an afternoon?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Is this abstraction making things easier to change, or just avoiding repetition?&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this means paralysis. You don't need to design every microservice like it might vanish. But developing an instinct for deletion cost with the same way you develop an instinct for performance or readability, it'll quietly make your codebases healthier.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Code You Don't Have to Write
&lt;/h2&gt;

&lt;p&gt;There's a final point worth making: the easiest code to delete is the code you never write.&lt;/p&gt;

&lt;p&gt;Every feature is a liability. Every abstraction is a maintenance surface. Every dependency is a relationship you're now in. The code that doesn't exist has no bugs, no coupling, no deletion cost.&lt;/p&gt;

&lt;p&gt;This doesn't mean build nothing. It means be deliberate. When you feel the urge to add a new layer of abstraction, to generalize something that's only been used once, to build for a use case that might never arrive... pause.&lt;/p&gt;

&lt;p&gt;Maybe the right move is to wait. To write the minimal thing. To leave room for deletion.&lt;/p&gt;

&lt;p&gt;Because software that can change easily is software that can survive. And the secret to changeability isn't clever architecture or brilliant abstractions.&lt;/p&gt;

&lt;p&gt;It's knowing that what you built today can be gracefully taken apart tomorrow.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>webdev</category>
      <category>productivity</category>
      <category>architecture</category>
    </item>
    <item>
      <title>" SaaS in 2026 Is the Plumbing, Not the Hype "</title>
      <dc:creator>Adam - The Developer</dc:creator>
      <pubDate>Thu, 30 Apr 2026 03:20:11 +0000</pubDate>
      <link>https://dev.to/adamthedeveloper/-kh2</link>
      <guid>https://dev.to/adamthedeveloper/-kh2</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/arunkant/why-im-building-saas-in-2026-55hn" class="crayons-story__hidden-navigation-link"&gt;Why I'm Building SaaS in 2026&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
      &lt;a href="https://dev.to/arunkant/why-im-building-saas-in-2026-55hn" class="crayons-article__context-note crayons-article__context-note__feed"&gt;&lt;p&gt;SaaS as reliable plumbing for fragile agents&lt;/p&gt;

&lt;/a&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/arunkant" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F136893%2F76d71e2a-5d18-4fbb-a4fb-33d3a69d532a.png" alt="arunkant profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/arunkant" class="crayons-story__secondary fw-medium m:hidden"&gt;
              arunkant
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                arunkant
                
              
              &lt;div id="story-author-preview-content-3586685" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/arunkant" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F136893%2F76d71e2a-5d18-4fbb-a4fb-33d3a69d532a.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;arunkant&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/arunkant/why-im-building-saas-in-2026-55hn" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Apr 29&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/arunkant/why-im-building-saas-in-2026-55hn" id="article-link-3586685"&gt;
          Why I'm Building SaaS in 2026
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/saas"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;saas&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/agents"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;agents&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/workflows"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;workflows&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/arunkant/why-im-building-saas-in-2026-55hn" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;48&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/arunkant/why-im-building-saas-in-2026-55hn#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              30&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            4 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success crayons-icon c-btn__icon"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
    </item>
    <item>
      <title>Why High-Performing Teams Look Like They Do Nothing</title>
      <dc:creator>Adam - The Developer</dc:creator>
      <pubDate>Mon, 27 Apr 2026 06:46:12 +0000</pubDate>
      <link>https://dev.to/adamthedeveloper/why-high-performing-teams-look-like-they-do-nothing-2o4j</link>
      <guid>https://dev.to/adamthedeveloper/why-high-performing-teams-look-like-they-do-nothing-2o4j</guid>
      <description>&lt;p&gt;There is a belief quietly embedded in some engineering cultures that suffering equals seriousness. That if your team is not always reachable, not always slightly panicked, not always racing something, then they must not be working hard enough.&lt;/p&gt;

&lt;p&gt;This belief is wrong. And it is worth naming clearly, because it does real damage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Confusion
&lt;/h2&gt;

&lt;p&gt;Challenge and stress are not the same thing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Challenge&lt;/strong&gt; is being handed a problem you have never solved before and being trusted to figure it out. It is learning something hard. It is designing a system under real constraints. It is disagreeing with a technical decision and having to defend your position with evidence. Challenge stretches you. When it is over, you feel like you grew.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chronic stress&lt;/strong&gt; is your phone buzzing at 10pm. It is a deadline moved up without explanation. It is the ambient anxiety of knowing you must always appear available, always appear busy, always appear to be giving more. When it is over, you feel hollowed out. And then it starts again.&lt;/p&gt;

&lt;p&gt;Some managers treat the second thing as if it were the first. They see responsiveness and call it ownership. They see exhaustion and call it passion. They create urgency as a management style, not because the work demands it, but because urgency feels like leadership.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Happens
&lt;/h2&gt;

&lt;p&gt;It is not always malicious. A lot of it comes from a simple measurement problem.&lt;/p&gt;

&lt;p&gt;Stress is visible. You can see who responds fastest. You can see who is online at midnight. You can see who never pushes back on a deadline. These signals are easy to read, easy to reward, and easy to confuse with performance.&lt;/p&gt;

&lt;p&gt;Genuine challenge is harder to observe. Deep thinking is invisible. The best engineering decisions often look like nothing happened, because a problem was caught early. The developer who said "we should not build this yet" and saved the team three months of rework does not have a story that makes it into the all-hands.&lt;/p&gt;

&lt;p&gt;So the proxy gets promoted: availability, urgency, perpetual motion.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Costs
&lt;/h2&gt;

&lt;p&gt;People in chronically stressed environments often look highly productive for a while. They respond fast. They ship things. They are visibly busy.&lt;/p&gt;

&lt;p&gt;But the quality of their thinking degrades. Their decisions get narrower. They stop exploring better options and start choosing the fastest acceptable one. Their appetite for hard problems shrinks, because they do not have the cognitive space to sit with difficulty. They start optimizing for speed over correctness, for appearing capable over actually being capable.&lt;/p&gt;

&lt;p&gt;And eventually, the best ones leave. Not always loudly. Sometimes they just get quieter, do what is asked, and start looking elsewhere. The people who stay longest in high-stress, low-challenge environments are often those with fewer options, not those with the most to contribute.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Real Challenge Looks Like
&lt;/h2&gt;

&lt;p&gt;This is not an argument against hard work, tight timelines, or high standards. Those things are real, and good engineers often want them.&lt;/p&gt;

&lt;p&gt;Real challenge tends to have a few properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It requires thinking, not just doing.&lt;/strong&gt; The constraint is intellectual, not just temporal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There is genuine ownership.&lt;/strong&gt; The person doing the work has some say in how it gets done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The pressure is contextual, not permanent.&lt;/strong&gt; There are hard sprints and then there is recovery. The urgency is tied to real circumstances, not manufactured as a default state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure is information, not catastrophe.&lt;/strong&gt; People can take risks without dreading consequences.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Paranoia is not a feature. Misery is not a signal of commitment. A team that is always stressed is not a team that is being challenged. It is a team that is being depleted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Silence Can Be the Sound of Quality
&lt;/h2&gt;

&lt;p&gt;I experienced this firsthand when I was leading a team.&lt;/p&gt;

&lt;p&gt;We were, by most measures, the quietest team around. Low communication overhead. Calm standups. Not a lot of noise in the incident channels. And the reason was simple: we had very few bugs, and when something did get fixed, it stayed fixed. We were not constantly putting out fires because we were not constantly starting them.&lt;/p&gt;

&lt;p&gt;But I heard through the grapevine that other teams thought we were not doing much. Because we were not loud. Because we were not visibly scrambling.&lt;/p&gt;

&lt;p&gt;Meanwhile, the teams that were most vocal, most communicative, most frantically active were dealing with recurring issues, patches on top of patches, the same problems resurfacing in slightly different shapes. That busyness was real. The work was real. But a significant portion of it was self-generated, the cost of not getting things right the first time.&lt;/p&gt;

&lt;p&gt;The irony is sharp: doing quality work made us invisible. And being invisible got mistaken for being idle.&lt;/p&gt;

&lt;p&gt;This is what happens when communication volume becomes a proxy for productivity. The team fighting fires all day is easy to see. The team that quietly prevented the fires does not have a highlight reel.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Note to Managers
&lt;/h2&gt;

&lt;p&gt;If your team seems disengaged, it is worth asking whether you have been offering them challenge or just stress. The two feel similar from the outside, especially if you are the one setting the pace.&lt;/p&gt;

&lt;p&gt;Challenge requires trust. It means giving someone a genuinely hard problem, stepping back, and letting them work through it. That is harder to do than creating urgency. It requires believing that thinking is work, that slowness can be wisdom, and that the goal is excellent output over a career, not maximum output over a quarter.&lt;/p&gt;

&lt;p&gt;The teams that do the most interesting, lasting work are rarely the ones running on panic. They are the ones who are genuinely absorbed in hard problems, who have space to think, and who know the difference between a real fire and a manufactured one.&lt;/p&gt;

&lt;p&gt;That distinction is worth protecting.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>productivity</category>
      <category>career</category>
      <category>startup</category>
    </item>
    <item>
      <title>The Developer Who Reviews Everything and Ships Nothing</title>
      <dc:creator>Adam - The Developer</dc:creator>
      <pubDate>Sat, 18 Apr 2026 06:28:10 +0000</pubDate>
      <link>https://dev.to/adamthedeveloper/the-developer-who-reviews-everything-and-ships-nothing-1e28</link>
      <guid>https://dev.to/adamthedeveloper/the-developer-who-reviews-everything-and-ships-nothing-1e28</guid>
      <description>&lt;p&gt;You've seen this person. Maybe you've worked with them for years.&lt;/p&gt;

&lt;p&gt;They leave 40 comments on your PR. Variable names, spacing philosophy, whether your abstraction is "truly necessary," a link to a 2014 blog post about hexagonal architecture. The review sits open for a week. Then two.&lt;/p&gt;

&lt;p&gt;Meanwhile, their own tickets age quietly in the backlog. Untouched.&lt;/p&gt;




&lt;h2&gt;
  
  
  Before We Go There
&lt;/h2&gt;

&lt;p&gt;Most strict reviewers are not villains.&lt;/p&gt;

&lt;p&gt;A lot of them have been burned. They've watched a rushed merge take down production at 2am. They've inherited a codebase where "we'll clean it up later" compounded for three years into something unmaintainable. Strictness in review often comes from real scar tissue. That's not dysfunction. That's experience talking.&lt;/p&gt;

&lt;p&gt;This article isn't about those people.&lt;/p&gt;

&lt;p&gt;It's about a specific, observable pattern: the developer whose review activity is consistently high, whose shipping activity is consistently low, and whose involvement reliably increases cycle time without a corresponding increase in outcome quality. That pattern has a real cost and it rarely gets named out loud.&lt;/p&gt;

&lt;p&gt;So let's name it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pattern
&lt;/h2&gt;

&lt;p&gt;It doesn't require mind-reading. It shows up in the data.&lt;/p&gt;

&lt;p&gt;PRs they author are rare. When they do appear, they're small, low-risk, or six weeks overdue. PRs they touch accumulate long threads, mostly non-blocking comments that aren't labeled as such, leaving authors to guess what actually needs to change. Review cycles on their queue run longer than the team average. Features that slip usually have their fingerprints somewhere in the timeline.&lt;/p&gt;

&lt;p&gt;That's the shape of it. Measurable. Repeatable. Worth paying attention to.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where They Put Their Time
&lt;/h2&gt;

&lt;p&gt;Senior engineers have limited hours. Where those hours go is a signal.&lt;/p&gt;

&lt;p&gt;A senior spending eight hours a week in code review and two hours writing code has made a choice. Sometimes that's the right call — architecture, incident response, debugging gnarly production issues, mentorship. Real contributions that don't show up in merge counts.&lt;/p&gt;

&lt;p&gt;But when the same person's review comments outnumber their merged commits by a factor of ten, quarter after quarter, you're looking at someone who has concentrated their influence in the one place where they can evaluate others without being evaluated themselves.&lt;/p&gt;

&lt;p&gt;That asymmetry is the tell.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Actually Costs
&lt;/h2&gt;

&lt;p&gt;When review cycles drag on for days, the effects compound quietly.&lt;/p&gt;

&lt;p&gt;The author loses context. They've moved on mentally to the next problem. When they return to address 30 comments, they're doing archaeology on their own work.&lt;/p&gt;

&lt;p&gt;Junior developers learn that shipping is scary. That there's always something wrong. That the bar is impossibly high, so maybe it's better to ask fewer questions and wait for someone to tell you what to do.&lt;/p&gt;

&lt;p&gt;Momentum dies. Not in one dramatic moment, but comment by comment, week by week.&lt;/p&gt;

&lt;p&gt;And the developer with the high standards? Present at every standup. Very visible. Very engaged. Zero shipped features to show for the sprint.&lt;/p&gt;




&lt;h2&gt;
  
  
  Nitpicking Is Not Mentorship
&lt;/h2&gt;

&lt;p&gt;There's a version of this that gets laundered through mentorship language.&lt;/p&gt;

&lt;p&gt;"I'm just trying to teach them." "How else will they learn?" "I wouldn't be doing my job if I let this slide."&lt;/p&gt;

&lt;p&gt;Genuine mentorship explains tradeoffs. It asks questions instead of demanding changes. It approves code that's good enough while offering perspective on what could be better next time.&lt;/p&gt;

&lt;p&gt;What it doesn't do is make someone feel like their work is never good enough, while that same reviewer's own code somehow never faces the same gauntlet.&lt;/p&gt;

&lt;p&gt;If your "mentorship" only flows one direction and none of your mentees can ship without your sign-off on 40 line items, that's not mentorship. That's a bottleneck with good branding.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Accountability Gap
&lt;/h2&gt;

&lt;p&gt;Here's a diagnostic worth running.&lt;/p&gt;

&lt;p&gt;Track for one month which developers on your team ship production code. Not who reviews, not who comments — who actually merges working features to production. Then look at who has the most comments open across other people's PRs.&lt;/p&gt;

&lt;p&gt;A large gap between those two lists is worth investigating. Not a verdict. Seniors do invisible work that won't show up in a merge count, and that work genuinely matters.&lt;/p&gt;

&lt;p&gt;But here's the cut: if someone's review involvement is consistently high, their direct output is consistently low, and the team's cycle time is getting worse, "I do invisible work" becomes a harder argument to sustain. Shipping isn't the only form of contribution. But consistent absence from shipping alongside heavy review influence is a smell, and pretending otherwise doesn't make it go away.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fixes With Actual Teeth
&lt;/h2&gt;

&lt;p&gt;The standard advice is: add review SLAs, separate blocking from non-blocking comments, track cycle time. All correct. All worth doing. All also pretty easy to quietly ignore.&lt;/p&gt;

&lt;p&gt;If the pattern is entrenched, you need something sharper.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cap non-blocking comments.&lt;/strong&gt; If a reviewer leaves more than five non-blocking comments, they roll them into a summary or they stay quiet. Unlimited low-stakes commentary costs the reviewer nothing and costs the author a morning. Change the economics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Require a patch for strong objections.&lt;/strong&gt; If a reviewer argues an approach is wrong, they should be able to show an alternative. Not to embarrass anyone — because it forces the objection to get concrete. A lot of "this architecture is problematic" comments evaporate the moment someone has to write the better version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make the ratio visible.&lt;/strong&gt; Track review comments opened vs. PRs merged per person, alongside cycle time per reviewer. Don't make it a performance metric. Just make it visible. Sunlight is usually enough.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Know who actually has veto power.&lt;/strong&gt; Not every PR needs every senior. Be explicit about who is a required approver versus who is optional feedback. When everyone with an opinion is a required approver, you've handed veto power to whoever is most willing to hold out longest.&lt;/p&gt;




&lt;h2&gt;
  
  
  Raising the Bar vs. Holding the Door Shut
&lt;/h2&gt;

&lt;p&gt;You can tell the difference by asking one question: does this person's involvement make the team ship more, or less?&lt;/p&gt;

&lt;p&gt;If every PR they touch becomes a negotiation, if every week they're reviewing has longer cycle times than the weeks they're out, if junior developers dread their feedback instead of seeking it — that's your answer.&lt;/p&gt;

&lt;p&gt;Raising the bar means the team gets better over time. Patterns become consistent. People grow and ship with more confidence.&lt;/p&gt;

&lt;p&gt;Holding the door shut means nothing gets through without a fight, nobody proposes anything ambitious, and your most capable people quietly start updating their resumes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Mirror
&lt;/h2&gt;

&lt;p&gt;When did you last ship something? When did you last put your own code up for the same level of scrutiny you apply to others?&lt;/p&gt;

&lt;p&gt;If your presence in the review process reliably slows shipping more than it improves outcomes, you are not raising standards. You are taxing the team.&lt;/p&gt;

&lt;p&gt;The engineers who actually elevate a codebase over time make things better and faster simultaneously. That's the bar. It's harder than leaving comments.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>programming</category>
      <category>career</category>
      <category>learning</category>
    </item>
    <item>
      <title>Your Caching Strategy Is Not a Strategy</title>
      <dc:creator>Adam - The Developer</dc:creator>
      <pubDate>Mon, 06 Apr 2026 04:46:45 +0000</pubDate>
      <link>https://dev.to/adamthedeveloper/your-caching-strategy-is-not-a-strategy-1he5</link>
      <guid>https://dev.to/adamthedeveloper/your-caching-strategy-is-not-a-strategy-1he5</guid>
      <description>&lt;p&gt;You have a slow endpoint. Someone suggests Redis. You add Redis. The endpoint gets faster. You ship it. Six months later you are debugging a production incident where users see stale data, your cache hit rate is 12%, and you have no idea what is actually in Redis anymore.&lt;/p&gt;

&lt;p&gt;That is not a caching strategy. That is a prayer with an expiry time.&lt;/p&gt;

&lt;p&gt;This post is not here to roast you. It is here to give you the patterns, the code, and the mental model to do this right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Caching Is a Contract&lt;/li&gt;
&lt;li&gt;The Three Classic Patterns (And When to Use Each)&lt;/li&gt;
&lt;li&gt;Build a Cache Client Worth Using&lt;/li&gt;
&lt;li&gt;Cache Key Design: More Important Than It Looks&lt;/li&gt;
&lt;li&gt;
Cache Invalidation: The Part Everyone Skips

&lt;ul&gt;
&lt;li&gt;TTL vs. Event Driven Invalidation&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

The Thundering Herd and How to Solve It

&lt;ul&gt;
&lt;li&gt;Solution 1: Jitter&lt;/li&gt;
&lt;li&gt;Solution 2: Stale While Revalidate&lt;/li&gt;
&lt;li&gt;Solution 3: Distributed Lock on Cache Miss&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Observability: Know What Is Actually Happening&lt;/li&gt;

&lt;li&gt;The Decision Checklist Before You Add a Cache&lt;/li&gt;

&lt;li&gt;Putting It All Together&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  Caching Is a Contract
&lt;/h2&gt;

&lt;p&gt;Before you touch Redis, understand what you are agreeing to.&lt;/p&gt;

&lt;p&gt;Caching is a contract. You are telling your system: "I accept that this data may be slightly wrong for a period of time, in exchange for speed." Most teams sign that contract without reading it.&lt;/p&gt;

&lt;p&gt;Before you add any cache entry, answer these four questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What is the acceptable staleness window for this data?&lt;/li&gt;
&lt;li&gt;Who invalidates this entry and when?&lt;/li&gt;
&lt;li&gt;What happens when the cache is cold?&lt;/li&gt;
&lt;li&gt;What happens when the cache is wrong?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you cannot answer all four, you do not have a caching strategy. You have optimism.&lt;/p&gt;

&lt;p&gt;The rest of this post is about answering each of those questions with real code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Three Classic Patterns (And When to Use Each)
&lt;/h2&gt;

&lt;p&gt;There are three fundamental ways to integrate a cache with your database. Most teams only know one and use it everywhere.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;How It Works&lt;/th&gt;
&lt;th&gt;When to Use It&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cache Aside&lt;/td&gt;
&lt;td&gt;App checks cache, misses go to DB, app writes to cache&lt;/td&gt;
&lt;td&gt;Default. Works well for most read heavy workloads.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write Through&lt;/td&gt;
&lt;td&gt;Every write goes to DB and cache together, atomically&lt;/td&gt;
&lt;td&gt;Read heavy data that changes infrequently. Keeps cache always warm.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write Behind&lt;/td&gt;
&lt;td&gt;Write to cache immediately, flush to DB asynchronously&lt;/td&gt;
&lt;td&gt;Very high write throughput: analytics, metrics, event ingestion, rate limiting counters.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most engineers default to cache aside everywhere, which is fine until it is not. Write behind in particular is underused. When you are recording analytics events or incrementing rate limit counters, you do not need each write to round trip to a database. Write to Redis, flush to Postgres in batches. Your database handles a fraction of the load.&lt;/p&gt;

&lt;p&gt;The important thing is that you choose consciously. Each pattern has tradeoffs. Write behind carries real risk of data loss if Redis fails before the flush. That is acceptable for a view counter and unacceptable for a financial transaction. Know which one you are dealing with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a Cache Client Worth Using
&lt;/h2&gt;

&lt;p&gt;Before diving into patterns, establish a typed, reusable cache client. This becomes the foundation for everything below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;RedisClientType&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;redis&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;CacheOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;      &lt;span class="c1"&gt;// base TTL in seconds&lt;/span&gt;
  &lt;span class="nl"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// max random seconds to add (prevents stampedes)&lt;/span&gt;
  &lt;span class="nl"&gt;stale&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// extra seconds to serve stale while revalidating&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;CacheEntry&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;cachedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CacheClient&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RedisClientType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redisUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;redisUrl&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;RedisClientType&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nf"&gt;getUnderlyingClient&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;RedisClientType&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;effectiveTTL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CacheOptions&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jitter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jitter&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ttl&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CacheEntry&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;CacheEntry&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CacheOptions&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;effectiveTTL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;staleTTL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stale&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CacheEntry&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;cachedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="na"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ttl&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setEx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;staleTTL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;delByPattern&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keys&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;CacheEntry&lt;/code&gt; wrapper stores &lt;code&gt;cachedAt&lt;/code&gt; and &lt;code&gt;expiresAt&lt;/code&gt; alongside the value. This unlocks stale while revalidate later without a second Redis call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache Key Design: More Important Than It Looks
&lt;/h2&gt;

&lt;p&gt;This is one of the most overlooked parts of a caching system. Your key schema is an architectural decision, not a naming convention.&lt;/p&gt;

&lt;p&gt;A good cache key answers five questions, in order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app : v2 : user : 123 : profile
 ^     ^     ^     ^      ^
 |     |     |     |      shape or query variant
 |     |     |     entity ID
 |     |     entity type
 |     cache version
 app namespace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More examples:&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;app&lt;/span&gt;:&lt;span class="n"&gt;v2&lt;/span&gt;:&lt;span class="n"&gt;user&lt;/span&gt;:&lt;span class="m"&gt;123&lt;/span&gt;:&lt;span class="n"&gt;permissions&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;:&lt;span class="n"&gt;v2&lt;/span&gt;:&lt;span class="n"&gt;feed&lt;/span&gt;:&lt;span class="n"&gt;user&lt;/span&gt;:&lt;span class="m"&gt;123&lt;/span&gt;:&lt;span class="n"&gt;page&lt;/span&gt;:&lt;span class="m"&gt;2&lt;/span&gt;:&lt;span class="n"&gt;limit&lt;/span&gt;:&lt;span class="m"&gt;20&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;:&lt;span class="n"&gt;v2&lt;/span&gt;:&lt;span class="n"&gt;product&lt;/span&gt;:&lt;span class="m"&gt;456&lt;/span&gt;:&lt;span class="n"&gt;inventory&lt;/span&gt;:&lt;span class="n"&gt;warehouse&lt;/span&gt;:&lt;span class="n"&gt;uk&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bad keys look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user:123
profile_123_v2
user_data_new_123
temp_user_123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No namespace means you cannot isolate patterns for deletion. No version means changing the shape of a cached object requires flushing all of Redis. No structure means you cannot delete "everything for user 123" with a single pattern.&lt;/p&gt;

&lt;p&gt;The key schema also tells you something about your architecture. If your keys look inconsistent, your caching layer grew organically without a plan. Standardize the schema early and enforce it through a key builder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CACHE_VERSION&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;CACHE_VERSION&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;v1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;userProfile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="s2"&gt;`app:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;CACHE_VERSION&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:user:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:profile`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;userPermissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="s2"&gt;`app:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;CACHE_VERSION&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:user:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:permissions`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;userFeed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="s2"&gt;`app:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;CACHE_VERSION&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:feed:user:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:page:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:limit:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;userAll&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="s2"&gt;`app:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;CACHE_VERSION&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:user:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:*`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when you need to invalidate all data for a user, it is one call: &lt;code&gt;delByPattern(keys.userAll(userId))&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To handle a breaking shape change, bump &lt;code&gt;CACHE_VERSION&lt;/code&gt; in your deploy config. Old keys expire naturally. New requests populate the new shape. No coordinated flush against production Redis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache Invalidation: The Part Everyone Skips
&lt;/h2&gt;

&lt;p&gt;Most cache bugs are not "we cached the wrong thing." They are "we forgot to uncache it when the underlying data changed."&lt;/p&gt;

&lt;p&gt;Here is the pattern that causes incidents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// The read path is carefully thought through&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getUserProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;userProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setEx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// The write path does not think about the cache at all&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updateUserProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// cache is now wrong. silently. for the next hour.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix is to own both paths in the same service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CacheClient&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;getProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserProfile&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;userProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserProfile&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;updateProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserProfile&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserProfile&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Write through: update cache immediately with fresh data&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;userProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;suspendAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;suspendUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Permissions must be immediately consistent.&lt;/span&gt;
    &lt;span class="c1"&gt;// Never serve stale permission data. Delete, do not wait for TTL.&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;userPermissions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;suspendAccount&lt;/code&gt; does not use write through. It deletes the permissions key outright. Serving a stale permission is a security issue, not a UX issue. The next read will hit the database and get the correct answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  TTL vs. Event Driven Invalidation
&lt;/h3&gt;

&lt;p&gt;These are two different tools for two different problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TTL invalidation&lt;/strong&gt; is for data where being slightly stale is acceptable. Exchange rates, public blog posts, product catalog pages. Set a TTL and let entries expire naturally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Event driven invalidation&lt;/strong&gt; is for data where correctness matters. User permissions, account status, pricing. Delete or update the cache entry at the moment of the change, not on a timer.&lt;/p&gt;

&lt;p&gt;Most systems use TTL for everything because it requires less upfront thinking. Then they get burned when a permissions update does not take effect for an hour. The fix is not to lower the TTL. Lowering the TTL is how you get a thundering herd.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Thundering Herd and How to Solve It
&lt;/h2&gt;

&lt;p&gt;A typical Redis hit takes 1 to 3ms. A typical database query takes 50 to 300ms. That means one cache miss can cost as much as 100 cache hits. At scale, getting this wrong does not just make things slow. It brings services down.&lt;/p&gt;

&lt;p&gt;Imagine a hot cache entry expires at 3:00:00 AM. You have 500 concurrent users. At 3:00:01, all 500 get a cache miss simultaneously and fire a database query. Your database, handling 10 queries per second comfortably, suddenly receives 500 in the same second and collapses.&lt;/p&gt;

&lt;p&gt;You just traded slightly stale data for a full outage.&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;Without protection:

500 requests ──► 500 DB queries ──► DB overwhelmed ──► Outage

With jitter + lock:

500 requests ──► 1 DB query ──► Cache filled ──► 499 served from cache
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three solutions, applied in layers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution 1: Jitter
&lt;/h3&gt;

&lt;p&gt;The simplest fix. Add randomness to expiry so entries do not all expire at the same moment. Already built into the &lt;code&gt;CacheClient&lt;/code&gt; above via the &lt;code&gt;jitter&lt;/code&gt; option.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Without jitter: 500 entries for a popular endpoint all expire at 03:00:00&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// With jitter: entries expire anywhere between 3600 and 3900 seconds&lt;/span&gt;
&lt;span class="c1"&gt;// Stampede risk drops dramatically for zero added complexity&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;jitter&lt;/code&gt; everywhere. It costs nothing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution 2: Stale While Revalidate
&lt;/h3&gt;

&lt;p&gt;Serve the stale entry immediately. Trigger a background refresh. The next request gets fresh data. This is how HTTP caching has worked for decades.&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;Request ──► Cache hit? ──► Yes, and fresh? ──► Return immediately
                │
                │ Yes, but stale?
                ├──► Return immediately (user does not wait)
                │    └──► Trigger background refresh
                │
                │ No (full miss)
                └──► Fetch from DB ──► Populate cache ──► Return
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;FetchFn&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getWithStaleRevalidate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CacheClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;fetchFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FetchFn&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CacheOptions&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isStale&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isStale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Serve the stale value immediately, refresh in the background&lt;/span&gt;
      &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;refreshInBackground&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fetchFn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Full miss: fetch synchronously&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchFn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;refreshInBackground&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CacheClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;fetchFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FetchFn&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CacheOptions&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchFn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Background cache refresh failed for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usage at call sites is one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getWithStaleRevalidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;userProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&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;Users never wait on a cache refresh. The background task handles it. The next user gets the fresh value.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution 3: Distributed Lock on Cache Miss
&lt;/h3&gt;

&lt;p&gt;For hot single keys, when a miss happens only one process should fetch and repopulate. Others wait briefly. This prevents 500 processes all querying the database for the same row at the same time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Redlock&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;redlock&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LockedCacheClient&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;CacheClient&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;redlock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Redlock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redisUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redisUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;redlock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Redlock&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUnderlyingClient&lt;/span&gt;&lt;span class="p"&gt;()]);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;getOrFetch&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;fetchFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FetchFn&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CacheOptions&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lockKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`lock:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;redlock&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;lockKey&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Re-check after acquiring the lock.&lt;/span&gt;
      &lt;span class="c1"&gt;// Another process may have already populated the cache while we waited.&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;recheck&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recheck&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;recheck&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchFn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Lock contention: fall back to a direct DB fetch rather than failing the request&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchFn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;release&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The re-check after acquiring the lock is critical. Without it, the second process acquires the lock and queries the database anyway even though the first process just populated the cache. This is the double checked locking pattern applied to distributed systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability: Know What Is Actually Happening
&lt;/h2&gt;

&lt;p&gt;You cannot improve what you cannot see. Wrap your cache client with metrics once and label call sites with a pattern name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Histogram&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Registry&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prom-client&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ObservableCacheClient&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;CacheClient&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;misses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;hitLatency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Histogram&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;missLatency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Histogram&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redisUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Registry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redisUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Counter&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="s2"&gt;cache_hits_total&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;help&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Total cache hits&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;labelNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;key_pattern&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;registers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;misses&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Counter&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="s2"&gt;cache_misses_total&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;help&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Total cache misses&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;labelNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;key_pattern&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;registers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hitLatency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Histogram&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="s2"&gt;cache_hit_duration_seconds&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;help&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Latency of cache hits&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;labelNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;key_pattern&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;registers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;registry&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;missLatency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Histogram&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="s2"&gt;cache_miss_duration_seconds&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;help&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Latency of cache misses including the upstream fetch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;labelNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;key_pattern&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="na"&gt;registers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;registry&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="k"&gt;async&lt;/span&gt; &lt;span class="nx"&gt;getOrFetchObserved&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;keyPattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;fetchFn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FetchFn&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CacheOptions&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;performance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inc&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key_pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;keyPattern&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hitLatency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key_pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;keyPattern&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nf"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;misses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inc&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key_pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;keyPattern&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetchFn&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;missLatency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;key_pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;keyPattern&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nf"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The three metrics that matter most:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hit rate per key pattern, not aggregate.&lt;/strong&gt; If your overall hit rate is 80% but a critical key pattern sits at 20%, the aggregate number is hiding a real problem. Always break this down by pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Eviction rate.&lt;/strong&gt; If Redis is evicting keys because you are out of memory, you are thrashing, not caching. A Redis hit at 1ms becomes meaningless if the key you need was evicted 30 seconds ago. Increase memory, shorten TTLs, or stop caching data that expires before it is ever read again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Miss latency.&lt;/strong&gt; At scale, a cache miss costs 50 to 300ms of database time. If your service depends on sub-10ms responses, one cold endpoint can blow your entire p99. Miss latency tells you exactly how bad the degradation is when your cache fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Decision Checklist Before You Add a Cache
&lt;/h2&gt;

&lt;p&gt;Not every performance problem needs a cache. Run through this before reaching for Redis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the query actually slow or just called too often?&lt;/strong&gt; N+1 patterns make caching look like the answer when a JOIN is. Profile the query execution plan before adding a cache layer on top of a structural problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can the data be denormalized instead?&lt;/strong&gt; If you always cache the same join result, consider materializing it in the schema. A cache is sometimes a workaround for a schema designed for write convenience rather than read performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is this actually read heavy?&lt;/strong&gt; If data is written more often than it is read, cache invalidation overhead exceeds the savings. You are paying the write cost twice: once to the database, once to update or invalidate the cache entry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the cost of serving stale data?&lt;/strong&gt; Product listings: probably acceptable. Account balance: no. Permissions and access control: never. Make this decision explicitly. Saying "we will just set a short TTL" is not an answer. It is a way of avoiding the question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is your cold start story?&lt;/strong&gt; If your service falls over every time it restarts because the cache is cold, write through caching or a warming script is the fix, not hoping traffic is light during the deploy window.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It All Together
&lt;/h2&gt;

&lt;p&gt;Here is a service that uses everything from this post: typed versioned keys, write through on mutations, stale while revalidate on reads, jitter throughout, immediate delete for permissions, and full observability.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductionUserService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ObservableCacheClient&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;getProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserProfile&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;getWithStaleRevalidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;userProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&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="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;updateProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserProfile&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UserProfile&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Write through: fresh data goes straight to cache on every mutation&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;userProfile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;jitter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;updatePermissions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Permission&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updatePermissions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Access control data is deleted immediately, never served stale&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;del&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;userPermissions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reads use stale while revalidate so users never block on a cache refresh. Profile writes use write through so the cache stays warm after every mutation. Permission changes delete immediately because serving a stale permission is a security issue, not a UX issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;The teams that handle caching well treat it as a first class architectural concern. They document cache contracts: what is cached, for how long, what triggers invalidation, and what the acceptable staleness window is. They test cold start and stampede scenarios explicitly. They monitor cache health with the same seriousness as database health.&lt;/p&gt;

&lt;p&gt;The teams that handle caching poorly add Redis when things get slow and ship it.&lt;/p&gt;

&lt;p&gt;Both approaches work right up until they do not. The difference is that the first team knows exactly what breaks and why when it does. The second team opens their laptop at past midnight and starts reading documentation for the first time or throw in " pls help me, redis no work, idk what is redis no more " into ChatGPT.&lt;/p&gt;

&lt;p&gt;The patterns in this post are not theoretical. They are the things you wish were already in the codebase when the incident starts. Put them in before it does.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>performance</category>
      <category>redis</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
