<?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: Tushar Dwivedi</title>
    <description>The latest articles on DEV Community by Tushar Dwivedi (@tushar_bytonomics).</description>
    <link>https://dev.to/tushar_bytonomics</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%2F4086897%2Fa2ffbbe2-a125-48c5-9965-c3892a11796f.png</url>
      <title>DEV Community: Tushar Dwivedi</title>
      <link>https://dev.to/tushar_bytonomics</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tushar_bytonomics"/>
    <language>en</language>
    <item>
      <title>Let us talk about caching (Part 2)</title>
      <dc:creator>Tushar Dwivedi</dc:creator>
      <pubDate>Fri, 21 Aug 2026 04:47:30 +0000</pubDate>
      <link>https://dev.to/tushar_bytonomics/let-us-talk-about-caching-part-2-4l4m</link>
      <guid>https://dev.to/tushar_bytonomics/let-us-talk-about-caching-part-2-4l4m</guid>
      <description>&lt;h2&gt;
  
  
  Quick recap
&lt;/h2&gt;

&lt;p&gt;This is Part 2 of a series — if you haven't read &lt;a href="https://dev.to/tushar_bytonomics/let-us-talk-about-caching-29f7"&gt;Part 1&lt;/a&gt;, the short version: every Go service at smritea.ai kept reinventing the same caching logic, so it got pulled into a small generic library called &lt;a href="https://github.com/Bytonomics/smartcache" rel="noopener noreferrer"&gt;smartcache&lt;/a&gt; — a &lt;code&gt;Cache[T]&lt;/code&gt; sitting on a swappable backend, with read-through and write-through built in. Building it surfaced three real problems, each fixed in a few lines: a cache stampede fixed with &lt;code&gt;singleflight&lt;/code&gt;, synchronized TTL expiry fixed with jitter, and repeated database hits on keys that don't exist fixed with negative caching.&lt;/p&gt;

&lt;p&gt;Part 1 closed on a gap none of that solved: a &lt;code&gt;User&lt;/code&gt; that needs to be found three ways, by ID, by email, by slug. One row, three lookup paths. Cache it three times and an update has to land in three places at once — miss one and you're serving a stale email lookup for a user whose name already changed. Cache it once under the ID and the other two lookups get no benefit from caching at all. That's where this post picks up.&lt;/p&gt;

&lt;p&gt;The trade-off at the center of this post rhymes with something more famous: CAP theorem, the idea that a distributed system can't maximize consistency, availability, and partition tolerance all at once — something has to give. What follows isn't CAP itself, but it's the same shape of problem.&lt;/p&gt;

&lt;p&gt;Solving the three-keys gap turned into a small distributed-systems problem, and the fix ended up being two different answers depending on whether you're running one Redis instance or a cluster. Before getting into why, it's worth being precise about that CAP comparison, since it's easy to overstate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sounds like CAP theorem?
&lt;/h2&gt;

&lt;p&gt;Loosely, yes — not literally. CAP is about consistency versus availability when a network partition splits your nodes apart. What follows here is a trade-off between two different things: keeping one entity's keys atomic under one Lua call, versus spreading those keys across a cluster so no single node carries all of one entity's traffic. There's no partition involved, and — as you'll see — the "weaker" option here doesn't actually give up correctness, only the guarantee that every key changes in one atomic step. It's the same &lt;em&gt;kind&lt;/em&gt; of problem CAP made famous — you can't maximize two good properties in a distributed system at once, something has to give — just not CAP itself. Worth saying up front so nobody reads further expecting an actual CAP proof.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store the value once, point at it from everywhere else
&lt;/h2&gt;

