<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged caching</title>
<link>https://coderlegion.com/tag/caching</link>
<description>Powered by Question2Answer</description>
<item>
<title>Polar Bear Cache: Local Caching Without Repeated Remote Calls</title>
<link>https://coderlegion.com/32489/polar-bear-cache-local-caching-without-repeated-remote-calls</link>
<description>&lt;p&gt;&lt;strong&gt;Shared storage provides the source of truth. Local caches provide speed. Events help keep cache state in sync.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Project:&lt;/strong&gt; Polar Bear Cache — &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/babyblue94520/polar-bear-cache&quot;&gt;GitHub repository&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In my previous discussions about microservices, I explored how shared storage and event-driven synchronization can work together to manage distributed session state.&lt;/p&gt;
&lt;p&gt;This time, I want to focus on another part of the problem: caching frequently accessed data across multiple service instances.&lt;/p&gt;
&lt;h3 id=&quot;the-problem-faster-reads-distributed-cache-invalidation&quot;&gt;The Problem: Faster Reads, Distributed Cache Invalidation&lt;/h3&gt;
&lt;p&gt;In a microservices architecture, multiple service instances often need the same data, especially data that is read frequently but updated relatively infrequently.&lt;/p&gt;
&lt;p&gt;A distributed cache can reduce database load, but accessing it still involves network round trips and potentially serialization and deserialization overhead.&lt;/p&gt;
&lt;p&gt;So, what if each service instance could keep frequently accessed data in its own local cache, while receiving notifications when that data changes elsewhere?&lt;/p&gt;
&lt;p&gt;That is the idea behind &lt;strong&gt;Polar Bear Cache&lt;/strong&gt;, an open-source Java caching library designed to work with Spring's familiar cache annotations.&lt;/p&gt;
&lt;h3 id=&quot;the-design-local-cache-event-driven-invalidation&quot;&gt;The Design: Local Cache + Event-Driven Invalidation&lt;/h3&gt;
&lt;p&gt;Each service instance maintains its own local cache. When data changes, an event service can notify other instances that specific cache entries need to be invalidated.&lt;/p&gt;
&lt;p&gt;The key principle is simple:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Broadcast invalidation events, not the entire cached data.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Polar Bear Cache integrates with Spring's cache annotations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@Cacheable&lt;/code&gt; — Returns a cached value when available, or executes the underlying method to load it when the value is missing.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@CachePut&lt;/code&gt; — Updates the local cache and, when event-service integration is configured, notifies other instances to invalidate the corresponding entry.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@CacheEvict&lt;/code&gt; — Evicts cached data locally and, when event-service integration is configured, notifies other instances to invalidate their corresponding entries.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For example, when a product is updated:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The application updates the source of truth and its local cache.&lt;/li&gt;
&lt;li&gt;It publishes a cache-invalidation event through the event service.&lt;/li&gt;
&lt;li&gt;Other service instances receive the event and evict the corresponding local entry.&lt;/li&gt;
&lt;li&gt;Their next read loads the data again from the source of truth.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This allows repeated reads to stay local in the common case, while cross-instance communication is used to coordinate invalidation when data changes.&lt;/p&gt;
&lt;h3 id=&quot;when-does-this-approach-make-sense-&quot;&gt;When Does This Approach Make Sense?&lt;/h3&gt;
&lt;p&gt;This design is particularly suitable for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Read-heavy workloads with relatively infrequent updates.&lt;/li&gt;
&lt;li&gt;Applications running multiple service instances, each with its own local cache.&lt;/li&gt;
&lt;li&gt;Use cases where brief delays in cache invalidation are acceptable.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There is an important trade-off: event-driven invalidation is asynchronous. Between a data update and the time other instances process the invalidation event, stale data may still be served.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Polar Bear Cache does not guarantee strong consistency across service instances.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Depending on the application, you may also need to consider event-delivery reliability, duplicate events, cache expiration, and recovery strategies when a service instance misses an event.&lt;/p&gt;
&lt;p&gt;The goal is not to eliminate every consistency trade-off, but to make local caching practical in distributed applications without requiring every read to go through a remote cache.&lt;/p&gt;
&lt;h3 id=&quot;why-polar-bear-&quot;&gt;Why “Polar Bear”?&lt;/h3&gt;
&lt;p&gt;The name comes from a somewhat optimistic engineering idea:&lt;/p&gt;
&lt;p&gt;Local caching&lt;br&gt;
→ Fewer remote cache requests and network round trips&lt;br&gt;
→ Potentially more throughput from the same resources&lt;br&gt;
→ Potentially fewer servers needed for a given workload&lt;br&gt;
→ Lower energy consumption and greenhouse gas emissions&lt;br&gt;
→ Maybe we can help save polar bears. 🐻❄️🧊&lt;/p&gt;
&lt;p&gt;Of course, the actual impact depends on workload characteristics, infrastructure, and deployment choices. Performance improvements do not automatically translate into lower energy consumption, but I like the idea that better resource efficiency could benefit both applications and the environment.&lt;/p&gt;
&lt;h3 id=&quot;i-d-love-to-hear-your-thoughts&quot;&gt;I'd Love to Hear Your Thoughts&lt;/h3&gt;
&lt;p&gt;I'm interested in how other engineers approach local caching in distributed systems.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How do you handle cache invalidation across service instances?&lt;/li&gt;
&lt;li&gt;Do you prefer event-driven invalidation, short TTLs, versioned keys, or a combination?&lt;/li&gt;
&lt;li&gt;What failure scenarios would you consider essential to address in this design?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you're interested in the implementation, feedback and technical discussion are very welcome.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Less remote access, more ice.&lt;/em&gt; 🐻❄️🧊&lt;/p&gt;
&lt;h2 id=&quot;java-spring-microservices-caching-distributedsystems-opensource&quot;&gt;Java #Spring #Microservices #Caching #DistributedSystems #OpenSource&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32489/polar-bear-cache-local-caching-without-repeated-remote-calls</guid>
<pubDate>Mon, 26 Oct 2026 10:07:01 +0000</pubDate>
</item>
<item>
<title>Caching Strategies Demystified: Cache-Aside, Write-Through, and Write-Behind</title>
<link>https://coderlegion.com/30441/caching-strategies-demystified-cache-aside-write-through-and-write-behind</link>
<description>&lt;p&gt;In software development, caching is the practice of storing frequently used data in a high-speed, temporary storage layer so future requests can be served faster. &lt;strong&gt;Caching strategies&lt;/strong&gt; are the architectural patterns that dictate how your application coordinates the movement of data between this high-speed temporary memory (the cache) and your permanent, slower storage system (the database). Choosing the right strategy ensures your application stays incredibly fast while keeping data accurate and consistent.&lt;/p&gt;
&lt;h4 id=&quot;the-notebook-and-the-library-analogy&quot;&gt;The Notebook and the Library Analogy&lt;/h4&gt;
&lt;p&gt;Imagine you are a researcher studying in a massive university library (the database) containing millions of books. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cache-Aside (Lazy Loading):&lt;/strong&gt; You keep a blank personal notebook (the cache) on your desk. When you need a fact, you look in your notebook first. If it is there (a cache hit), you read it instantly. If not (a cache miss), you walk into the library archives, search the shelves, find the fact, write it down in your notebook, and then read it. &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write-Through:&lt;/strong&gt; Every time you discover a new piece of information, you write it in your personal notebook and immediately send a copy to the library's archiving desk to ensure both records are updated simultaneously before you proceed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write-Behind (Write-Back):&lt;/strong&gt; When you are brainstorming rapidly, you scribble your notes exclusively in your notebook. At the end of the day, you hand your notebook to a library assistant who updates the main archives in the background while you sleep.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;why-caching-strategies-matter-in-the-tech-industry&quot;&gt;Why Caching Strategies Matter in the Tech Industry&lt;/h4&gt;
&lt;p&gt;Without these strategies, modern platforms would collapse. If a viral social media post forces millions of users to fetch the same user profile directly from a database, the database will exhaust its connection pool, slow down, and eventually crash. &lt;/p&gt;
&lt;p&gt;Software engineers use these patterns strategically to balance speed and data integrity:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cache-Aside&lt;/strong&gt; prevents databases from being choked by heavy read traffic while keeping implementation simple.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write-Through&lt;/strong&gt; avoids data discrepancies, ensuring that critical data (like bank account balances) is never out of sync between the memory and the disk.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write-Behind&lt;/strong&gt; is utilized in write-heavy systems (like logging user activity or IoT sensor tracking) to group thousands of database writes into batch updates, protecting disk-based storage from wearing out or bottlenecking.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;implementing-cache-aside-in-node-js-amp-express&quot;&gt;Implementing Cache-Aside in Node.js &amp;amp; Express&lt;/h4&gt;
&lt;p&gt;Here is how you can implement a standard &lt;strong&gt;Cache-Aside&lt;/strong&gt; strategy using Express.js and MySQL. In this scenario, we check an in-memory cache object before querying our relational database.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const express = require('express');
const mysql = require('mysql2/promise');
const app = express();