&lt;p&gt;The fix for "one row, three keys" is pointer indirection: the value lives at exactly one key, and every alias is a small key that resolves to it instead of holding a copy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bc:{user}:5                          -&amp;gt; {"ID":"5","Name":"Ada",...}   (the value, stored once)
bc:grp:{user}:email:foo@bar.com    -&amp;gt; 5                                (pointer -&amp;gt; primary key)
bc:grp:{user}:slug:ada             -&amp;gt; 5                                (pointer -&amp;gt; primary key)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNqNUcuOnDAQ_BXLqxEXdsIwMIAVrbSvnHJKojkk5GCgYdAYe9Q2m52w_Es-JP-UX4gNJDtSLnFf3F1dVVZ5oKWqgDK6Wg2tbA0jg2cO0IHHiFdwDZ5P5sGeY8sLAdpzOydsO47neyUUutWrcOvKbS_QJ3g2r_C70NUFfKewArzgP95Hdzu3IFoJr_Ps1pWbF7w8Nqh6WU3A5taVN47japXLWqhv5YGjIe8_5JLY8_glp0XJGjyxodeAI4OOt4LVSv36-aPguC5Vl9Ov5Pr65gVBK_EEmhhFlgeSI5xJ_EL2s86iEb8t8M2NDYQ8cdGDT7RRCBVRsgQrNlt__Mdai75hvOL_4zdrlIJr_QD17EPqVgi2hORrg-oIbMncL11Y7GoTuLpgk_1Mpj5tsK0oM2hfTDtAG4Rt6eCWczp9b06ZvVYcjznN5Wg5Jy4_K9X9odnkmwNlNRfadv2p4gYeWt4g7_5OEeT0qb00lG3SSYOygT5TFobb9S4OsiSI4jRLo61Fz5RF4TrK7Am3SbAL4igZffp9cg3WaeKgdJdkSRZY3vgbwqXYUw" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNqNUcuOnDAQ_BXLqxEXdsIwMIAVrbSvnHJKojkk5GCgYdAYe9Q2m52w_Es-JP-UX4gNJDtSLnFf3F1dVVZ5oKWqgDK6Wg2tbA0jg2cO0IHHiFdwDZ5P5sGeY8sLAdpzOydsO47neyUUutWrcOvKbS_QJ3g2r_C70NUFfKewArzgP95Hdzu3IFoJr_Ps1pWbF7w8Nqh6WU3A5taVN47japXLWqhv5YGjIe8_5JLY8_glp0XJGjyxodeAI4OOt4LVSv36-aPguC5Vl9Ov5Pr65gVBK_EEmhhFlgeSI5xJ_EL2s86iEb8t8M2NDYQ8cdGDT7RRCBVRsgQrNlt__Mdai75hvOL_4zdrlIJr_QD17EPqVgi2hORrg-oIbMncL11Y7GoTuLpgk_1Mpj5tsK0oM2hfTDtAG4Rt6eCWczp9b06ZvVYcjznN5Wg5Jy4_K9X9odnkmwNlNRfadv2p4gYeWt4g7_5OEeT0qb00lG3SSYOygT5TFobb9S4OsiSI4jRLo61Fz5RF4TrK7Am3SbAL4igZffp9cg3WaeKgdJdkSRZY3vgbwqXYUw" alt="Aliases" width="721" height="198"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A read through an alias is two hops: read the pointer, get the primary key, read the value key built from that primary key. Delete the value key and every alias breaks at the same instant — each one resolves to a key that's gone, which reads as a miss and reloads from the database. One value, no duplication, and eviction only has to happen in one place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this needs a Lua script
&lt;/h2&gt;

&lt;p&gt;Writing or evicting a group isn't a single command. Evict-by-alias has to: read the pointer, find the primary key it points to, read that record's list of registered aliases, then delete the value key and every one of those alias pointers. That's read, branch on what you read, then delete several keys — and Redis transactions (&lt;code&gt;MULTI&lt;/code&gt;/&lt;code&gt;EXEC&lt;/code&gt;) can't do that. They queue commands blind; there's no way to read a value mid-transaction and decide what to delete based on it. &lt;code&gt;WATCH&lt;/code&gt; plus a retry loop can get you there, but it gets expensive fast under concurrent writes to the same entity.&lt;/p&gt;

&lt;p&gt;The straightforward fix is a Lua script — the read-branch-delete sequence runs atomically on the Redis server itself, no client-side retry loop needed. Here's the whole read path for the single-instance version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;pk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&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="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;pk&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ARGV&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="o"&gt;..&lt;/span&gt; &lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pointer lookup, primary-key resolve, value read — one round trip, one atomic script.&lt;/p&gt;

&lt;h2&gt;
  
  
  The atomicity requirement creates a hotspot
&lt;/h2&gt;

&lt;p&gt;To make that script atomic, every key belonging to one entity — the value, every alias pointer, the membership index — has to live on the same Redis Cluster node, because Lua scripts can only touch keys on one node at a time. Left alone, Redis Cluster hashes each key independently and scatters them across nodes by design — and a Lua call touching keys on different nodes doesn't degrade gracefully, it's rejected outright, the same &lt;code&gt;CROSSSLOT&lt;/code&gt; error you'd get from any multi-key command spanning slots. Redis Cluster gives you a way to force keys together instead: a hash tag, the part of a key wrapped in &lt;code&gt;{}&lt;/code&gt;, decides which slot the key lands on — everything else in the key name is ignored for hashing purposes. Tag everything for one entity with &lt;code&gt;{user}&lt;/code&gt; and Redis Cluster puts it all on the same slot, which is exactly the constraint the atomic script needs.&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;// AliasColocated tags every key of an entity with {ns}: all of the entity's keys share one&lt;/span&gt;
&lt;span class="c"&gt;// Cluster slot and every op is a single atomic Lua cascade. One slot per entity (hotspot risk&lt;/span&gt;
&lt;span class="c"&gt;// on Cluster with a hot entity). Default.&lt;/span&gt;
&lt;span class="n"&gt;AliasColocated&lt;/span&gt; &lt;span class="n"&gt;AliasMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;iota&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works, and it's genuinely atomic — but it pins one entity's entire key set to one node. Redis's own &lt;a href="https://redis.io/blog/redis-clustering-best-practices-with-keys/" rel="noopener noreferrer"&gt;clustering best practices post&lt;/a&gt; is direct about this: overusing hash tags gets you "an unbalanced cluster, or worse, one full node and many empty nodes," and the advice is to use them sparingly. A &lt;code&gt;User&lt;/code&gt; with a normal amount of traffic on a cluster is fine under this scheme. A &lt;code&gt;User&lt;/code&gt; that happens to be extremely active — or an entity type with a naturally hot member, like a popular org or a trending post — puts all of its traffic on one node no matter how many nodes the cluster has. You can verify this yourself with &lt;a href="https://redis.io/docs/latest/commands/cluster-keyslot/" rel="noopener noreferrer"&gt;&lt;code&gt;CLUSTER KEYSLOT&lt;/code&gt;&lt;/a&gt;: every key sharing a hash tag maps to the same slot number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two strategies, chosen per cache
&lt;/h2&gt;

&lt;p&gt;Rather than pick one answer for every cache, &lt;code&gt;smartcache&lt;/code&gt; exposes both and lets you choose:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;AliasMode&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="c"&gt;// AliasColocated tags every key of an entity with {ns}: all of the entity's keys share one&lt;/span&gt;
    &lt;span class="c"&gt;// Cluster slot and every op is a single atomic Lua cascade. One slot per entity (hotspot risk&lt;/span&gt;
    &lt;span class="c"&gt;// on Cluster with a hot entity). Default.&lt;/span&gt;
    &lt;span class="n"&gt;AliasColocated&lt;/span&gt; &lt;span class="n"&gt;AliasMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;iota&lt;/span&gt;
    &lt;span class="c"&gt;// AliasSharded tags value+members with {ns:pk} (one slot per record) and reverse pointers with&lt;/span&gt;
    &lt;span class="c"&gt;// {ns:field:value} (one slot per alias), distributing load. GetByAlias is a two-hop resolve with&lt;/span&gt;
    &lt;span class="c"&gt;// validate-on-read; writes/evicts are a record-slot atomic Lua plus best-effort reverse-pointer&lt;/span&gt;
    &lt;span class="c"&gt;// ops. No hotspot, no stale reads, weaker cross-key atomicity (self-healing).&lt;/span&gt;
    &lt;span class="n"&gt;AliasSharded&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNqNU0tu2zAQvQrBwNBGcPWzZRNFgVhJV10EadBFqy4oiZYJU6RAUkhcQUAPkQP0ID1NL9ArlJRs-ZMEKEcLcmbeI99opoW5KAhEcDJpKacagdbRG1IRBwEnw4o4LhgcX7CkOGNEOTanlrTCcpcIJqRNvQpCazZ7H3ogT_oY_hhYOwmvhCyIPMHfJtFqbhMY5eToX15bs_4M59tSioYXfcC_tuZ0XTeZpHzNxGO-wVKDh1XKgVmqyUqJ6w2wXDnWpPiWwnEP_vx8BoIToJjQoCYSEK6p3qXw-wC3q6CS5JoKDj7dH72Jb4iyHLWNIrJDszNIEgzBUtaHBFJhytBaiL-_f2VYTnNRnUPCFxDFmhLhAo95hBcXqj4bsUWvab97qcg83hTZ7Z32jBnF6j8E3p8KRLPufSbffRjIevIzijv_4vWv6R0oxCN_BX9ZsFH8G6CxFDnDSt2QNcjHn7qmjKGr23m4jOau0lJsCdp3pmuzJNo34gWD2pdwwA-t-Abe96yd4E0_uEngJuHxHafRe9-9M19wuAO6sJS0gEjLhriwItKUyxxha1Ep7GcthchsCyy3KUx5ZzA15l-FqA4wMwblBqI1Zsqcmrow195QbFqjGr3SVMpOWMM1RH7Uc0DUwieIgiCczmfeMvai2WK5iMKFC3cQRcE0WpoVhLE392ZR3LnwR3-rN13EfciLYn8WBXHc_QM9l1RS" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNqNU0tu2zAQvQrBwNBGcPWzZRNFgVhJV10EadBFqy4oiZYJU6RAUkhcQUAPkQP0ID1NL9ArlJRs-ZMEKEcLcmbeI99opoW5KAhEcDJpKacagdbRG1IRBwEnw4o4LhgcX7CkOGNEOTanlrTCcpcIJqRNvQpCazZ7H3ogT_oY_hhYOwmvhCyIPMHfJtFqbhMY5eToX15bs_4M59tSioYXfcC_tuZ0XTeZpHzNxGO-wVKDh1XKgVmqyUqJ6w2wXDnWpPiWwnEP_vx8BoIToJjQoCYSEK6p3qXw-wC3q6CS5JoKDj7dH72Jb4iyHLWNIrJDszNIEgzBUtaHBFJhytBaiL-_f2VYTnNRnUPCFxDFmhLhAo95hBcXqj4bsUWvab97qcg83hTZ7Z32jBnF6j8E3p8KRLPufSbffRjIevIzijv_4vWv6R0oxCN_BX9ZsFH8G6CxFDnDSt2QNcjHn7qmjKGr23m4jOau0lJsCdp3pmuzJNo34gWD2pdwwA-t-Abe96yd4E0_uEngJuHxHafRe9-9M19wuAO6sJS0gEjLhriwItKUyxxha1Ep7GcthchsCyy3KUx5ZzA15l-FqA4wMwblBqI1Zsqcmrow195QbFqjGr3SVMpOWMM1RH7Uc0DUwieIgiCczmfeMvai2WK5iMKFC3cQRcE0WpoVhLE392ZR3LnwR3-rN13EfciLYn8WBXHc_QM9l1RS" alt="Sharded v/s Colocated" width="736" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The obvious question: if tagging everything with &lt;code&gt;{user}&lt;/code&gt; gets you perfect atomicity in one Lua hop, why not just always do that? Because it permanently ties that entity's entire key set to whichever single node its hash tag happens to land on. Fine for most entities. Not fine for the one that turns out to be unusually busy or unusually large — there's no routing around it without changing the tag, and changing the tag is what gives up the atomicity in the first place.&lt;/p&gt;