// Mocking our fast memory cache (e.g., Redis)
const memoryCache = new Map();

// MySQL connection pool configuration
const dbPool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  database: 'production_db'
});

// Route utilizing Cache-Aside
app.get('/api/users/:id', async (req, res) =&amp;gt; {
  const userId = req.params.id;
  const cacheKey = `user:${userId}`;

  try {
    // 1. Check if the data exists in the high-speed cache
    if (memoryCache.has(cacheKey)) {
      console.log('Cache Hit! Serving from memory.');
      return res.json(memoryCache.get(cacheKey));
    }

    // 2. Cache Miss: Retrieve data from the slow MySQL database
    console.log('Cache Miss. Fetching from MySQL database...');
    const [rows] = await dbPool.query('SELECT id, name, email FROM users WHERE id = ?', [userId]);

    if (rows.length === 0) {
      return res.status(404).json({ error: 'User not found' });
    }

    const userData = rows[0];

    // 3. Write data to the cache so the next request is instant
    memoryCache.set(cacheKey, userData);

    return res.json(userData);
  } catch (error) {
    return res.status(500).json({ error: 'Internal Server Error' });
  }
});

app.listen(3000, () =&amp;gt; console.log('Server running on port 3000'));
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;key-takeaway&quot;&gt;Key Takeaway&lt;/h4&gt;
&lt;p&gt;Caching is not a one-size-fits-all solution; it is a careful compromise between performance and accuracy. While caching can make your application feel instantaneous, selecting the wrong update strategy can lead to stale data displays or lost records, making it vital to match your architectural pattern to your application's specific read-and-write ratios.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Originally published on my blog. You can &lt;a rel=&quot;nofollow&quot; href=&quot;https://sauravtbpandey.blogspot.com/2026/10/how-to-scale-your-database-guide-to.html&quot;&gt;read the alternative breakdown here&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30441/caching-strategies-demystified-cache-aside-write-through-and-write-behind</guid>
<pubDate>Fri, 09 Oct 2026 10:55:06 +0000</pubDate>
</item>
<item>
<title>Semantic Caching for LLM Apps Without Serving the Wrong Answer</title>
<link>https://coderlegion.com/28359/semantic-caching-for-llm-apps-without-serving-the-wrong-answer</link>
<description>&lt;h2 id=&quot;semantic-caching-for-llm-apps-without-serving-the-wrong-answer&quot;&gt;Semantic Caching for LLM Apps Without Serving the Wrong Answer&lt;/h2&gt;
&lt;p&gt;A customer asks your support assistant: &lt;em&gt;&quot;Can I get a refund if I cancel within 14 days?&quot;&lt;/em&gt; Your semantic cache finds a stored question that is almost word-for-word the same — &lt;em&gt;&quot;Can I get a refund if I cancel after 14 days?&quot;&lt;/em&gt; — scores it above your threshold, and returns the stored answer before the model is even called. The answer is fluent, confident, and exactly wrong.&lt;/p&gt;
&lt;p&gt;That exchange is the whole risk of semantic caching. The idea itself is sound: when a new question means the same thing as one you have already answered, reuse the answer and skip the model — no output tokens, no generation latency. The naive build (embed the question, take the nearest neighbour, serve it above 0.9) is also how you ship wrong answers at scale, because embedding similarity measures how close two questions are in &lt;em&gt;topic&lt;/em&gt;, not whether they have the same &lt;em&gt;answer&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;You can't tune your way out of that with a better number. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://arxiv.org/abs/2502.03771&quot;&gt;vCache paper&lt;/a&gt; (ICLR 2026) found that &quot;correct and incorrect cache hits have highly overlapping similarity distributions, suggesting that fixed thresholds are either unreliable or must be set extremely high to avoid errors.&quot; Separate &lt;a rel=&quot;nofollow&quot; href=&quot;https://arxiv.org/abs/2504.00584&quot;&gt;research on universal text embeddings&lt;/a&gt; found &quot;a significant lack of negation awareness&quot;, with models &quot;often interpreting negated text pairs as semantically similar.&quot; Negation, numbers and conditions are precisely the words that decide a support answer.&lt;/p&gt;
&lt;p&gt;So this article treats similarity as a way to &lt;em&gt;find candidates&lt;/em&gt;, never as the verdict: scoped keys, two thresholds, a deterministic guard for the words embeddings miss, a cheap verifier for the grey zone, invalidation tied to your sources, and a shadow-mode rollout that measures the false-hit rate before one cached answer reaches a user. It is the caching layer we cover when teaching how to &lt;a rel=&quot;nofollow&quot; href=&quot;https://cursuri-ai.ro/en/courses/build-and-ship-a-production-ai-saas&quot;&gt;ship an AI product to real traffic at Cursuri-AI.ro&lt;/a&gt;, written out in code.&lt;/p&gt;
&lt;h3 id=&quot;three-caches-that-get-confused&quot;&gt;Three caches that get confused&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Exact-match response cache &lt;/th&gt;&lt;th&gt;Semantic response cache &lt;/th&gt;&lt;th&gt;Provider prompt caching&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Key &lt;/td&gt;&lt;td&gt;Hash of the normalized request &lt;/td&gt;&lt;td&gt;Embedding of the question, inside a scope &lt;/td&gt;&lt;td&gt;Exact prefix of the prompt&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;What it skips &lt;/td&gt;&lt;td&gt;The whole model call &lt;/td&gt;&lt;td&gt;The whole model call &lt;/td&gt;&lt;td&gt;Re-processing the cached prefix&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Saves &lt;/td&gt;&lt;td&gt;Input, output, latency &lt;/td&gt;&lt;td&gt;Input, output, latency &lt;/td&gt;&lt;td&gt;Most of the input cost of the prefix&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Can it return a wrong answer? &lt;/td&gt;&lt;td&gt;Only if the key is incomplete &lt;/td&gt;&lt;td&gt;Yes, by design &lt;/td&gt;&lt;td&gt;No, the model still generates&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Lives in &lt;/td&gt;&lt;td&gt;Your infrastructure &lt;/td&gt;&lt;td&gt;Your infrastructure &lt;/td&gt;&lt;td&gt;The provider&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Prompt caching isn't a competitor. It makes re-reading a stable prefix cheap: on Anthropic's current lineup a cache read costs 10% of the base input price, 5% on Claude Opus 5.5 and 2.5% on Claude Fable 5.1. But the model still runs and still writes every output token. A semantic cache skips the model entirely.&lt;/p&gt;
&lt;p&gt;That shifts the business case. When your prompts already cache well, the input side of a request is cheap, and a semantic cache mostly buys you &lt;strong&gt;output tokens and latency&lt;/strong&gt;. Use both layers: a semantic-cache miss still goes to the model with a cached prefix.&lt;/p&gt;
&lt;h3 id=&quot;first-an-expected-value-check&quot;&gt;First, an expected-value check&lt;/h3&gt;
&lt;p&gt;A semantic cache has a benefit you can calculate and a cost teams usually leave out: the wrong answers it serves. Put both in one function before you build anything.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def monthly_value(
    requests: int,
    hit_rate: float,              # share of requests answered from cache
    false_hit_rate: float,        # share of cache hits that were wrong
    usd_per_generation: float,    # average cost of a fresh answer
    usd_per_wrong_answer: float,  # support ticket, refund, lost customer: your number
    usd_per_lookup: float,        # embedding call + index query
) -&amp;gt; tuple[float, int]:
    hits = requests * hit_rate
    wrong = hits * false_hit_rate
    value = (hits * usd_per_generation
             - requests * usd_per_lookup
             - wrong * usd_per_wrong_answer)
    return value, round(wrong)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it with illustrative numbers: 200,000 questions a month, a 25% hit rate, $0.02 per generated answer, and a wrong answer that costs $5 of support time. At a 2% false-hit rate the cache saves $1,000 of generation and serves 1,000 wrong answers worth $5,000: a net loss of about $4,000 before lookup costs. At 0.2% it serves 100 wrong answers and comes out about $500 ahead. &lt;strong&gt;The hit rate sets how much you can save. The false-hit rate decides whether you save anything at all.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Where the arithmetic tends to work: support and FAQ assistants, documentation Q&amp;amp;A, internal help desks, product questions — high repetition, long answers, one correct answer for everyone in a scope. That is the territory of &lt;a rel=&quot;nofollow&quot; href=&quot;https://cursuri-ai.ro/en/courses/ai-for-customer-support-and-service&quot;&gt;AI-assisted customer support&lt;/a&gt;, and it is where the rest of this design pays for itself. Where it doesn't work: coding agents, anything personalized, multi-step tool use, creative generation (users expect variety), and questions about live state such as &quot;is the API down right now?&quot;.&lt;/p&gt;