&lt;p&gt;Colocated is the default — one atomic call, correct everywhere, and fine as long as no single entity's traffic outgrows one node. Sharded spreads the value and each alias pointer across different slots, which means a read is now two network hops through two different nodes instead of one atomic call on one node — a small, real latency cost, paid in exchange for not having one node absorb an entire entity's traffic. That's the part that needs its own correctness argument.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the distributed version correct anyway
&lt;/h2&gt;

&lt;p&gt;Spreading keys across slots means the atomic guarantee is gone — a pointer and the record it points to are no longer updated together. What has to not happen is a stale pointer returning the &lt;em&gt;wrong&lt;/em&gt; value. The fix is checking the pointer's claim against the record before trusting it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="kd"&gt;local&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&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="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'HGET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ARGV&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="o"&gt;~=&lt;/span&gt; &lt;span class="n"&gt;ARGV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;KEYS[1]&lt;/code&gt; is the value, &lt;code&gt;KEYS[2]&lt;/code&gt; is the record's own membership index (which fields point at it, and with what value). If the record's index doesn't actually list this alias anymore, the script returns nothing — a miss, not a wrong answer. A pointer that's out of date because of a race just resolves to "not found" and gets reloaded. That's validate-on-read: it turns "the pointer might be stale" into "a stale pointer can only ever produce a safe miss."&lt;/p&gt;

&lt;p&gt;Cleanup gets the matching treatment — a pointer is deleted only if it still points at the record being evicted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'GET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&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="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ARGV&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="k"&gt;then&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'DEL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KEYS&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="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's compare-and-delete. Without it, evicting user 5 right after their email got reassigned to user 99 could delete the pointer that now correctly belongs to user 99. With it, the delete only fires if the pointer still agrees with what's being evicted.&lt;/p&gt;

&lt;p&gt;Worth being precise about what Sharded actually gives up, since it's less than it sounds like: not correctness — no caller ever gets a wrong value, validate-on-read rules that out — and not strong consistency of the data itself. What it gives up is atomicity &lt;em&gt;across&lt;/em&gt; keys, and the price for that is an extra network hop per read plus, occasionally, a stale reverse-pointer sitting around until it's overwritten or compare-deleted — which shows up as a few extra cache misses while it self-heals, not as stale data reaching a caller. That's the actual trade: a small, bounded amount of latency and cache-miss noise, spent to keep one hot entity from owning a whole node.&lt;/p&gt;

&lt;p&gt;The public interface both strategies implement is the same either way — &lt;code&gt;Cache[T]&lt;/code&gt; never knows which one it's talking to:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;AliasOps&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;GetValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;PutValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;EvictByPrimary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;GetByAlias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;AliasRef&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;PutByAlias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;primary&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;AliasRef&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;EvictByAlias&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt; &lt;span class="n"&gt;AliasRef&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And picking a strategy is one field on registration:&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;ttl&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hour&lt;/span&gt;
&lt;span class="n"&gt;sharded&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;smartcache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AliasSharded&lt;/span&gt;
&lt;span class="n"&gt;cache&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;smartcache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RegisterAliasGroup&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;mgr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"user"&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;smartcache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntityOptions&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;TTL&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;AliasMode&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;sharded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;// omit this and you get AliasColocated, the default&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where this leaves things
&lt;/h2&gt;

&lt;p&gt;Colocated for a single Redis instance, or a cluster where no entity is expected to get disproportionately hot.&lt;/p&gt;

&lt;p&gt;Sharded once you're on a cluster and that assumption stops holding. Neither one is a half-measure.&lt;/p&gt;

&lt;p&gt;Colocated is fully atomic by design, and Sharded is correct by construction even under races, just for a different set of guarantees.&lt;/p&gt;

&lt;p&gt;There's one thing left on the table, and it's a real gap rather than a hypothetical one: every operation on an alias-group cache currently goes through the Lua path, including a plain lookup by primary key that was never part of any alias group to begin with. That's correct, just wasted work for the common case.&lt;/p&gt;

&lt;p&gt;The fix on paper is a Bloom filter in front of the Lua call — check membership cheaply first, skip straight to a plain &lt;code&gt;GET&lt;/code&gt; when a key was never registered as an alias. I have not built it yet. Because it's a pure performance optimisation with no correctness story attached to it.&lt;/p&gt;

&lt;p&gt;And if you want to poke at the real code, &lt;a href="https://github.com/Bytonomics/smartcache" rel="noopener noreferrer"&gt;smartcache&lt;/a&gt; is public. And stay tuned. I'm going to add more chapters to this story as well as to the actual project.&lt;/p&gt;

&lt;p&gt;───────── ⋆⋅☆⋅⋆ ─────────&lt;/p&gt;

&lt;p&gt;Thanks for reading this far! If it helped, I'd love for you to stick around — find me here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://medium.com/@tdwivedi2708" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/tushar_bytonomics"&gt;Dev.to&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.linkedin.com/in/tushar-dwivedi/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://twitter.com/tushar2708" rel="noopener noreferrer"&gt;X&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/tushar2708" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>redis</category>
      <category>distributedsystems</category>
      <category>go</category>
      <category>programming</category>
    </item>
    <item>
      <title>Let us talk about caching (Part 1)</title>
      <dc:creator>Tushar Dwivedi</dc:creator>
      <pubDate>Thu, 20 Aug 2026 15:40:36 +0000</pubDate>
      <link>https://dev.to/tushar_bytonomics/let-us-talk-about-caching-29f7</link>
      <guid>https://dev.to/tushar_bytonomics/let-us-talk-about-caching-29f7</guid>
      <description>&lt;p&gt;There's an old line, usually traced back to &lt;a href="https://martinfowler.com/bliki/TwoHardThings.html" rel="noopener noreferrer"&gt;Phil Karlton&lt;/a&gt; at Netscape"&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There are only two hard things in computer science:&lt;br&gt;
cache invalidation and naming things.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;More on it in the Side Quest&lt;/p&gt;

&lt;p&gt;I'd heard it as a joke for years. Then I spent a few weeks actually living the first half of it, and stopped finding it funny. This is that story — starting with the boring part, which is where it always starts.&lt;/p&gt;

&lt;p&gt;At smritea.ai, the backend is predominantly Go, and for a while every section of code that needed to cache something just wrote its own version of the same fifteen lines:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check Redis, return data if found.&lt;/li&gt;
&lt;li&gt;If it's a miss, go to the database, find it, write it to redis, return it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And that just works, but then there are bugs that are hard to track. By the third or fourth service, it was the same bug waiting to happen in slightly different clothes.&lt;/p&gt;

&lt;p&gt;Before AI started doing our bidding, this is what used to happen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;At one place, a developer forgot to set a TTL&lt;/li&gt;
&lt;li&gt;Another didn't handle a cache error as a miss.&lt;/li&gt;
&lt;li&gt;Third one had to fix a quick bug, and had no time to invalidate on write.&lt;/li&gt;
&lt;li&gt;I looked at it, thought that we need better engineer practices, wondered who approved all those PRs, realised it was me. And then left it for a later clean-up.&lt;/li&gt;
&lt;li&gt;Later never came.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then came AI, and all of this mismatch happens in 10X more places, 10X more often.&lt;/p&gt;

&lt;p&gt;None of that is hard to fix individually. What's hard is fixing it consistently across a growing number of services when the fix keeps getting copy-pasted instead of shared.&lt;/p&gt;