&lt;h3 id=&quot;the-design-similarity-proposes-rules-decide&quot;&gt;The design: similarity proposes, rules decide&lt;/h3&gt;
&lt;p&gt;Every request goes through the same six steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Normalize&lt;/strong&gt; the question (case, whitespace; multi-turn chats are covered further down).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scope&lt;/strong&gt; it: compute a key from everything that can change the correct answer — tenant, locale, plan, model, prompt version, knowledge-base version. Lookups never cross scopes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exact match&lt;/strong&gt; first: a hash lookup catches verbatim repeats with no similarity risk.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Nearest neighbour&lt;/strong&gt; within the scope.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Guard&lt;/strong&gt;: below the low threshold, a miss. Different critical tokens, a miss. Above the high threshold, a hit. In between, verify.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On a miss&lt;/strong&gt;, generate the answer, then decide &lt;em&gt;at write time&lt;/em&gt; whether it may be cached at all.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;the-table&quot;&gt;The table&lt;/h4&gt;
&lt;p&gt;Postgres with &lt;code&gt;pgvector&lt;/code&gt; is enough for this; use Redis or a vector store if that is what you already run. Storage is not the hard part.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE answer_cache (
    id              BIGSERIAL    PRIMARY KEY,
    scope_key       TEXT         NOT NULL,  -- hash of tenant, locale, plan, model, prompt and KB versions
    question        TEXT         NOT NULL,  -- normalized question
    question_hash   TEXT         NOT NULL,  -- exact-match layer
    question_sig    TEXT         NOT NULL,  -- critical tokens, see below
    embedding       VECTOR(1024) NOT NULL,  -- match your embedding model's dimension
    answer          TEXT         NOT NULL,
    source_doc_ids  TEXT[]       NOT NULL DEFAULT '{}',
    created_at      TIMESTAMPTZ  NOT NULL DEFAULT now(),
    expires_at      TIMESTAMPTZ  NOT NULL,
    hit_count       INT          NOT NULL DEFAULT 0,
    UNIQUE (scope_key, question_hash)
);

CREATE INDEX ON answer_cache USING hnsw (embedding vector_cosine_ops);
CREATE INDEX ON answer_cache USING gin (source_doc_ids);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One &lt;code&gt;pgvector&lt;/code&gt; detail bites multi-tenant caches. As the README puts it, &quot;with approximate indexes, filtering is applied &lt;em&gt;after&lt;/em&gt; the index is scanned&quot;. With many scopes, the nearest neighbours the HNSW scan returns can all belong to other tenants, the &lt;code&gt;WHERE scope_key = …&lt;/code&gt; filter removes them, and you get zero rows: a miss that isn't one. Turn on iterative index scans (pgvector 0.8.0+, &lt;code&gt;SET hnsw.iterative_scan = relaxed_order&lt;/code&gt;), use partial indexes for a few large scopes, or partition when there are many.&lt;/p&gt;
&lt;h4 id=&quot;the-lookup&quot;&gt;The lookup&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import hashlib
from dataclasses import dataclass

HIGH, LOW = 0.95, 0.88   # placeholders, not recommendations: calibrate them (see below)

@dataclass
class Candidate:
    id: int
    question: str
    answer: str
    similarity: float

def normalize(q: str) -&amp;gt; str:
    return &quot; &quot;.join(q.lower().split())

def lookup(db, scope_key: str, question: str) -&amp;gt; tuple[str, Candidate | None]:
    q = normalize(question)
    exact = db.fetchone(
        &quot;SELECT id, question, answer FROM answer_cache &quot;
        &quot;WHERE scope_key = %s AND question_hash = %s AND expires_at &amp;gt; now()&quot;,
        (scope_key, hashlib.sha256(q.encode()).hexdigest()),
    )
    if exact:
        return &quot;hit&quot;, Candidate(exact[&quot;id&quot;], exact[&quot;question&quot;], exact[&quot;answer&quot;], 1.0)

    vec = embed(q)   # normalized vector; pgvector's psycopg adapter registered
    row = db.fetchone(
        &quot;&quot;&quot;
        SELECT id, question, question_sig, answer,
               1 - (embedding &amp;lt;=&amp;gt; %s) AS similarity
          FROM answer_cache
         WHERE scope_key = %s AND expires_at &amp;gt; now()
         ORDER BY embedding &amp;lt;=&amp;gt; %s
         LIMIT 1
        &quot;&quot;&quot;,
        (vec, scope_key, vec),
    )
    if row is None or row[&quot;similarity&quot;] &amp;lt; LOW:
        return &quot;miss&quot;, None
    if row[&quot;question_sig&quot;] != critical_signature(q):
        return &quot;miss&quot;, None          # same topic, different negation, number or condition
    cand = Candidate(row[&quot;id&quot;], row[&quot;question&quot;], row[&quot;answer&quot;], row[&quot;similarity&quot;])
    return (&quot;hit&quot; if cand.similarity &amp;gt;= HIGH else &quot;verify&quot;), cand
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;=&amp;gt;&lt;/code&gt; is &lt;code&gt;pgvector&lt;/code&gt;'s cosine distance, so &lt;code&gt;1 - distance&lt;/code&gt; is the similarity you compare against thresholds. The function returns one of three states instead of a boolean, and that third state is the point of the design.&lt;/p&gt;
&lt;h3 id=&quot;guard-the-words-embeddings-miss&quot;&gt;Guard the words embeddings miss&lt;/h3&gt;
&lt;p&gt;Embeddings are weakest exactly where support answers are most sensitive: negation, numbers and conditions. &lt;em&gt;Within&lt;/em&gt; 14 days versus &lt;em&gt;after&lt;/em&gt; 14 days, &lt;em&gt;with&lt;/em&gt; versus &lt;em&gt;without&lt;/em&gt;, &lt;em&gt;EU&lt;/em&gt; versus &lt;em&gt;US&lt;/em&gt;. A cheap, deterministic check runs before any model is involved: pull out the tokens that flip meaning, and require them to match.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import re