&lt;p&gt;So I pulled it out into a library — &lt;code&gt;smartcache&lt;/code&gt; — and built it the way I'd want to have found it: one generic cache type, a few well-understood failure modes handled once, and a backend you can swap out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the cache
&lt;/h2&gt;

&lt;p&gt;The core type is &lt;code&gt;Cache[T]&lt;/code&gt;, generic over whatever you're caching, sitting on top of a small backend interface:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;CacheStore&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Get returns the raw bytes for key, or ErrStoreMiss if the key is absent.&lt;/span&gt;
    &lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// Set stores val under key with the given ttl. A ttl &amp;lt;= 0 means no expiry.&lt;/span&gt;
    &lt;span class="n"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ttl&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="c"&gt;// Delete removes key. Deleting an absent key is not an error.&lt;/span&gt;
    &lt;span class="n"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="c"&gt;// Exists reports whether key is present (and not expired).&lt;/span&gt;
    &lt;span class="n"&gt;Exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Cache[T]&lt;/code&gt; never sees Redis directly — it talks to &lt;code&gt;CacheStore&lt;/code&gt;, so a Redis-backed store and an in-process map-backed store (&lt;code&gt;memstore&lt;/code&gt;, mostly for tests) are interchangeable. Reading is read-through:&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="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="n"&gt;GetByKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loader&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Outcome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call it with a key and a &lt;code&gt;loader&lt;/code&gt; function. On a hit, you get the cached value. On a miss, &lt;code&gt;loader&lt;/code&gt; runs, its result gets cached, and you get that back instead. Writing has the mirror shape — &lt;code&gt;PutByKey&lt;/code&gt; runs a &lt;code&gt;writer&lt;/code&gt;, caches exactly what it returns. The caller never manually juggles "did I remember to cache this."&lt;/p&gt;

&lt;p&gt;That much was the easy part. What made it worth writing down is what happened once services actually started using it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem one: everyone misses at the same time
&lt;/h2&gt;

&lt;p&gt;A cached key expires. In the next few milliseconds, twenty requests for that same key show up. All twenty see a miss, because none of them know about the other nineteen. All twenty go to the database for the same row, at the same moment. This is the cache stampede — sometimes called the thundering herd — and it gets worse exactly when you can least afford it: a hot key, under load.&lt;/p&gt;

&lt;p&gt;The fix doesn't need anything clever, just coordination between concurrent callers for the same key. Go's &lt;code&gt;singleflight&lt;/code&gt; package does exactly that — one call runs, everyone else waiting on the same key gets handed its result instead of starting their own:&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="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lerr&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;group&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="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lerr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doLoad&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="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lerr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doLoad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;doLoad&lt;/code&gt; is the closure that actually calls the loader and populates the cache. &lt;code&gt;group.Do&lt;/code&gt; keys on the cache key itself, so twenty concurrent misses on &lt;code&gt;"user:5"&lt;/code&gt; collapse into one database call, and nineteen callers get the shared result instead of hitting the database at all. It's on by default; you can turn it off per cache if you have a reason to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem two: everyone expires at the same time
&lt;/h2&gt;

&lt;p&gt;Stampede protection covers concurrent misses on one key. There's a second version of the same failure: a batch of keys all written with the same TTL will all expire at the same moment, and you get a wave of misses across many different keys simultaneously instead of one key. Same underlying cause — synchronised timing — different trigger.&lt;/p&gt;

&lt;p&gt;The fix is jitter: shave a small, random amount off each TTL so keys written together don't expire together.&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;// defaultJitterFraction is the fraction of the base TTL that jitter may shave&lt;/span&gt;
&lt;span class="c"&gt;// off when a cache does not override it. 0.10 =&amp;gt; an effective TTL in&lt;/span&gt;
&lt;span class="c"&gt;// [0.9*base, base].&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;defaultJitterFraction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0.10&lt;/span&gt;

&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;jitterRand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rand&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Float64&lt;/span&gt;

&lt;span class="c"&gt;// applyJitter returns a downward-jittered TTL: base - jitterRand()*fraction*base.&lt;/span&gt;
&lt;span class="c"&gt;// It returns base unchanged when base &amp;lt;= 0 (infinite/none) or fraction &amp;lt;= 0&lt;/span&gt;
&lt;span class="c"&gt;// (jitter disabled). The result is always in [(1-fraction)*base, base] and is&lt;/span&gt;
&lt;span class="c"&gt;// never negative, since jitterRand() is in [0,1) and fraction is in [0,1).&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;applyJitter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fraction&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&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;base&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;fraction&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&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="n"&gt;base&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;delta&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jitterRand&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;fraction&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&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;base&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;delta&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every TTL — positive and negative (more on that below) — goes through this before it's used. A 10% jitter on a 10-minute TTL means each key actually expires somewhere between 9 and 10 minutes in. Spread that across a few thousand keys and the expiry wave turns into a trickle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem three: Reduce the cost of saying NO. (Negative caching)
&lt;/h2&gt;

&lt;p&gt;The third one shows up with lookups that can legitimately miss — a user by an ID that doesn't exist, a slug someone mistyped. If a request for a missing key comes in repeatedly, every single one falls through to the database, gets nothing back, and does it again next time. The cache isn't helping at all for that access pattern, because there's never anything to cache — or so it seems until you cache the fact that it's missing, not the missing thing itself.&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;// negativeMarker is stored in place of a real value to negative-cache "not found".&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;negativeMarker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\x00&lt;/span&gt;&lt;span class="s"&gt;smartcache&lt;/span&gt;&lt;span class="se"&gt;\x00&lt;/span&gt;&lt;span class="s"&gt;negative&lt;/span&gt;&lt;span class="se"&gt;\x00&lt;/span&gt;&lt;span class="s"&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 the loader reports &lt;code&gt;ErrNotFound&lt;/code&gt; and a negative TTL is configured, &lt;code&gt;GetByKey&lt;/code&gt; writes this marker instead of a real value:&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="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lerr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrNotFound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NegativeTTL&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&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;sErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;negativeMarker&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;negativeTTL&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="n"&gt;sErr&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="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrNotFound&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="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrNotFound&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next request for that key hits the marker, recognises it, and returns "not found" without touching the database — for a shorter TTL than a real hit, since you'd rather notice a newly-created record sooner than a newly-updated one. It's off by default; you turn it on by setting &lt;code&gt;NegativeTTL&lt;/code&gt;, and only where a missing-key flood is actually plausible.&lt;/p&gt;

&lt;p&gt;Be careful though, and keep the TTL low. You don't want the cache to stand in the way for too long, in case the DB actually changes&lt;/p&gt;

&lt;p&gt;Now that I think of it, what if we remove the negative cache key whenever we write to the key (truly write-through). Too much?&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNqNks-OmzAQxl9lNJcmEhuRAA34sFIApYdWPTQ9tfTghVmwCnZqm-ymUaQ-RJ-wT1IDyWqrXOqDxfj7ffPH5oSlqggZGvrRkywpF7zWvCskuLXn2opS7Lm0sAFu4NNAGRfc6ulrPb3Vs0HPeNnQ18_fbuXddtCNkHVLj62oG3vL5GONnFv-wA0VciI2d_f3GYN3ZNPjezrOCuwNaRYV6EGreEV6PoHp_4KZA3dbBrVW_X6Rq3_ISn1w7IX8qCyBOpCGgXftaS0OZOCpES3B5o0ZE4MwICRMY8GfX7-hVLwlU1IFSloFtiEwvCMoedtOmXdb10Sesktns9I-z0er7qVxrpImLk_vLt0eeNvTi3madTzzwDRcu1pPwjbwoNw21CFtruM6eMNgmIuqV2fp9QxmY3eXbFKBoVLJaniQIdMcPay1qJBZ7QDsSHd8CPE0ZCvQzddRgcx9Vlx_L7CQZ-dxj_pFqe5qc7ddN8geeWtc1O8rbq-_4wtC0l1GpnppkS3jMQWyEz4jC8JgEcWrwI8SP175wdLDI7IwWQTryPfXb1dJkIRRcPbw51jTX8TrMHHLOZZRvF6F57-pzPX2" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNqNks-OmzAQxl9lNJcmEhuRAA34sFIApYdWPTQ9tfTghVmwCnZqm-ymUaQ-RJ-wT1IDyWqrXOqDxfj7ffPH5oSlqggZGvrRkywpF7zWvCskuLXn2opS7Lm0sAFu4NNAGRfc6ulrPb3Vs0HPeNnQ18_fbuXddtCNkHVLj62oG3vL5GONnFv-wA0VciI2d_f3GYN3ZNPjezrOCuwNaRYV6EGreEV6PoHp_4KZA3dbBrVW_X6Rq3_ISn1w7IX8qCyBOpCGgXftaS0OZOCpES3B5o0ZE4MwICRMY8GfX7-hVLwlU1IFSloFtiEwvCMoedtOmXdb10Sesktns9I-z0er7qVxrpImLk_vLt0eeNvTi3madTzzwDRcu1pPwjbwoNw21CFtruM6eMNgmIuqV2fp9QxmY3eXbFKBoVLJaniQIdMcPay1qJBZ7QDsSHd8CPE0ZCvQzddRgcx9Vlx_L7CQZ-dxj_pFqe5qc7ddN8geeWtc1O8rbq-_4wtC0l1GpnppkS3jMQWyEz4jC8JgEcWrwI8SP175wdLDI7IwWQTryPfXb1dJkIRRcPbw51jTX8TrMHHLOZZRvF6F57-pzPX2" alt="Swim Lane" width="1330" height="580"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Wrap a &lt;code&gt;Manager&lt;/code&gt; around all of this and you get per-cache OTLP metrics for free — hit/miss/load counts and load latency, without every service hand-rolling its own instrumentation. Register a cache once, and stampede protection, jitter, and negative caching are already there; you only turn on what a given cache actually needs.&lt;/p&gt;