NEGATIONS = re.compile(r&quot;\b(not|no|never|without|except|unless|cannot|can't|don't|doesn't|isn't|won't)\b&quot;)
NUMBERS = re.compile(r&quot;\d+(?:[.,]\d+)?&quot;)
CONDITIONS = re.compile(r&quot;\b(before|after|within|over|under|more than|less than|at least|at most)\b&quot;)
DOMAIN_TERMS = {&quot;eu&quot;, &quot;us&quot;, &quot;uk&quot;, &quot;basic&quot;, &quot;pro&quot;, &quot;enterprise&quot;, &quot;ios&quot;, &quot;android&quot;}  # your vocabulary

def critical_signature(q: str) -&amp;gt; str:
    words = set(re.findall(r&quot;[a-z0-9']+&quot;, q))
    parts = [
        &quot;,&quot;.join(sorted(set(NEGATIONS.findall(q)))),
        &quot;,&quot;.join(sorted(set(NUMBERS.findall(q)))),
        &quot;,&quot;.join(sorted(set(CONDITIONS.findall(q)))),
        &quot;,&quot;.join(sorted(words &amp;amp; DOMAIN_TERMS)),
    ]
    return &quot;|&quot;.join(parts)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The refund example from the opening now fails the guard (&lt;code&gt;within&lt;/code&gt; ≠ &lt;code&gt;after&lt;/code&gt;) and becomes a miss, without any model deciding anything.&lt;/p&gt;
&lt;p&gt;This check is deliberately blunt. It will turn some genuine paraphrases into misses (&quot;I can't log in&quot; and &quot;login is not working&quot; carry different negation tokens), and that is the right trade: a miss costs one generation, a false hit costs a wrong answer. Have whoever owns the product catalog own &lt;code&gt;DOMAIN_TERMS&lt;/code&gt; too, so a new plan name doesn't quietly become a blind spot.&lt;/p&gt;
&lt;h3 id=&quot;the-grey-zone-verify-don-t-guess&quot;&gt;The grey zone: verify, don't guess&lt;/h3&gt;
&lt;p&gt;Between the two thresholds, ask a small, fast model one narrow question: does the stored answer fully answer the new question? Structured outputs keep the reply machine-readable.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import json
import anthropic

client = anthropic.Anthropic()
VERIFIER_MODEL = &quot;claude-haiku-4-5-20251001&quot;   # the smallest current model you trust

VERIFY_SYSTEM = (
    &quot;You decide whether a stored answer can be reused for a new question. &quot;
    &quot;reusable is true only if the stored answer fully and correctly answers the new &quot;
    &quot;question: nothing the new question asks is missing, and no condition in the new &quot;
    &quot;question (dates, amounts, negations, products, regions) changes the answer.&quot;
)

REUSABLE = {
    &quot;type&quot;: &quot;object&quot;,
    &quot;properties&quot;: {&quot;reusable&quot;: {&quot;type&quot;: &quot;boolean&quot;}},
    &quot;required&quot;: [&quot;reusable&quot;],
    &quot;additionalProperties&quot;: False,
}

def still_answers(new_question: str, cand: Candidate) -&amp;gt; bool:
    try:
        resp = client.messages.create(
            model=VERIFIER_MODEL,
            max_tokens=100,
            system=VERIFY_SYSTEM,
            output_config={&quot;format&quot;: {&quot;type&quot;: &quot;json_schema&quot;, &quot;schema&quot;: REUSABLE}},
            messages=[{&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: (
                f&quot;&amp;lt;new_question&amp;gt;{new_question}&amp;lt;/new_question&amp;gt;\n&quot;
                f&quot;&amp;lt;stored_question&amp;gt;{cand.question}&amp;lt;/stored_question&amp;gt;\n&quot;
                f&quot;&amp;lt;stored_answer&amp;gt;{cand.answer}&amp;lt;/stored_answer&amp;gt;&quot;
            )}],
        )
        text = &quot;&quot;.join(b.text for b in resp.content if b.type == &quot;text&quot;)
        return json.loads(text)[&quot;reusable&quot;] is True
    except (anthropic.APIError, json.JSONDecodeError, KeyError):
        return False    # any doubt is a miss, including an outage or a retired model ID
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At $1/$5 per million tokens, Haiku 4.5 turns a verification into a short prompt and a one-field answer: a small fraction of a full generation on a frontier model, on the model Anthropic lists as its fastest. A verified hit is still slower than a clean one, since it adds a round trip, so narrow the grey zone once your data shows where it is. Keep the verifier's model ID in one constant, because small models get retired like any other, and notice that every failure path returns a miss. A verifier that fails open turns an outage into a stream of unchecked cache hits.&lt;/p&gt;
&lt;p&gt;Put together, the request path is short:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def answer(db, request) -&amp;gt; str:
    scope = scope_key_for(request)
    state, cand = lookup(db, scope, request.question)
    if state == &quot;hit&quot; or (state == &quot;verify&quot; and still_answers(request.question, cand)):
        db.execute(&quot;UPDATE answer_cache SET hit_count = hit_count + 1 WHERE id = %s&quot;, (cand.id,))
        return cand.answer
    result = generate(request)                 # your normal model call
    if cacheable(result):
        store(db, scope, request.question, result)
    return result.text

def store(db, scope: str, question: str, result, ttl_hours: int = 24) -&amp;gt; None:
    q = normalize(question)
    db.execute(
        &quot;&quot;&quot;
        INSERT INTO answer_cache (scope_key, question, question_hash, question_sig,
                                  embedding, answer, source_doc_ids, expires_at)
        VALUES (%s, %s, %s, %s, %s, %s, %s, now() + make_interval(hours =&amp;gt; %s))
        ON CONFLICT (scope_key, question_hash) DO NOTHING
        &quot;&quot;&quot;,
        (scope, q, hashlib.sha256(q.encode()).hexdigest(), critical_signature(q),
         embed(q), result.text, result.source_doc_ids, ttl_hours),
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;source_doc_ids&lt;/code&gt; records which documents the answer was built from. It looks like bookkeeping until the day a policy page changes; the invalidation section below is where it pays off.&lt;/p&gt;
&lt;h3 id=&quot;calibrate-thresholds-on-your-own-data&quot;&gt;Calibrate thresholds on your own data&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;HIGH&lt;/code&gt; and &lt;code&gt;LOW&lt;/code&gt; are not constants you copy from a blog post, this one included. They depend on your embedding model, your domain and your users. Measure them:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Take a few weeks of logged questions. For each one, find its nearest earlier neighbour in the same scope and record the similarity.&lt;/li&gt;
&lt;li&gt;Label a few hundred of those pairs: &lt;em&gt;same answer&lt;/em&gt; or &lt;em&gt;different answer&lt;/em&gt;. Humans are best; an LLM judge with human spot-checks works if you audit it.&lt;/li&gt;
&lt;li&gt;Pick the lowest threshold that meets your error budget, and look at how much traffic it leaves you.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def pick_threshold(
    pairs: list[tuple[float, bool]],    # (similarity, same_answer)
    max_false_hit: float = 0.01,
    min_support: int = 50,
) -&amp;gt; tuple[float, float] | None:
    for t in sorted({s for s, _ in pairs}):
        served = [same for s, same in pairs if s &amp;gt;= t]
        if len(served) &amp;lt; min_support:
            return None                  # too little data above this point to trust
        if served.count(False) / len(served) &amp;lt;= max_false_hit:
            return t, len(served) / len(pairs)   # threshold, share of candidates served
    return None
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If it returns &lt;code&gt;None&lt;/code&gt; on a few hundred labeled pairs, that is a finding, not a bug: no single threshold keeps your error budget on this data, which is exactly the overlap vCache describes. You then need the verifier for everything above &lt;code&gt;LOW&lt;/code&gt;, or per-question thresholds, or no semantic layer at all (exact-match only). Treat this the way you would treat &lt;a rel=&quot;nofollow&quot; href=&quot;https://cursuri-ai.ro/en/courses/rag-retrieval-augmented-generation-in-practice&quot;&gt;any retrieval quality problem&lt;/a&gt;: a labeled dataset and a metric, not intuition about what 0.9 &quot;means&quot;.&lt;/p&gt;
&lt;h3 id=&quot;scope-or-leak-the-cache-is-a-cross-user-channel&quot;&gt;Scope or leak: the cache is a cross-user channel&lt;/h3&gt;
&lt;p&gt;A semantic cache that serves user B an answer generated for user A is, by construction, a way to move data between users. If A's answer was built from A's account data, A's private documents or A's conversation, that is a data breach delivered with low latency.&lt;/p&gt;
&lt;p&gt;Two rules close it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The scope key includes everything that can change the answer.&lt;/strong&gt; Tenant, locale, the user's plan (entitlements change answers), model ID, system prompt version, and a version of the knowledge base the answer was built from:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def scope_key_for(request) -&amp;gt; str:
    parts = [request.tenant_id, request.locale, request.plan,
             MODEL_ID, PROMPT_VERSION, KB_SNAPSHOT_ID]
    return hashlib.sha256(&quot;|&quot;.join(parts).encode()).hexdigest()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Model and prompt versions in the key mean a deploy that changes either one starts a fresh cache automatically, and the old entries simply expire.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cacheability is decided at write time, from what the generation actually touched.&lt;/strong&gt; Before generating, you often can't tell whether a question needs private data; afterwards, you know exactly what the model called and read:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def cacheable(result) -&amp;gt; bool:
    return (
        result.stop_reason == &quot;end_turn&quot;
        and not result.used_account_tools        # looked up this user's orders, invoices…
        and not result.read_private_documents    # retrieval hit anything not visible to the whole scope
        and not result.used_conversation_history
        and not result.flagged_by_guardrails
        and bool(result.text.strip())
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The same channel runs the other way, too. If someone can get a harmful answer generated once — a prompt-injected link, an offensive reply — a shared cache will serve it to everyone who asks something similar. Cache only answers that passed your output guardrails; consider promoting an answer to the shared cache only after the same question has come from several distinct users with no negative feedback; and show a &quot;cached answer&quot; marker with a regenerate button, which also gives you a false-hit signal at no extra cost. Poisoning, cross-tenant leakage and injection belong on the same threat model when you're &lt;a rel=&quot;nofollow&quot; href=&quot;https://cursuri-ai.ro/en/courses/ai-security-defending-llm-applications&quot;&gt;securing an LLM application&lt;/a&gt;; the cache is just one more component that stores model output.&lt;/p&gt;
&lt;h3 id=&quot;invalidation-tie-entries-to-their-sources&quot;&gt;Invalidation: tie entries to their sources&lt;/h3&gt;
&lt;p&gt;A cached answer is correct until the thing it was based on changes. Three mechanisms, from coarse to precise:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TTL by content class.&lt;/strong&gt; Prices and policies: hours to a day. How-to documentation: days to weeks. Nothing lives forever.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Versions in the scope key.&lt;/strong&gt; A new model, prompt or knowledge-base snapshot means a new scope; nothing needs deleting.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Purge by source.&lt;/strong&gt; Store the IDs of the documents each answer was built from, and delete the dependent entries when one changes:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def purge_for_document(db, doc_id: str) -&amp;gt; int:
    return db.execute(
        &quot;DELETE FROM answer_cache WHERE source_doc_ids @&amp;gt; ARRAY[%s]::text[]&quot;,
        (doc_id,),
    ).rowcount
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hook it to whatever already re-indexes your RAG corpus: the moment a document is re-embedded, the answers built on its old version go away. Of the three, this is the one teams skip, and it is the one that explains why the bot quoted last quarter's refund policy.&lt;/p&gt;
&lt;h3 id=&quot;multi-turn-conversations&quot;&gt;Multi-turn conversations&lt;/h3&gt;
&lt;p&gt;&quot;And for the Enterprise plan?&quot; means nothing without the previous turn, and embedding it on its own will match whatever else was asked about Enterprise. Two safe options: cache only first-turn questions that stand on their own, or rewrite each follow-up into a standalone question with a small model before lookup. The rewrite costs a call on every multi-turn request, which eats into the savings, so measure whether it pays. Never key the cache on the raw last message of a conversation.&lt;/p&gt;
&lt;h3 id=&quot;roll-it-out-in-shadow-mode&quot;&gt;Roll it out in shadow mode&lt;/h3&gt;
&lt;p&gt;Don't find out your false-hit rate from customers. Run the cache in shadow first: on every request, do the lookup, still generate a fresh answer, serve the fresh one, and log both.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def handle(db, request) -&amp;gt; str:
    scope = scope_key_for(request)
    state, cand = lookup(db, scope, request.question)
    result = generate(request)
    if state != &quot;miss&quot;:
        log_shadow(request.question, cand, result.text, state)   # judged offline
    if cacheable(result):
        store(db, scope, request.question, result)
    return result.text
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Offline, have a judge (with human spot-checks) decide whether each would-be cached answer was equivalent to the fresh one. After a week or two you have a measured false-hit rate per state, &lt;code&gt;hit&lt;/code&gt; and &lt;code&gt;verify&lt;/code&gt;, at your current thresholds, and &lt;code&gt;monthly_value()&lt;/code&gt; stops being a guess. Switch serving on only when the numbers clear your budget, and keep a kill switch. In production, track four things: hit rate, measured false-hit rate (keep sampling), latency for hits and misses, and negative feedback on cached versus fresh answers.&lt;/p&gt;
&lt;h3 id=&quot;cached-answers-are-personal-data-processing-too&quot;&gt;Cached answers are personal data processing too&lt;/h3&gt;
&lt;p&gt;Anything you store about users' questions and your answers to them falls under the GDPR if it can relate to a person. Three consequences for the cache. Shared entries must never contain personal data, which the write-time rule above enforces. TTLs are your retention policy, so write them down as one. And if you keep any per-user cache, it belongs in your erasure flow: &quot;delete my data&quot; has to reach the cache, not only the main database. Designing &lt;a rel=&quot;nofollow&quot; href=&quot;https://cursuri-ai.ro/en/courses/ai-data-privacy-and-eu-ai-act-compliance&quot;&gt;data protection into AI systems&lt;/a&gt; from the start is cheap; bolting it on after an audit is not.&lt;/p&gt;
&lt;h3 id=&quot;faq&quot;&gt;FAQ&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;What similarity threshold should I use for semantic caching?&lt;/strong&gt;&lt;br&gt;
There is no universal value. Similarities depend on the embedding model and the domain, and correct and incorrect hits overlap. Calibrate on labeled pairs from your own traffic, use two thresholds with a verifier in between, and accept that some workloads have no safe threshold at all.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Is semantic caching the same as prompt caching?&lt;/strong&gt;&lt;br&gt;
No. Prompt caching lives at the provider, discounts re-reading an identical prompt prefix, and still generates a fresh answer, so it can't return a wrong one. Semantic caching lives in your infrastructure and skips generation entirely. They stack: a semantic miss still benefits from a cached prefix.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Should I use Redis or Postgres for a semantic cache?&lt;/strong&gt;&lt;br&gt;
Whichever you already operate. Both can store vectors and run approximate nearest-neighbour search. The hard parts are scoping, thresholds and invalidation, and none of those depend on the store.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can I semantically cache an AI agent's responses?&lt;/strong&gt;&lt;br&gt;
Not end to end. Agent runs call tools, read live state and cause side effects. Cache below the agent instead: exact-match caching of read-only tool results, with short TTLs, carries far less risk.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How do I know the cache is serving wrong answers?&lt;/strong&gt;&lt;br&gt;
Shadow mode before launch, then continuous sampling: a judge compares a sample of cached answers against fresh generations every week. Add a visible &quot;cached&quot; marker with a regenerate option, and watch whether users regenerate cached answers more often than fresh ones.&lt;/p&gt;
&lt;h3 id=&quot;the-short-version&quot;&gt;The short version&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Embedding similarity tells you two questions are about the same thing, not that they have the same answer.&lt;/li&gt;
&lt;li&gt;Do the expected-value check first: the false-hit rate decides whether the cache saves money at all.&lt;/li&gt;
&lt;li&gt;Scope every lookup by tenant, plan, locale, model, prompt and knowledge-base version. Decide cacheability at write time from what the generation touched.&lt;/li&gt;
&lt;li&gt;Guard negations, numbers and conditions deterministically, and verify the grey zone with a small model.&lt;/li&gt;
&lt;li&gt;Calibrate thresholds on labeled pairs from your own traffic, and expect overlap.&lt;/li&gt;
&lt;li&gt;Purge answers when their source documents change, roll out in shadow mode, and keep a kill switch.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A semantic cache built this way is less aggressive than the one in most tutorials. It serves fewer hits, and every hit it serves has cleared more than a similarity score.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;I build and teach production AI systems at &lt;a rel=&quot;nofollow&quot; href=&quot;https://cursuri-ai.ro/en/courses&quot;&gt;Cursuri-AI.ro&lt;/a&gt;, Eastern Europe's AI education platform — hands-on courses on shipping LLM features that stay correct, secure and affordable under real traffic.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28359/semantic-caching-for-llm-apps-without-serving-the-wrong-answer</guid>
<pubDate>Wed, 23 Sep 2026 14:39:11 +0000</pubDate>
</item>
<item>
<title>The Difference Between a Cache and a Lie</title>
<link>https://coderlegion.com/24933/the-difference-between-a-cache-and-a-lie</link>
<description>&lt;p&gt;A cache is a promise with an expiration date.&lt;/p&gt;
&lt;p&gt;That sounds obvious until a production system starts depending on cached state as if it were truth. Then the cache is no longer an optimization. It is a second source of truth with worse durability, weaker ownership, and fewer people watching it.&lt;/p&gt;
&lt;p&gt;That is where cache bugs become painful.&lt;/p&gt;
&lt;p&gt;The hard part of caching is not storing a value. The hard part is deciding what the value is allowed to mean.&lt;/p&gt;
&lt;h3 id=&quot;a-cache-should-have-a-narrow-job&quot;&gt;A Cache Should Have a Narrow Job&lt;/h3&gt;
&lt;p&gt;The safest caches answer narrow questions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Have we recently computed this result?
Have we recently seen this object absent?
Have we recently fetched this configuration?
Can we avoid doing the same expensive work again?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The more dangerous caches answer authority-shaped questions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Does this object exist?
Is this user allowed?
Is this state current?
Has this workflow completed?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Sometimes a cache must sit near one of those questions. That is fine. But the design needs to say exactly how wrong the cache is allowed to be and what happens when it is wrong.&lt;/p&gt;
&lt;h3 id=&quot;the-three-questions-every-cache-needs&quot;&gt;The Three Questions Every Cache Needs&lt;/h3&gt;
&lt;p&gt;Before adding a cache, I like to ask three questions.&lt;/p&gt;
&lt;p&gt;First:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;What is the source of truth?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the team cannot answer this clearly, the cache will eventually become part of the data model by accident.&lt;/p&gt;
&lt;p&gt;Second:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;What is the failure mode when the cache is unavailable?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fail-open and fail-closed are both valid choices, but they belong to different classes of systems. A performance cache in front of an idempotent delete path can usually fail open. An authorization decision probably cannot.&lt;/p&gt;
&lt;p&gt;Third:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;What is the failure mode when the cache is stale?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is where many designs get uncomfortable. A stale cache entry might mean an extra database read, which is fine. It might mean stale search results for a minute, which may be acceptable. It might mean granting access after permission was revoked, which is not the same kind of risk.&lt;/p&gt;
&lt;h3 id=&quot;ttl-is-not-invalidation&quot;&gt;TTL Is Not Invalidation&lt;/h3&gt;
&lt;p&gt;TTL is a recovery mechanism. It is not a complete correctness story.&lt;/p&gt;
&lt;p&gt;A TTL says:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;If we miss an invalidation, the system will eventually heal.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It does not say:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;The system is correct until the TTL expires.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If stale data can cause a correctness bug, the system still needs an invalidation path, a version check, a source-of-truth read, or a design that makes stale data harmless.&lt;/p&gt;
&lt;p&gt;TTL is useful because production systems are imperfect. It should not be the only thing standing between the user and a wrong answer.&lt;/p&gt;
&lt;h3 id=&quot;fail-open-vs-fail-closed&quot;&gt;Fail-Open vs Fail-Closed&lt;/h3&gt;
&lt;p&gt;The right behavior depends on what the cache protects.&lt;/p&gt;
&lt;p&gt;For a performance cache:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;cache hit   -&amp;gt; use cached result
cache miss  -&amp;gt; recompute or read from source
cache error -&amp;gt; bypass cache
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This keeps correctness independent of cache availability.&lt;/p&gt;
&lt;p&gt;For a safety or authorization cache, bypassing the cache may be wrong. If the cache is the only fast path to know whether an operation is allowed, fail-open can turn an outage into a security issue. In those systems, the right answer may be to fail closed, degrade the feature, or require a source-of-truth check.&lt;/p&gt;
&lt;p&gt;The cache design should make that tradeoff explicit.&lt;/p&gt;
&lt;h3 id=&quot;when-a-cache-becomes-a-lie&quot;&gt;When a Cache Becomes a Lie&lt;/h3&gt;
&lt;p&gt;A cache becomes a lie when the rest of the system forgets that it is allowed to be wrong.&lt;/p&gt;
&lt;p&gt;Common warning signs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;no documented source of truth&lt;/li&gt;
&lt;li&gt;no owner for invalidation&lt;/li&gt;
&lt;li&gt;no metric for hit rate, miss rate, or stale reads&lt;/li&gt;
&lt;li&gt;business logic that only exists in cache-writing code&lt;/li&gt;
&lt;li&gt;cache entries with no version, generation, or TTL&lt;/li&gt;
&lt;li&gt;code that treats cache errors as successful reads&lt;/li&gt;
&lt;li&gt;tests that mock the cache but never test cache loss&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The worst cache bugs are not caused by Redis being down. They are caused by Redis being trusted too much.&lt;/p&gt;
&lt;h3 id=&quot;the-lesson&quot;&gt;The Lesson&lt;/h3&gt;
&lt;p&gt;A good cache makes a system faster without becoming more important than the data it stores.&lt;/p&gt;
&lt;p&gt;It is allowed to be missing. It is allowed to be cold. It is allowed to be flushed. It may be useful when present and boring when absent.&lt;/p&gt;
&lt;p&gt;The moment a cache cannot be lost, rebuilt, bypassed, or corrected, it is no longer just a cache.&lt;/p&gt;
&lt;p&gt;It is part of the system of record, whether the architecture diagram admits it or not.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24933/the-difference-between-a-cache-and-a-lie</guid>
<pubDate>Wed, 19 Aug 2026 15:51:36 +0000</pubDate>
</item>
<item>
<title>Why I Prefer Boring Caches</title>
<link>https://coderlegion.com/24339/why-i-prefer-boring-caches</link>
<description>&lt;p&gt;I like caches that are easy to delete.&lt;/p&gt;
&lt;p&gt;That may sound strange, because the whole point of a cache is that it improves something important: latency, cost, load, or availability. But the safest caches are the ones the system can survive without. They make the steady state better without becoming the only thing holding correctness together.&lt;/p&gt;
&lt;p&gt;That usually means the cache is boring.&lt;/p&gt;
&lt;p&gt;Exact keys. Simple values. Clear TTLs. Obvious invalidation. Metrics. A fallback path. No clever inference that future maintainers have to rediscover during an incident.&lt;/p&gt;
&lt;h3 id=&quot;what-makes-a-cache-boring&quot;&gt;What Makes a Cache Boring&lt;/h3&gt;
&lt;p&gt;A boring cache has a small contract.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Given this exact key, return the recently stored value if it exists.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It does not try to become a database. It does not hide business rules inside serialization code. It does not require five services to update it in exactly the right order. It does not make correctness depend on a best-effort write that nobody can replay.&lt;/p&gt;
&lt;p&gt;The boring cache is an optimization. The source of truth remains somewhere else.&lt;/p&gt;
&lt;h3 id=&quot;exact-beats-clever&quot;&gt;Exact Beats Clever&lt;/h3&gt;
&lt;p&gt;Approximate structures are powerful, but they are easy to misuse.&lt;/p&gt;
&lt;p&gt;Bloom filters, Cuckoo filters, sampled caches, and lossy sketches all have places where they shine. They are excellent when the direction of error is safe. They are risky when a false positive or an operationally incomplete view can suppress work that should have happened.&lt;/p&gt;
&lt;p&gt;Before using an approximate cache, ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What does a false positive do?&lt;/li&gt;
&lt;li&gt;What does a false negative do?&lt;/li&gt;
&lt;li&gt;Can the structure be rebuilt completely?&lt;/li&gt;
&lt;li&gt;What happens if updates are missed during an outage?&lt;/li&gt;
&lt;li&gt;Is the approximation mathematical, operational, or both?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The last question matters. A data structure may have no false negatives in theory while the system still has false negatives because the structure did not observe every write.&lt;/p&gt;
&lt;p&gt;That is not a data structure problem. It is a system design problem.&lt;/p&gt;
&lt;h3 id=&quot;ttls-are-a-safety-net&quot;&gt;TTLs Are a Safety Net&lt;/h3&gt;
&lt;p&gt;I like TTLs because they give a cache a half-life.&lt;/p&gt;
&lt;p&gt;They make stale state temporary. They make missed invalidations survivable. They make emergency flushes less scary because the system was already designed for cache entries to disappear.&lt;/p&gt;
&lt;p&gt;But TTLs should be chosen with the error mode in mind.&lt;/p&gt;
&lt;p&gt;A five-minute stale product description may be harmless. A five-minute stale permission decision may be unacceptable. A one-hour absence marker may be fine if it only suppresses repeated no-op work and is cleared by the create path.&lt;/p&gt;
&lt;p&gt;The TTL is not just a performance setting. It is a correctness boundary.&lt;/p&gt;
&lt;h3 id=&quot;observability-is-part-of-the-design&quot;&gt;Observability Is Part of the Design&lt;/h3&gt;
&lt;p&gt;A cache without metrics is a superstition.&lt;/p&gt;
&lt;p&gt;At minimum, I want to know:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;hit rate&lt;/li&gt;
&lt;li&gt;miss rate&lt;/li&gt;
&lt;li&gt;error rate&lt;/li&gt;
&lt;li&gt;latency&lt;/li&gt;
&lt;li&gt;evictions or expirations&lt;/li&gt;
&lt;li&gt;size or key count&lt;/li&gt;
&lt;li&gt;fallback rate&lt;/li&gt;
&lt;li&gt;invalidation rate&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The most useful cache metric is often not hit rate. It is fallback behavior. If the cache fails, does the system keep working? If the hit rate drops, do downstream dependencies absorb the load? If invalidation spikes, is that expected or is a writer misbehaving?&lt;/p&gt;
&lt;p&gt;The cache should tell operators when it is helping and when it is merely present.&lt;/p&gt;
&lt;h3 id=&quot;the-fallback-path-must-stay-healthy&quot;&gt;The Fallback Path Must Stay Healthy&lt;/h3&gt;
&lt;p&gt;The fallback path is production code.&lt;/p&gt;
&lt;p&gt;If the cache normally handles 95 percent of reads, it is tempting to ignore the uncached path. That path may be slower, less tested, or underprovisioned. Then the cache has an incident and the fallback path collapses under traffic it has not seen in months.&lt;/p&gt;
&lt;p&gt;A boring cache design keeps the fallback path honest:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;load test without the cache&lt;/li&gt;
&lt;li&gt;sample bypasses in production when safe&lt;/li&gt;
&lt;li&gt;alert on fallback latency&lt;/li&gt;
&lt;li&gt;keep source-of-truth reads efficient enough to survive cache loss&lt;/li&gt;
&lt;li&gt;document the emergency flush procedure&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The cache is only optional if the fallback really works.&lt;/p&gt;
&lt;h3 id=&quot;the-lesson-1&quot;&gt;The Lesson&lt;/h3&gt;
&lt;p&gt;Clever caches often move complexity. Boring caches remove it.&lt;/p&gt;
&lt;p&gt;They are exact where correctness needs exactness. They are bounded where memory or staleness matters. They are observable enough to prove they help. They degrade in a way the rest of the system understands.&lt;/p&gt;
&lt;p&gt;The best cache does not make people ask, &quot;What does this thing know?&quot;&lt;/p&gt;
&lt;p&gt;It makes people say, &quot;If it is there, use it. If it is not, carry on.&quot;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24339/why-i-prefer-boring-caches</guid>
<pubDate>Tue, 11 Aug 2026 14:30:00 +0000</pubDate>
</item>
<item>
<title>Five Caching Strategies Every Backend Dev Should Know</title>
<link>https://coderlegion.com/14654/five-caching-strategies-every-backend-dev-should-know</link>
<description>
&lt;p&gt;&lt;img src=&quot;https://blog.ratnesh-maurya.com/images/blog/Distributed-Caching-Strategies-Architecture.png&quot; alt=&quot;Five Caching Strategies Every Backend Dev Should Know&quot;&gt;&lt;/p&gt;&lt;h1&gt;Five Caching Strategies Every Backend Dev Should Know&lt;/h1&gt;
&lt;p&gt;If you build backend systems, you eventually run into &lt;strong&gt;caching&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Someone says &amp;ldquo;just put Redis in front of it&amp;rdquo; and suddenly your app is faster&amp;hellip; until you start seeing stale data, cache misses, and weird bugs when you deploy.&lt;/p&gt;
&lt;p&gt;This post explains &lt;strong&gt;five core caching strategies&lt;/strong&gt; in plain language:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cache-Aside&lt;/li&gt;
&lt;li&gt;Read-Through&lt;/li&gt;
&lt;li&gt;Write-Through&lt;/li&gt;
&lt;li&gt;Write-Back&lt;/li&gt;
&lt;li&gt;Write-Around&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You&amp;rsquo;ll see &lt;strong&gt;how each one works&lt;/strong&gt;, &lt;strong&gt;what can go wrong&lt;/strong&gt;, and &lt;strong&gt;when to use it&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A cache is just a faster, smaller copy of some data.
The hard part is &lt;strong&gt;keeping that copy useful&lt;/strong&gt;: when to fill it, when to read from it, and when to update or skip it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;1. Cache-Aside (Lazy Loading)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Idea:&lt;/strong&gt;
The application talks to the cache and the database.
It &amp;ldquo;checks the cache first, then falls back to the DB&amp;rdquo; and fills the cache on a miss.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;If the key is in the cache (a hit), just return it. Super fast.


If the key is missing, the app queries the database instead.


The app stores the result in the cache and returns it to the user.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why people love it&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Simple and flexible&lt;/strong&gt;: logic lives in your code; you control what goes in the cache.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Resilient&lt;/strong&gt;: if the cache dies, the app can still read from the DB (just slower).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory-efficient&lt;/strong&gt;: only data that is actually read gets cached.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What can go wrong&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;first&lt;/strong&gt; request for a key is slow (cold cache).&lt;/li&gt;
&lt;li&gt;If the DB is updated &lt;strong&gt;outside&lt;/strong&gt; your code path (e.g. admin tool, another service), the cache can serve &lt;strong&gt;stale data&lt;/strong&gt; until TTL expires or you manually invalidate.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;When to use&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Great default for &lt;strong&gt;read-heavy&lt;/strong&gt; apps (blogs, product catalogs, many dashboards).&lt;/li&gt;
&lt;li&gt;When you&amp;rsquo;re just adding caching to an existing codebase and want full control.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;2. Read-Through&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Idea:&lt;/strong&gt;
The application pretends the &lt;strong&gt;cache is the main database&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;It calls the cache for every read. On a miss, the &lt;strong&gt;cache layer itself&lt;/strong&gt; fetches from the DB, stores the result, and returns it.&lt;/p&gt;
&lt;p&gt;With &lt;strong&gt;Cache-Aside&lt;/strong&gt;, your app code calls both cache and DB.
With &lt;strong&gt;Read-Through&lt;/strong&gt;, your app only calls the cache; a loader behind the cache talks to the DB.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cleaner application code&lt;/strong&gt;: your code only knows &amp;ldquo;get from cache&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;Cache libraries or infrastructure can handle loading, retries, and metrics.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If some writes go &lt;strong&gt;directly to the DB&lt;/strong&gt; (bypassing the cache API), the cache can easily become &lt;strong&gt;stale&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;You need a cache provider or library that supports read-through loading.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;When to use&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Larger teams where you want &lt;strong&gt;centralized cache logic&lt;/strong&gt;, not ad-hoc caching scattered everywhere.&lt;/li&gt;
&lt;li&gt;Situations where infra/platform team owns the cache layer.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;3. Write-Through&lt;/h2&gt;
&lt;p&gt;So far we talked about reads.
Now let&amp;rsquo;s talk about &lt;strong&gt;writes&lt;/strong&gt;: how we keep cache and DB in sync.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Write-Through&lt;/strong&gt; means:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;On a write, update the cache &lt;strong&gt;and&lt;/strong&gt; the database synchronously.
Only when both succeed is the write considered &amp;ldquo;done&amp;rdquo;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Flow&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;User updates something (e.g. changes quantity in a cart).&lt;/li&gt;
&lt;li&gt;App writes the new value into the &lt;strong&gt;cache&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;App also writes the same value into the &lt;strong&gt;database&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Only if both succeed do we return &amp;ldquo;OK&amp;rdquo; to the user.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Strong consistency&lt;/strong&gt;: cache and DB always match for that key.&lt;/li&gt;
&lt;li&gt;Reads from the cache always see the latest value.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Slower writes&lt;/strong&gt;: every write has to hit both cache and DB before returning.&lt;/li&gt;
&lt;li&gt;If DB is slow, your API write latency is also slow.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;When to use&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When correctness matters more than raw speed:
&lt;ul&gt;
&lt;li&gt;balances in a &lt;strong&gt;wallet / bank&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;inventory&lt;/strong&gt; in an e&amp;#8209;commerce site,&lt;/li&gt;
&lt;li&gt;data where a stale read would be really bad.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;4. Write-Back (Write-Behind)&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Write-Back&lt;/strong&gt; trades strict consistency for &lt;strong&gt;speed&lt;/strong&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;On a write, update &lt;strong&gt;only the cache&lt;/strong&gt; and return success quickly.
Later, an async process flushes those changes to the database.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Flow&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;User sends a write.&lt;/li&gt;
&lt;li&gt;App writes to cache and returns &lt;code&gt;200 OK&lt;/code&gt; immediately.&lt;/li&gt;
&lt;li&gt;A background worker or the cache itself periodically writes batched updates into the DB.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Very fast writes&lt;/strong&gt;: app isn&amp;rsquo;t blocked waiting for DB.&lt;/li&gt;
&lt;li&gt;Great for high-throughput workloads:
&lt;ul&gt;
&lt;li&gt;counters (views, likes),&lt;/li&gt;
&lt;li&gt;logs / telemetry,&lt;/li&gt;
&lt;li&gt;social feed events.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Risk of data loss&lt;/strong&gt;: if the cache crashes before flushing to DB, those changes are gone.&lt;/li&gt;
&lt;li&gt;DB is &lt;strong&gt;eventually&lt;/strong&gt; updated, not immediately.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;When to use&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Write-heavy workloads where losing a tiny fraction of data is acceptable:
&lt;ul&gt;
&lt;li&gt;metrics, dashboards, click-streams,&lt;/li&gt;
&lt;li&gt;non-critical logs.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;5. Write-Around&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Write-Around&lt;/strong&gt; takes a different approach:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Writes &lt;strong&gt;skip the cache&lt;/strong&gt; and go straight to the DB.
The cache is only filled later when data is read.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Flow&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A write goes &lt;strong&gt;directly&lt;/strong&gt; into the database.&lt;/li&gt;
&lt;li&gt;The cache is &lt;strong&gt;not&lt;/strong&gt; updated.&lt;/li&gt;
&lt;li&gt;On the next read, the app (or cache) sees a &lt;strong&gt;miss&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;loads from DB,&lt;/li&gt;
&lt;li&gt;populates the cache,&lt;/li&gt;
&lt;li&gt;and returns the value.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Pros&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prevents &amp;ldquo;polluting&amp;rdquo; the cache with data that is &lt;strong&gt;written but rarely read&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Good when you have big write batches (imports, ETL jobs) that users won&amp;rsquo;t read immediately.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cons&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;first read&lt;/strong&gt; after a write is always a &lt;strong&gt;cache miss&lt;/strong&gt; (slower).&lt;/li&gt;
&lt;li&gt;More frequent DB reads if many keys are only read once.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;When to use&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Large imports, archival data, logs that might be read only occasionally.&lt;/li&gt;
&lt;li&gt;Systems where memory is tight and you want the cache to focus on &lt;strong&gt;truly hot&lt;/strong&gt; data.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Side-by-side comparison&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s a quick comparison of these five strategies using the labels you provided.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;How teams typically choose&lt;/h2&gt;
&lt;p&gt;To connect this with the interactive content you built:&lt;/p&gt;
&lt;p&gt;These charts show two things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cache-Aside&lt;/strong&gt; dominates in real systems because it&amp;rsquo;s &lt;strong&gt;simple and resilient&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Write-Back&lt;/strong&gt; gives the fastest writes but carries the most risk;
&lt;strong&gt;Write-Through&lt;/strong&gt; is safest but slower on each write.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Which caching strategy should you pick? (a practical rule-set)&lt;/h2&gt;
&lt;p&gt;If you only remember one thing from this post, remember this:
&lt;strong&gt;you pick a caching strategy based on the cost of a stale read and the cost of a slow write.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a fast, real-world decision guide:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Most read-heavy apps&lt;/strong&gt; (catalogs, blogs, dashboards):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Start with &lt;strong&gt;Cache-Aside&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Add &lt;strong&gt;TTLs&lt;/strong&gt; and a plan for &lt;strong&gt;invalidation&lt;/strong&gt; if data changes.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;You want a clean abstraction and centralized caching&lt;/strong&gt; (platform/infra owned):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;Read-Through&lt;/strong&gt; so product code only depends on the cache API.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A stale read would be painful&lt;/strong&gt; (wallet balances, inventory, &amp;ldquo;did my payment go through?&amp;rdquo;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Prefer &lt;strong&gt;Write-Through&lt;/strong&gt; (consistency &amp;gt; speed).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Writes are hot and you can tolerate some eventual consistency&lt;/strong&gt; (counters, analytics, clickstream):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Consider &lt;strong&gt;Write-Back&lt;/strong&gt;, but only with durability guardrails (below).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;You write lots of data that is rarely read&lt;/strong&gt; (imports, ETL, logs):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;Write-Around&lt;/strong&gt; to avoid polluting the cache.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;Two failure modes teams hit in production (and what to do)&lt;/h2&gt;
&lt;p&gt;Caching rarely fails in obvious ways. It fails with &amp;ldquo;the DB is melting&amp;rdquo; or &amp;ldquo;why is data stale?&amp;rdquo;.
Two patterns show up everywhere.&lt;/p&gt;
&lt;h3&gt;1) Cache stampede (thundering herd)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; a hot key expires (or cache restarts), and suddenly thousands of requests miss at once and slam the DB.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mitigations:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Request coalescing / single-flight&lt;/strong&gt;: only one request recomputes the value; others wait and reuse it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stale-while-revalidate&lt;/strong&gt;: keep serving a slightly stale value while one background refresh updates it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Jittered TTLs&lt;/strong&gt;: add randomization to expiration times so many keys don&amp;rsquo;t expire at the same second.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2) Hot key / hotspotting&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; one key becomes so popular (home feed, trending item, auth config) that a single cache shard or a single DB row gets hammered.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mitigations:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Shard the key&lt;/strong&gt; (e.g. &lt;code&gt;trending:v1:0..N&lt;/code&gt;) and merge results.&lt;/li&gt;
&lt;li&gt;Use a &lt;strong&gt;two-layer cache&lt;/strong&gt;: per-instance in-memory (tiny TTL) + shared Redis.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Precompute&lt;/strong&gt; or cache at a higher level (edge/CDN) if the data is public.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2&gt;What to monitor so caching doesn&amp;rsquo;t become &amp;ldquo;magic&amp;rdquo;&lt;/h2&gt;
&lt;p&gt;Whatever strategy you choose, watch these:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cache hit rate&lt;/strong&gt; (overall and by endpoint)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DB QPS&lt;/strong&gt; (does caching actually reduce it?)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;p95/p99 latency&lt;/strong&gt; (cache helps only if tail latency improves)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Eviction rate / memory pressure&lt;/strong&gt; (are you constantly evicting hot data?)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stale reads / correctness signals&lt;/strong&gt; (app-specific: inventory mismatches, stale dashboards, etc.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you can&amp;rsquo;t measure these, caching will eventually surprise you.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;Putting it all together (for a newbie)&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;re just starting with caching:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Start with Cache-Aside&lt;/strong&gt; for reads.
It&amp;rsquo;s simple, safe, and easy to reason about.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add Read-Through&lt;/strong&gt; if you want a cleaner abstraction and your cache provider supports it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;For writes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;strong&gt;Write-Through&lt;/strong&gt; when you absolutely need the cache and DB to agree on every write.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;Write-Back&lt;/strong&gt; when you care a lot about write speed and can tolerate a little risk.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;Write-Around&lt;/strong&gt; when you write lots of data that users won&amp;rsquo;t read immediately.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Whatever you choose, always:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set reasonable &lt;strong&gt;TTLs&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;Have a plan for &lt;strong&gt;invalidating&lt;/strong&gt; keys,&lt;/li&gt;
&lt;li&gt;And monitor &lt;strong&gt;hit rate&lt;/strong&gt; and &lt;strong&gt;latency&lt;/strong&gt; so you can adjust over time.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;With just these five strategies, you can handle most caching problems you&amp;rsquo;ll run into as a backend engineer.&lt;/p&gt;
</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/14654/five-caching-strategies-every-backend-dev-should-know</guid>
<pubDate>Sun, 19 Apr 2026 19:33:53 +0000</pubDate>
</item>
</channel>
</rss>