&lt;p&gt;None of these three problems are unusual — they're the standard failure modes of caching anything at scale, and Redis's own docs on the &lt;a href="https://redis.io/docs/latest/develop/use-cases/cache-aside/" rel="noopener noreferrer"&gt;cache-aside pattern&lt;/a&gt; and its &lt;a href="https://redis.io/blog/cache-layer-architecture-guide/" rel="noopener noreferrer"&gt;cache layer architecture guide&lt;/a&gt; cover the same ground. What changed for us wasn't the theory — it was having one place these fixes live, instead of re-deriving them per service.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it didn't solve
&lt;/h2&gt;

&lt;p&gt;Everything above assumes one key maps to one value. That held until a repository needed to look up a &lt;code&gt;User&lt;/code&gt; three different ways — by ID, by email, by slug — and I had to decide what "cache the user" even means when there are three doors into the same room. Caching the value three times means an update has to hit three keys and a bug leaves two of them stale. Caching it once under the ID means the email and slug lookups never get to use the cache at all.&lt;/p&gt;

&lt;p&gt;That's where this got genuinely harder, and it's worth being precise about where. None of it is a problem on a single Redis instance — with everything on one node, there's nothing to hotspot in the first place. It turns into one on Redis Cluster: making the value and its aliases update atomically means Redis has to guarantee all of those keys live on the same node, and forcing that onto a busy or large entity puts its entire traffic on one node no matter how big the cluster is. That's where Part 2 picks up, and it took two tries to get the fix right.&lt;/p&gt;

&lt;p&gt;And if you want to poke at the real code, &lt;a href="https://github.com/Bytonomics/smartcache" rel="noopener noreferrer"&gt;smartcache&lt;/a&gt; is public.&lt;/p&gt;

&lt;p&gt;If you enjoyed this read and want to dive deeper into the trade-off pool, then &lt;a href="https://dev.to/tushar_bytonomics/let-us-talk-about-caching-part-2-4l4m"&gt;Part 2&lt;/a&gt; is published as well&lt;/p&gt;

&lt;p&gt;───────── ⋆⋅☆⋅⋆ ─────────&lt;/p&gt;

&lt;h2&gt;
  
  
  Side Quest
&lt;/h2&gt;

&lt;p&gt;The Karlton quote up top is real, but its internet trail is thinner than its fame suggests. I ended up taking a detour while writing this article, because I really had to include it.&lt;/p&gt;

&lt;p&gt;The earliest known appearance online is &lt;a href="https://www.tbray.org/ongoing/When/200x/2005/12/23/UPI" rel="noopener noreferrer"&gt;Tim Bray's blog, from December 2005&lt;/a&gt; — Bray says he first heard it from Karlton around 1996–97. &lt;a href="https://martinfowler.com/bliki/TwoHardThings.html" rel="noopener noreferrer"&gt;Martin Fowler's 2009 bliki entry&lt;/a&gt; is what actually popularised it, and &lt;a href="https://www.karlton.org/2017/12/naming-things-hard/" rel="noopener noreferrer"&gt;Karlton's own son has written about hearing him say it&lt;/a&gt; as early as around 1970 at Carnegie Mellon — without being sure it was the first time either.&lt;/p&gt;

&lt;p&gt;───────── ⋆⋅☆⋅⋆ ─────────&lt;/p&gt;

&lt;p&gt;Thanks for reading this far!&lt;/p&gt;

&lt;p&gt;If it helped, I'd love for you to stick around — find me on:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://medium.com/@tdwivedi2708" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;&lt;br&gt;
&lt;a href="https://dev.to/tushar_bytonomics"&gt;Dev.to&lt;/a&gt;&lt;br&gt;
&lt;a href="https://www.linkedin.com/in/tushar-dwivedi/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;br&gt;
&lt;a href="https://twitter.com/tushar2708" rel="noopener noreferrer"&gt;X&lt;/a&gt;&lt;br&gt;
&lt;a href="https://github.com/tushar2708" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>redis</category>
      <category>distributedsystems</category>
      <category>go</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
