<?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: Gouranga Das Samrat</title>
    <description>The latest articles on DEV Community by Gouranga Das Samrat (@gouranga-das-khulna).</description>
    <link>https://dev.to/gouranga-das-khulna</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%2F2193879%2F834a1499-2027-4355-87be-bb678e90ae5c.jpg</url>
      <title>DEV Community: Gouranga Das Samrat</title>
      <link>https://dev.to/gouranga-das-khulna</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gouranga-das-khulna"/>
    <language>en</language>
    <item>
      <title>Distributed Locks</title>
      <dc:creator>Gouranga Das Samrat</dc:creator>
      <pubDate>Sun, 13 Sep 2026 04:00:00 +0000</pubDate>
      <link>https://dev.to/gouranga-das-khulna/distributed-locks-4148</link>
      <guid>https://dev.to/gouranga-das-khulna/distributed-locks-4148</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;One-liner:&lt;/strong&gt; A distributed lock ensures that only one node in a cluster can perform a critical operation at a time — preventing race conditions across services.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  ❓ Why Do You Need Distributed Locks?
&lt;/h2&gt;

&lt;p&gt;In a single-server world, a mutex or semaphore handles concurrency. But in distributed systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Multiple service replicas run simultaneously&lt;/li&gt;
&lt;li&gt;They all share the same database or resource&lt;/li&gt;
&lt;li&gt;Without coordination, two instances might process the same job, double-charge a user, or corrupt shared state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Classic example:&lt;/strong&gt; Flash sale — 100 units in stock, 10,000 concurrent requests. Without a lock, you oversell.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔑 Redis Distributed Lock (Redlock)
&lt;/h2&gt;

&lt;p&gt;The most common approach: use Redis &lt;code&gt;SET NX EX&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Acquire lock&lt;/span&gt;
SET lock:resource_id &amp;lt;unique_token&amp;gt; NX EX 10
&lt;span class="c"&gt;# NX = only set if key does NOT exist&lt;/span&gt;
&lt;span class="c"&gt;# EX 10 = auto-expire in 10 seconds (TTL safety net)&lt;/span&gt;

&lt;span class="c"&gt;# Returns OK → you have the lock&lt;/span&gt;
&lt;span class="c"&gt;# Returns nil → someone else has it&lt;/span&gt;

&lt;span class="c"&gt;# Release lock (Lua script — atomic!)&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;redis.call&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;, KEYS[1]&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; ARGV[1] &lt;span class="k"&gt;then
  return &lt;/span&gt;redis.call&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"DEL"&lt;/span&gt;, KEYS[1]&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else
  return &lt;/span&gt;0
end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why a unique token?&lt;/strong&gt; Prevents a slow process from releasing a lock that was already acquired by someone else after TTL expiry.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚙️ How It Works (Step by Step)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Service A                    Redis                   Service B
   |                           |                        |
   |── SET lock:job1 tokenA ──&amp;gt;|                        |
   |   NX EX 10                |                        |
   |&amp;lt;── OK (lock acquired) ────|                        |
   |                           |                        |
   |   (doing work...)         |── SET lock:job1 ──────&amp;gt;|
   |                           |   tokenB NX EX 10      |
   |                           |&amp;lt;── nil (locked) ───────|
   |                           |   (retry or fail)      |
   |                           |                        |
   |── DEL lock:job1 ─────────&amp;gt;|                        |
   |   (if token matches)      |                        |
   |&amp;lt;── lock released ─────────|                        |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ⚠️ Edge Cases &amp;amp; Gotchas
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;What Happens&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Process crashes&lt;/td&gt;
&lt;td&gt;Lock stuck forever&lt;/td&gt;
&lt;td&gt;TTL auto-expire&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network partition&lt;/td&gt;
&lt;td&gt;Two leaders think they have lock&lt;/td&gt;
&lt;td&gt;Redlock (multi-node)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GC pause longer than TTL&lt;/td&gt;
&lt;td&gt;Process resumes, lock already expired&lt;/td&gt;
&lt;td&gt;Fencing tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis crashes&lt;/td&gt;
&lt;td&gt;Lock lost&lt;/td&gt;
&lt;td&gt;Use Redis Cluster / persistence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Fencing Token Pattern
&lt;/h3&gt;

&lt;p&gt;Each lock acquisition returns a monotonically increasing token number. The resource (e.g., DB) only accepts writes with tokens higher than the last seen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lock acquired → token 42
Lock expired, re-acquired → token 43
Stale process tries to write with token 42 → REJECTED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🏛️ Alternatives to Redis Locks
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Use When&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Redis (SET NX)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Atomic key-value&lt;/td&gt;
&lt;td&gt;Most common, low latency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ZooKeeper&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ephemeral nodes + watchers&lt;/td&gt;
&lt;td&gt;Need strong consistency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;etcd&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Leases + CAS&lt;/td&gt;
&lt;td&gt;Kubernetes-style infra&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DB row lock&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SELECT FOR UPDATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Already using SQL, low scale&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  ✅ Pros
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Sub-millisecond lock acquisition with Redis&lt;/li&gt;
&lt;li&gt;TTL prevents deadlocks from crashed processes&lt;/li&gt;
&lt;li&gt;Simple to implement with &lt;code&gt;SET NX EX&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Scales with Redis Cluster&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ❌ Cons
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Redis is single point of failure (use Redlock for HA)&lt;/li&gt;
&lt;li&gt;Clock drift can cause issues with TTL-based expiry&lt;/li&gt;
&lt;li&gt;Redlock is controversial (Martin Kleppmann vs. Antirez debate)&lt;/li&gt;
&lt;li&gt;Not suitable for long-held locks (use queue instead)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ⚖️ When to Use / When NOT to Use
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;✅ Use when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Preventing double processing (payment, job scheduling)&lt;/li&gt;
&lt;li&gt;Flash sales / inventory deduction&lt;/li&gt;
&lt;li&gt;Leader election among service instances&lt;/li&gt;
&lt;li&gt;Rate limiting per resource&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;❌ Avoid when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Operations take longer than a reasonable TTL (use idempotency + queue instead)&lt;/li&gt;
&lt;li&gt;You need guaranteed strong consistency (use ZooKeeper + fencing)&lt;/li&gt;
&lt;li&gt;The resource is already serialized (single-threaded queue consumer)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>systemdesign</category>
      <category>distributedsystems</category>
      <category>backend</category>
      <category>security</category>
    </item>
    <item>
      <title>Circuit Breaker Pattern</title>
      <dc:creator>Gouranga Das Samrat</dc:creator>
      <pubDate>Sat, 12 Sep 2026 04:00:00 +0000</pubDate>
      <link>https://dev.to/gouranga-das-khulna/circuit-breaker-pattern-39op</link>
      <guid>https://dev.to/gouranga-das-khulna/circuit-breaker-pattern-39op</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;One-liner:&lt;/strong&gt; A circuit breaker stops calling a failing service to give it time to recover — instead of hammering it with requests that are guaranteed to fail.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  ❓ The Problem: Cascading Failures
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Request
    ↓
Service A  ──► Service B  ──► Service C (DOWN 💥)
    ↑              ↑
Threads hang   Threads hang
(timeout 30s)  (timeout 30s)

→ Service A's thread pool exhausts
→ Service A goes down
→ Everything upstream dies
→ Full cascade failure 🔥
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without circuit breakers, &lt;strong&gt;one slow service kills your entire system&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔌 The Three States
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────┐
│                      CLOSED                             │
│              (Normal operation)                         │
│   Requests flow through. Track failure rate.            │
│   Failure threshold exceeded → trip to OPEN             │
└──────────────────────────┬──────────────────────────────┘
                           │ failures &amp;gt; threshold
                           ▼
┌─────────────────────────────────────────────────────────┐
│                       OPEN                              │
│              (Service is DOWN)                          │
│   All requests IMMEDIATELY fail (no network call)       │
│   Return cached/default response                        │
│   Wait for reset timeout (e.g., 60s) → HALF-OPEN       │
└──────────────────────────┬──────────────────────────────┘
                           │ timeout elapsed
                           ▼
┌─────────────────────────────────────────────────────────┐
│                    HALF-OPEN                            │
│              (Testing recovery)                         │
│   Let a few probe requests through                      │
│   Success → CLOSED (recovered! ✅)                      │
│   Failure → back to OPEN (still down ❌)                │
└─────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  💻 Implementation Example (Conceptual)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CircuitBreaker&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;failureThreshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resetTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CLOSED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failureCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failureThreshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;failureThreshold&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resetTimeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resetTimeout&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OPEN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Fail fast — don't even try&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onFailure&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;onSuccess&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failureCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;CLOSED&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;onFailure&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failureCount&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failureCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;failureThreshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OPEN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HALF_OPEN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resetTimeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Service temporarily unavailable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;paymentBreaker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CircuitBreaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;callPaymentService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;resetTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🛠️ Production Libraries
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Resilience4j&lt;/strong&gt;, Hystrix (deprecated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;opossum&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;sony/gobreaker&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;pybreaker&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;.NET&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Polly&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Service mesh&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Istio&lt;/strong&gt; (no code changes needed)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🔗 Circuit Breaker + Fallback Strategies
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fallback Type&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cached response&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Return last known good data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Default value&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Show "0 recommendations" instead of error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Static response&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Return empty array, blank page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Redirect&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Send to static maintenance page&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Queue&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Accept request, process later&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  ✅ Pros
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Prevents cascading failures — protects the whole system&lt;/li&gt;
&lt;li&gt;Fail-fast gives users a quick response instead of 30s timeout&lt;/li&gt;
&lt;li&gt;Gives the failing service breathing room to recover&lt;/li&gt;
&lt;li&gt;Enables graceful degradation with fallbacks&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ❌ Cons
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Adds complexity to every service call&lt;/li&gt;
&lt;li&gt;Threshold tuning is tricky (too sensitive = flapping, too lax = slow to trip)&lt;/li&gt;
&lt;li&gt;Half-open probes can still let some failures through&lt;/li&gt;
&lt;li&gt;Stale cached data shown as fallback may mislead users&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ⚖️ When to Use / When NOT to Use
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;✅ Use when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Calling external APIs or microservices (anything that can fail)&lt;/li&gt;
&lt;li&gt;Third-party integrations (payment gateways, SMS providers)&lt;/li&gt;
&lt;li&gt;Inter-service communication in microservices architecture&lt;/li&gt;
&lt;li&gt;Any synchronous call that has a timeout risk&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;❌ Avoid when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Calling a local in-process function (no network, no need)&lt;/li&gt;
&lt;li&gt;Using async message queues (they decouple naturally)&lt;/li&gt;
&lt;li&gt;Already using a service mesh like Istio (it handles this for you)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>systemdesign</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>DDoS Protection &amp; Rate Limiting Abuse Cases</title>
      <dc:creator>Gouranga Das Samrat</dc:creator>
      <pubDate>Sun, 06 Sep 2026 04:00:00 +0000</pubDate>
      <link>https://dev.to/gouranga-das-khulna/ddos-protection-rate-limiting-abuse-cases-3aac</link>
      <guid>https://dev.to/gouranga-das-khulna/ddos-protection-rate-limiting-abuse-cases-3aac</guid>
      <description>&lt;blockquote&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;One-liner:&lt;/strong&gt; A DDoS (Distributed Denial of Service) attack overwhelms your system with fake traffic until real users can't get through. Defense is layered — no single solution works alone.
&lt;/h2&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  ❓ Types of Attacks
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Volumetric&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Flood bandwidth (Gbps UDP)&lt;/td&gt;
&lt;td&gt;Network layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Protocol&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Exhaust TCP connections (SYN flood)&lt;/td&gt;
&lt;td&gt;Transport layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Application (L7)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HTTP flood, slowloris&lt;/td&gt;
&lt;td&gt;Your app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Credential stuffing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Try billions of stolen passwords&lt;/td&gt;
&lt;td&gt;Auth endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scraping&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Extract all your data&lt;/td&gt;
&lt;td&gt;Business logic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Account enumeration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Guess valid emails&lt;/td&gt;
&lt;td&gt;User existence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🧱 Defense in Depth (Layers)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet
   │
   ▼
┌──────────────────────────────────┐
│  CDN / DDoS Scrubbing Center     │  ← Cloudflare, AWS Shield
│  Filters volumetric attacks      │    Absorbs Tbps-scale traffic
│  IP reputation, anycast routing  │
└──────────────────┬───────────────┘
                   │ (clean traffic only)
                   ▼
┌──────────────────────────────────┐
│  WAF (Web Application Firewall)  │  ← Cloudflare WAF, AWS WAF
│  Blocks SQLi, XSS, bad patterns  │    Rate limits by IP/user-agent
│  OWASP Top 10 rules              │    Bot fingerprinting
└──────────────────┬───────────────┘
                   │
                   ▼
┌──────────────────────────────────┐
│  API Gateway / Load Balancer     │  ← Rate limiting per API key
│  Request throttling              │    IP allowlist/blocklist
│  Auth enforcement                │    Quota per user tier
└──────────────────┬───────────────┘
                   │
                   ▼
┌──────────────────────────────────┐
│  Application Rate Limiting       │  ← Redis-backed counters
│  Per-user, per-endpoint limits   │    Sliding window algorithm
│  CAPTCHA triggers                │    Business logic rules
└──────────────────┬───────────────┘
                   │
                   ▼
            Your Services
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🔑 Rate Limiting Abuse Cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Credential Stuffing Defense
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Stricter limits on auth endpoints&lt;/span&gt;
&lt;span class="nf"&gt;rateLimiter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/login&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;15min&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ip&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/forgot-password&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1hr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ip&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/api/register&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1hr&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ip&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// After 5 failures: require CAPTCHA&lt;/span&gt;
&lt;span class="c1"&gt;// After 10 failures: temp block IP for 1 hour&lt;/span&gt;
&lt;span class="c1"&gt;// Alert security team if 1000+ failures from IP range&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. API Abuse Tiers
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Free&lt;/span&gt; &lt;span class="nx"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;
&lt;span class="nx"&gt;Pro&lt;/span&gt; &lt;span class="nx"&gt;tier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;000&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;day&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;
&lt;span class="nx"&gt;Enterprise&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nx"&gt;Unlimited&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;custom&lt;/span&gt; &lt;span class="nx"&gt;limits&lt;/span&gt;

&lt;span class="c1"&gt;// Headers to return (industry standard):&lt;/span&gt;
&lt;span class="nx"&gt;X&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;RateLimit&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;Limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="nx"&gt;X&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;RateLimit&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;Remaining&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
&lt;span class="nx"&gt;X&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;RateLimit&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;Reset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1699999999&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unix&lt;/span&gt; &lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;Retry&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;After&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;               &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;when&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Slow Loris Defense
&lt;/h3&gt;

&lt;p&gt;Attacker sends HTTP headers very slowly to hold connections open.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Nginx settings&lt;/span&gt;
&lt;span class="k"&gt;client_body_timeout&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;client_header_timeout&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;keepalive_timeout&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;send_timeout&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🤖 Bot Detection Signals
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Legitimate User&lt;/th&gt;
&lt;th&gt;Bot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Request rate&lt;/td&gt;
&lt;td&gt;~1-2 req/sec&lt;/td&gt;
&lt;td&gt;100s req/sec&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User-agent&lt;/td&gt;
&lt;td&gt;Chrome/Firefox&lt;/td&gt;
&lt;td&gt;Empty or spoofed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TLS fingerprint&lt;/td&gt;
&lt;td&gt;Real browser&lt;/td&gt;
&lt;td&gt;curl, Python requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Behavioral pattern&lt;/td&gt;
&lt;td&gt;Random, varied&lt;/td&gt;
&lt;td&gt;Uniform, sequential&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JavaScript execution&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No (headless bots)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IP reputation&lt;/td&gt;
&lt;td&gt;Clean&lt;/td&gt;
&lt;td&gt;Known datacenter/VPN&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare Bot Management&lt;/strong&gt; and &lt;strong&gt;reCAPTCHA v3&lt;/strong&gt; automate most of this.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚙️ AWS Shield Tiers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Protection&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Shield Standard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automatic L3/L4 protection&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Shield Advanced&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;L7 protection, DDoS cost protection, 24/7 DRT&lt;/td&gt;
&lt;td&gt;$3,000/month&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Most startups: Cloudflare Free or Pro tier is sufficient.&lt;/p&gt;




&lt;h2&gt;
  
  
  ✅ Pros
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CDN/scrubbing centers absorb traffic before it reaches you&lt;/li&gt;
&lt;li&gt;WAF handles OWASP Top 10 without code changes&lt;/li&gt;
&lt;li&gt;Rate limiting protects business logic and database&lt;/li&gt;
&lt;li&gt;Layered approach means no single point of failure&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ❌ Cons
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Cloudflare/Shield costs money at scale&lt;/li&gt;
&lt;li&gt;Sophisticated L7 attacks (mimic real users) are hard to distinguish&lt;/li&gt;
&lt;li&gt;Blocking legitimate users with aggressive rate limits hurts UX&lt;/li&gt;
&lt;li&gt;Misconfigured WAF rules cause false positives&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ⚖️ When to Use / When NOT to Use
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;✅ Use when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any public-facing API or website (DDoS is a real threat)&lt;/li&gt;
&lt;li&gt;Financial or health apps (credential stuffing target)&lt;/li&gt;
&lt;li&gt;APIs with expensive backend operations (LLM calls, DB writes)&lt;/li&gt;
&lt;li&gt;Any endpoint that is unauthenticated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;❌ Don't over-engineer when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Internal-only API (behind VPN/private network)&lt;/li&gt;
&lt;li&gt;Early prototype with 10 users — Cloudflare Free is enough&lt;/li&gt;
&lt;li&gt;Traffic is already behind authentication (harder to abuse)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>systemdesign</category>
      <category>backend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Authentication &amp; Authorization — JWT &amp; OAuth 2.0</title>
      <dc:creator>Gouranga Das Samrat</dc:creator>
      <pubDate>Sat, 05 Sep 2026 04:00:00 +0000</pubDate>
      <link>https://dev.to/gouranga-das-khulna/authentication-authorization-jwt-oauth-20-2id7</link>
      <guid>https://dev.to/gouranga-das-khulna/authentication-authorization-jwt-oauth-20-2id7</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;One-liner:&lt;/strong&gt; Authentication proves &lt;em&gt;who you are&lt;/em&gt;; Authorization proves &lt;em&gt;what you're allowed to do&lt;/em&gt;. JWT and OAuth 2.0 are the industry standards for doing both at scale.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🎫 JWT — JSON Web Token
&lt;/h2&gt;

&lt;p&gt;A self-contained, signed token that carries claims about the user. No database lookup needed to verify.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structure: Header.Payload.Signature
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;eyJhbGciOiJIUzI&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;NiJ&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="err"&gt;.eyJ&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;c&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;VySWQiOjQyLCJyb&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;xlIjoiYWRtaW&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="err"&gt;iLCJleHAiOjE&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;MDAwMDB&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="err"&gt;.abc&lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="err"&gt;sig&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="err"&gt;HEADER&lt;/span&gt;&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="err"&gt;PAYLOAD&lt;/span&gt;&lt;span class="w"&gt;                                     &lt;/span&gt;&lt;span class="err"&gt;SIGNATURE&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;                   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;                                           &lt;/span&gt;&lt;span class="err"&gt;HMACSHA&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"alg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"HS256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;                              &lt;/span&gt;&lt;span class="err"&gt;base&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="err"&gt;(header)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"."&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"typ"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JWT"&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;                           &lt;/span&gt;&lt;span class="err"&gt;base&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="err"&gt;(payload)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="nl"&gt;"iat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1699999999&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;                         &lt;/span&gt;&lt;span class="err"&gt;secretKey&lt;/span&gt;&lt;span class="w"&gt;
                        &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1700003599&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;←&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;expires&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;hr&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
                      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  JWT Flow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Login:    Client ──► POST /login {email, password}
                        Server validates, creates JWT
             Client ◄── { token: "eyJ..." }

2. Use API:  Client ──► GET /profile
                        Headers: Authorization: Bearer eyJ...
                        Server decodes JWT, checks exp, checks role
             Client ◄── { user data }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Stateless = Scalable
&lt;/h3&gt;

&lt;p&gt;Server doesn't store sessions. Any server instance can verify the token using the secret key. No Redis/DB lookup needed per request.&lt;/p&gt;

&lt;h3&gt;
  
  
  JWT Risks &amp;amp; Mitigations
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;th&gt;Mitigation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Token stolen&lt;/td&gt;
&lt;td&gt;Short expiry (15min) + Refresh tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can't invalidate before expiry&lt;/td&gt;
&lt;td&gt;Refresh token blacklist in Redis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payload visible (base64 ≠ encrypted)&lt;/td&gt;
&lt;td&gt;Never store sensitive data in payload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Weak secret&lt;/td&gt;
&lt;td&gt;Use RS256 (asymmetric) for multi-service&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🔑 OAuth 2.0 — Delegated Authorization
&lt;/h2&gt;

&lt;p&gt;OAuth lets a third party app access resources on your behalf, without sharing your password.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Sign in with Google"&lt;/strong&gt; is OAuth 2.0 in action.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authorization Code Flow (Recommended)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User         Client App        Authorization Server       Resource Server
  |               |             (Google/GitHub/etc)          (Your API)
  |──"Login"─────►|
  |               |──Redirect──►|
  |               |  client_id  |
  |◄──────────────|             |
  |────Login to──►|             |
  |   Google      |             |
  |               |◄─auth code──|
  |               |             |
  |               |──POST /token|
  |               |  code +     |
  |               |  client_secret
  |               |◄─access_token + refresh_token
  |               |                                         |
  |               |──GET /userinfo (Bearer access_token)───►|
  |               |◄── { id, email, name }──────────────────|
  |               |
  |◄──Logged in!──|
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key OAuth Concepts
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;What It Is&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Client&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Your app&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Resource Owner&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The user&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Authorization Server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Google, GitHub, Auth0 (issues tokens)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Resource Server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;API that accepts access tokens&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Access Token&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Short-lived (1hr) — use to call APIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Refresh Token&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Long-lived (30 days) — exchange for new access token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scope&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Permissions granted: &lt;code&gt;read:email&lt;/code&gt;, &lt;code&gt;write:profile&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🔐 Session vs JWT Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Session&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;JWT&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Storage&lt;/td&gt;
&lt;td&gt;Server-side (Redis/DB)&lt;/td&gt;
&lt;td&gt;Client-side (localStorage / cookie)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revocation&lt;/td&gt;
&lt;td&gt;Instant (delete session)&lt;/td&gt;
&lt;td&gt;Must wait for expiry (or blacklist)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scale&lt;/td&gt;
&lt;td&gt;Needs shared session store&lt;/td&gt;
&lt;td&gt;Stateless — any server works&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payload&lt;/td&gt;
&lt;td&gt;Just session ID&lt;/td&gt;
&lt;td&gt;Full claims (userId, role, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Monolith, small-medium scale&lt;/td&gt;
&lt;td&gt;Microservices, APIs, mobile&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  ✅ Pros
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;JWT:&lt;/strong&gt; Stateless, scalable, no DB lookup per request, cross-service&lt;br&gt;&lt;br&gt;
&lt;strong&gt;OAuth:&lt;/strong&gt; No password sharing, granular scopes, industry standard for SSO&lt;/p&gt;

&lt;h2&gt;
  
  
  ❌ Cons
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;JWT:&lt;/strong&gt; Revocation is hard, payload visible, long tokens add overhead&lt;br&gt;&lt;br&gt;
&lt;strong&gt;OAuth:&lt;/strong&gt; Complex flow, misconfig is a security disaster, token leakage risks&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚖️ When to Use / When NOT to Use
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;✅ JWT — use when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;REST APIs serving mobile or SPA clients&lt;/li&gt;
&lt;li&gt;Microservices that need to pass identity between services&lt;/li&gt;
&lt;li&gt;Stateless, horizontally scaled backends&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;✅ OAuth — use when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Third-party "Sign in with X" integrations&lt;/li&gt;
&lt;li&gt;Allowing external apps to access user data on your platform&lt;/li&gt;
&lt;li&gt;Enterprise SSO (Single Sign-On)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;❌ Avoid JWT when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need instant logout / token revocation (use sessions with Redis)&lt;/li&gt;
&lt;li&gt;Highly sensitive systems where payload visibility is a concern&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>systemdesign</category>
      <category>backend</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>HLD: Twitter / X</title>
      <dc:creator>Gouranga Das Samrat</dc:creator>
      <pubDate>Sun, 30 Aug 2026 02:00:00 +0000</pubDate>
      <link>https://dev.to/gouranga-das-khulna/hld-twitter-x-41kc</link>
      <guid>https://dev.to/gouranga-das-khulna/hld-twitter-x-41kc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Design a social media platform where users post tweets, follow each other, and see a timeline of tweets from accounts they follow.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1️⃣ Clarify Requirements
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Functional Requirements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Post a tweet (text, images, videos)&lt;/li&gt;
&lt;li&gt;Follow / unfollow users&lt;/li&gt;
&lt;li&gt;View home timeline (tweets from followed users, newest first)&lt;/li&gt;
&lt;li&gt;Like, retweet, reply&lt;/li&gt;
&lt;li&gt;User profile with tweet history&lt;/li&gt;
&lt;li&gt;Search tweets&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Non-Functional Requirements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High availability&lt;/strong&gt; — Twitter is a utility&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eventual consistency&lt;/strong&gt; — timeline can be slightly stale (seconds)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low latency&lt;/strong&gt; — timeline load &amp;lt; 200ms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read-heavy&lt;/strong&gt; — 100:1 read-to-write ratio&lt;/li&gt;
&lt;li&gt;Scale: 300M DAU, 500M tweets/day&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2️⃣ Estimate Scale
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tweets/day: 500M
Write QPS:  500M / 86,400 ≈ 5,800 writes/sec
Read QPS:   5,800 × 100 = 580,000 reads/sec
Peak read:  ~1-2M reads/sec

Storage:
  Tweet: 280 chars = ~300 bytes + metadata ≈ 1 KB
  500M tweets/day × 1 KB = 500 GB/day
  Media: ~100 TB/day (images, videos)

Timeline cache:
  300M users × 100 tweet IDs × 8 bytes = 240 GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3️⃣ API Design
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /tweets
Body: { text, mediaIds[] }
Response: { tweetId, createdAt }

GET /timeline/home?cursor=&amp;amp;limit=20
Response: { tweets: [...], nextCursor }

GET /users/:userId/tweets?cursor=
Response: { tweets: [...], nextCursor }

POST /tweets/:tweetId/likes
DELETE /tweets/:tweetId/likes

POST /users/:userId/follow
DELETE /users/:userId/follow
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4️⃣ The Core Problem: Home Timeline
&lt;/h2&gt;

&lt;p&gt;The hardest part. User A follows 500 people. How do you fetch their timeline?&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach 1: Pull (Fan-out on Read)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Timeline request:
1. Get all 500 followed user IDs
2. Query tweets from each user (last 24h)
3. Merge and sort by time
4. Return top 20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;❌ Slow: 500 DB queries per timeline request&lt;br&gt;&lt;br&gt;
❌ Gets worse as you follow more people&lt;br&gt;&lt;br&gt;
❌ Doesn't scale at 580K reads/sec&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach 2: Push (Fan-out on Write) — Twitter's Original Approach
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User A tweets:
1. Find all A's followers (e.g., 1,000 followers)
2. Push tweet ID to each follower's timeline cache
3. Timeline request → just read from your cached timeline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tweet created → [Fan-out Service]
                    → Redis: user1_timeline.lpush(tweetId)
                    → Redis: user2_timeline.lpush(tweetId)
                    → Redis: user3_timeline.lpush(tweetId)
                    ... (1,000 followers)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;✅ Timeline reads are O(1) — just read from Redis&lt;br&gt;&lt;br&gt;
❌ Celebrity problem: Lady Gaga has 100M followers → 100M Redis writes per tweet&lt;br&gt;&lt;br&gt;
❌ Wasted if followers don't check timeline&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach 3: Hybrid (Twitter's Current Approach)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Regular users (&amp;lt; 1M followers): Fan-out on Write
Celebrity users (&amp;gt; threshold):  Fan-out on Read

Timeline request:
1. Fetch pre-computed timeline from Redis (fan-out writes)
2. Fetch recent tweets from celebrity accounts you follow (fan-out reads)
3. Merge both, sort, return
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5️⃣ High-Level Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Client]
   │
   ▼
[CDN] ← static assets, media
   │
[API Gateway + LB]
   │
   ├──────────────────────────────────────┐
   │                                      │
[Tweet Service]                   [Timeline Service]
   │                                      │
   ├─► [Media Service] → [S3/CDN]         ├─► [Timeline Cache (Redis)]
   │                                      │         (500 tweet IDs per user)
   └─► [Tweets DB]                        └─► [Fan-out Service]
       (Cassandra: write-heavy,                   │
        time-series)                        [Message Queue (Kafka)]
                                                  │
                                        [Worker Pool: fan-out writers]
                                                  │
                                          [Timeline Cache Redis]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6️⃣ Database Design
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Tweets Table (Cassandra)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;tweets&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;tweet_id&lt;/span&gt;     &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;-- Snowflake ID (time-sortable)&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;      &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nb"&gt;text&lt;/span&gt;         &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;media_ids&lt;/span&gt;    &lt;span class="n"&gt;LIST&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;   &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;likes_count&lt;/span&gt;  &lt;span class="n"&gt;COUNTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;retweet_count&lt;/span&gt; &lt;span class="n"&gt;COUNTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tweet_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;-- partition by user, sort by tweet&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;CLUSTERING&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tweet_id&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why Cassandra?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Write-heavy (500M tweets/day)&lt;/li&gt;
&lt;li&gt;Time-series access pattern (recent tweets)&lt;/li&gt;
&lt;li&gt;Naturally partitioned by user_id&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Followers Table (Graph in Cassandra)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Who does user X follow?&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;following&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;follower_id&lt;/span&gt;  &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;followed_id&lt;/span&gt;  &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;   &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;follower_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;followed_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Who follows user X?&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;followers&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;followed_id&lt;/span&gt;  &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;follower_id&lt;/span&gt;  &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;   &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;followed_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;follower_id&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;Denormalized for O(1) lookup in both directions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Timeline Cache (Redis)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Key: timeline:{userId}
Value: Sorted Set of tweet IDs (score = timestamp)
Size: Keep last 800 tweet IDs
TTL: 7 days (trim inactive users)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7️⃣ Media Storage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tweet with image:
1. Client uploads image → [Media Service]
2. Media Service → S3 (original)
3. Media Service → async resize: thumbnail, medium, large
4. All sizes stored in S3
5. CloudFront CDN in front of S3
6. Tweet stores mediaId → resolved to CDN URL on read
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  8️⃣ Search
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tweets → Elasticsearch
Index: tweet_id, text, user_id, created_at, hashtags

Search query → Elasticsearch → ranked results
Trending hashtags → pre-computed every 5 min, stored in Redis
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  9️⃣ The Celebrity (Hotspot) Problem
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Elon Musk tweets → 100M followers
Fan-out on write: 100M Redis writes in seconds → IMPOSSIBLE

Solution:
- Maintain a "high-follower" list
- Skip fan-out for celebrities
- At read time: fetch celebrity tweets separately, merge with pre-computed timeline
- Cache celebrity recent tweets (they're read by millions)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🎨 Diagram
&lt;/h2&gt;

&lt;p&gt;The diagram shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full architecture: Client → CDN → Gateway → Services&lt;/li&gt;
&lt;li&gt;Fan-out on write flow (Kafka → workers → Redis)&lt;/li&gt;
&lt;li&gt;Hybrid timeline assembly (pre-computed + celebrity merge)&lt;/li&gt;
&lt;li&gt;Cassandra tweet storage partition scheme&lt;/li&gt;
&lt;li&gt;Media upload and CDN serving path&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ✅ Trade-offs Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Rationale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Timeline generation&lt;/td&gt;
&lt;td&gt;Hybrid push/pull&lt;/td&gt;
&lt;td&gt;Pure push fails for celebrities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tweet storage&lt;/td&gt;
&lt;td&gt;Cassandra&lt;/td&gt;
&lt;td&gt;Write-heavy, time-series&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeline cache&lt;/td&gt;
&lt;td&gt;Redis Sorted Set&lt;/td&gt;
&lt;td&gt;O(log N) insert, O(1) range read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fan-out&lt;/td&gt;
&lt;td&gt;Async via Kafka&lt;/td&gt;
&lt;td&gt;Don't block tweet creation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Media&lt;/td&gt;
&lt;td&gt;S3 + CDN&lt;/td&gt;
&lt;td&gt;Scalable object storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consistency&lt;/td&gt;
&lt;td&gt;Eventual&lt;/td&gt;
&lt;td&gt;Timeline can be 1-2s stale — fine&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>systemdesign</category>
      <category>algorithms</category>
      <category>backend</category>
    </item>
    <item>
      <title>HLD: Instagram Feed</title>
      <dc:creator>Gouranga Das Samrat</dc:creator>
      <pubDate>Sat, 29 Aug 2026 02:00:00 +0000</pubDate>
      <link>https://dev.to/gouranga-das-khulna/hld-instagram-feed-584f</link>
      <guid>https://dev.to/gouranga-das-khulna/hld-instagram-feed-584f</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Design a photo-sharing platform where users post photos and see a feed of photos from people they follow.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1️⃣ Clarify Requirements
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Functional Requirements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Upload photos/videos&lt;/li&gt;
&lt;li&gt;Follow/unfollow users&lt;/li&gt;
&lt;li&gt;View home feed (photos from followed users, newest first)&lt;/li&gt;
&lt;li&gt;Like, comment on photos&lt;/li&gt;
&lt;li&gt;User profile with photo grid&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Non-Functional Requirements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;High availability&lt;/li&gt;
&lt;li&gt;Eventual consistency for feed (seconds lag is fine)&lt;/li&gt;
&lt;li&gt;Low latency feed load &amp;lt; 200ms&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image-heavy&lt;/strong&gt; — media storage is the core challenge&lt;/li&gt;
&lt;li&gt;Scale: 500M DAU, 100M photos uploaded/day&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2️⃣ Estimate Scale
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Photo uploads/day: 100M
Write QPS: 100M / 86,400 ≈ 1,160 uploads/sec
Read QPS: 500M × 10 feed loads/day / 86,400 ≈ 58,000 reads/sec

Storage:
  Avg photo: 3 MB (original) + thumbnails (100KB, 500KB) ≈ 4 MB total
  100M photos/day × 4 MB = 400 TB/day
  5-year storage: ~730 PB (need tiered storage!)

CDN bandwidth:
  58K reads/sec × 10 photos/feed × 500KB avg = ~290 GB/sec outbound
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3️⃣ Architecture Overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Client]
   │
[CDN] ← photos served here (not from origin)
   │
[API Gateway + LB]
   │
   ├── [Photo Service] → upload → [S3] → trigger → [Resize Worker]
   │                                                      → S3 (thumbnail, medium, large)
   │                                                      → Update metadata DB
   │
   ├── [Feed Service] → Redis (pre-computed feed) → return photoIds → CDN URLs
   │
   ├── [User Service] → PostgreSQL
   │
   └── [Social Graph] → Cassandra (followers/following)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4️⃣ Photo Upload Flow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Client → POST /upload → Photo Service
2. Photo Service → generate uploadId → return S3 pre-signed URL
3. Client → upload directly to S3 (bypasses your servers!)
4. S3 → triggers Lambda / sends event to queue
5. Resize Worker → create 3 sizes → store in S3
6. Update metadata DB: photoId, userId, S3 keys, created_at
7. Fan-out → push photoId to followers' feed caches (async)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pre-signed URL pattern&lt;/strong&gt; = your servers never touch the image bytes.&lt;/p&gt;




&lt;h2&gt;
  
  
  5️⃣ Feed Generation (Hybrid Fan-out)
&lt;/h2&gt;

&lt;p&gt;Same pattern as Twitter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Regular users → fan-out on write&lt;/li&gt;
&lt;li&gt;Celebrities → fan-out on read, merge at read time
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Redis feed cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;feed:{userId}&lt;/span&gt;
  &lt;span class="na"&gt;Value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Sorted Set of {photoId, timestamp}&lt;/span&gt;
  &lt;span class="na"&gt;Size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;500 entries max&lt;/span&gt;
  &lt;span class="na"&gt;TTL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;7 days&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6️⃣ Database Schema
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Photos&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;photos&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;photo_id&lt;/span&gt;    &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;-- Snowflake ID&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;     &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;s3_key_orig&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;s3_key_med&lt;/span&gt;  &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;s3_key_thumb&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;caption&lt;/span&gt;     &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;  &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;likes_count&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Social graph (Cassandra)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="k"&gt;following&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;follower_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;followed_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;follower_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;followed_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7️⃣ Key Challenges
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Storage Tiering
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hot (0-7 days):   S3 Standard (fast, expensive)
Warm (7-90 days): S3 Infrequent Access (cheaper)
Cold (90+ days):  S3 Glacier (very cheap, slow retrieval)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Serving Photos
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;NEVER serve from origin → always from CDN&lt;/li&gt;
&lt;li&gt;URL format: &lt;code&gt;https://cdn.instagram.com/photos/{photoId}/{size}.jpg&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Immutable URLs (content never changes for same ID) → cache forever&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ✅ Trade-offs Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Rationale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Photo storage&lt;/td&gt;
&lt;td&gt;S3 + CDN&lt;/td&gt;
&lt;td&gt;Scalable object store, edge serving&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upload pattern&lt;/td&gt;
&lt;td&gt;Pre-signed URL&lt;/td&gt;
&lt;td&gt;Don't bottleneck servers with bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feed&lt;/td&gt;
&lt;td&gt;Hybrid fan-out&lt;/td&gt;
&lt;td&gt;Celebrities break pure push&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Social graph&lt;/td&gt;
&lt;td&gt;Cassandra&lt;/td&gt;
&lt;td&gt;Write-heavy, large-scale relationships&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image sizes&lt;/td&gt;
&lt;td&gt;3 variants&lt;/td&gt;
&lt;td&gt;Different contexts (thumbnail, feed, full)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>systemdesign</category>
      <category>backend</category>
      <category>algorithms</category>
    </item>
    <item>
      <title>From One Merged PR to a Repology Page: A Termux Maintainer Roundup</title>
      <dc:creator>Gouranga Das Samrat</dc:creator>
      <pubDate>Wed, 26 Aug 2026 05:44:45 +0000</pubDate>
      <link>https://dev.to/gouranga-das-khulna/from-one-merged-pr-to-a-repology-page-a-termux-maintainer-roundup-1c44</link>
      <guid>https://dev.to/gouranga-das-khulna/from-one-merged-pr-to-a-repology-page-a-termux-maintainer-roundup-1c44</guid>
      <description>&lt;p&gt;A few weeks ago, running &lt;code&gt;pkg show&lt;/code&gt; on my own phone and seeing my name next to &lt;code&gt;Maintainer:&lt;/code&gt; felt like a fluke. It wasn't a plan — it was &lt;a href="https://dev.to/gouranga-das-khulna/from-a-high-school-termux-user-to-a-package-maintainer-the-story-behind-my-first-merged-pr-on-kgc"&gt;one feature request for a password manager CLI&lt;/a&gt; that turned into weeks of build-system archaeology. Since then it hasn't really stopped.&lt;/p&gt;

&lt;p&gt;I've written four posts about individual pieces of this so far. This one is the zoomed-out version — everything currently sitting across &lt;a href="https://github.com/termux/termux-packages" rel="noopener noreferrer"&gt;&lt;code&gt;termux/termux-packages&lt;/code&gt;&lt;/a&gt; (the main repo) and &lt;a href="https://github.com/termux/termux-user-repository" rel="noopener noreferrer"&gt;&lt;code&gt;termux/termux-user-repository&lt;/code&gt;&lt;/a&gt; (TUR), plus what's shipped since the last post that I haven't written up on its own yet. If you want the receipts instead of my word for it, my &lt;a href="https://repology.org/maintainer/gouranga.das.khulna%40gmail.com" rel="noopener noreferrer"&gt;Repology maintainer page&lt;/a&gt; tracks every package live under my name in real time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The story so far
&lt;/h2&gt;

&lt;p&gt;In case you're coming in fresh, here's the series in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://dev.to/gouranga-das-khulna/from-a-high-school-termux-user-to-a-package-maintainer-the-story-behind-my-first-merged-pr-on-kgc"&gt;From a High School Termux User to a Package Maintainer&lt;/a&gt; — how &lt;a href="https://github.com/termux/termux-packages/pull/30987" rel="noopener noreferrer"&gt;&lt;code&gt;proton-pass-cli&lt;/code&gt;&lt;/a&gt; became my first merge on the main repo, and the sqlcipher symlink bug it dragged along with it.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/gouranga-das-khulna/rejected-on-main-accepted-on-tur-how-8-nerd-fonts-became-my-first-merge-on-the-termux-user-4e88"&gt;Rejected on Main, Accepted on TUR&lt;/a&gt; — nine Nerd Fonts packages that didn't fit the main repo's size policy, and my first merge on TUR instead.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/gouranga-das-khulna/from-a-4-year-old-feature-request-to-pkg-install-bun-my-second-merge-on-the-termux-user-567"&gt;From a 4-Year-Old Feature Request to &lt;code&gt;pkg install bun&lt;/code&gt;&lt;/a&gt; — closing a four-year-old issue by moving &lt;code&gt;bun&lt;/code&gt; from a rejected main-repo PR to a TUR merge.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dev.to/gouranga-das-khulna/the-day-i-merged-6-go-packages-into-termux-before-dinner-47ne"&gt;The Day I Merged 6 Go Packages Into Termux Before Dinner&lt;/a&gt; — &lt;code&gt;goimports&lt;/code&gt;, &lt;code&gt;golangci-lint&lt;/code&gt;, &lt;code&gt;air&lt;/code&gt;, &lt;code&gt;gotests&lt;/code&gt;, &lt;code&gt;goreleaser&lt;/code&gt;, and &lt;code&gt;govulncheck&lt;/code&gt;, all merged in one sitting.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything below picks up right after that last post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Main repo: cleanup, then a second wave
&lt;/h2&gt;

&lt;p&gt;The six Go packages didn't land perfectly clean. Right after merge, each one still carried a build dependency it didn't actually need at runtime, so I went back through all six with individual fix PRs: &lt;a href="https://github.com/termux/termux-packages/pull/31286" rel="noopener noreferrer"&gt;goimports&lt;/a&gt;, &lt;a href="https://github.com/termux/termux-packages/pull/31287" rel="noopener noreferrer"&gt;golangci-lint&lt;/a&gt;, &lt;a href="https://github.com/termux/termux-packages/pull/31288" rel="noopener noreferrer"&gt;air&lt;/a&gt;, &lt;a href="https://github.com/termux/termux-packages/pull/31289" rel="noopener noreferrer"&gt;goreleaser&lt;/a&gt;, &lt;a href="https://github.com/termux/termux-packages/pull/31290" rel="noopener noreferrer"&gt;gotests&lt;/a&gt;, and &lt;a href="https://github.com/termux/termux-packages/pull/31291" rel="noopener noreferrer"&gt;govulncheck&lt;/a&gt;. Small, boring PRs — the kind that don't make for a good story on their own, but matter for anyone who installs these packages later. &lt;code&gt;govulncheck&lt;/code&gt; also got bumped to &lt;a href="https://github.com/termux/termux-packages/pull/31293" rel="noopener noreferrer"&gt;1.7.0&lt;/a&gt; the same day.&lt;/p&gt;

&lt;p&gt;With the toolkit's foundation cleaned up, I kept adding to it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/termux/termux-packages/pull/31294" rel="noopener noreferrer"&gt;&lt;code&gt;git-cliff&lt;/code&gt;&lt;/a&gt; — changelog generator driven by conventional commits.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/termux/termux-packages/pull/31298" rel="noopener noreferrer"&gt;&lt;code&gt;git-absorb&lt;/code&gt;&lt;/a&gt; — automatically absorbs staged changes into the right earlier commits.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/termux/termux-packages/pull/31303" rel="noopener noreferrer"&gt;&lt;code&gt;gtrash&lt;/code&gt;&lt;/a&gt; — a trash can for the CLI, so &lt;code&gt;rm&lt;/code&gt; stops being permanent by accident.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/termux/termux-packages/pull/31309" rel="noopener noreferrer"&gt;&lt;code&gt;sqlc&lt;/code&gt;&lt;/a&gt; — generates type-safe code straight from SQL.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/termux/termux-packages/pull/31310" rel="noopener noreferrer"&gt;&lt;code&gt;golang-migrate&lt;/code&gt;&lt;/a&gt; — database migrations, CLI and library both.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/termux/termux-packages/pull/31312" rel="noopener noreferrer"&gt;&lt;code&gt;gotestsum&lt;/code&gt;&lt;/a&gt; — a nicer &lt;code&gt;go test&lt;/code&gt; output wrapper, pairs naturally with &lt;code&gt;gotests&lt;/code&gt; from the earlier batch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And &lt;code&gt;proton-pass-cli&lt;/code&gt;, the package that started all of this, &lt;a href="https://github.com/termux/termux-packages/pull/31314" rel="noopener noreferrer"&gt;got bumped to 2.3.3&lt;/a&gt; — still getting routine maintenance almost two weeks after the original merge.&lt;/p&gt;

&lt;p&gt;Not everything from this stretch made it into the main repo, and that's expected at this point: &lt;a href="https://github.com/termux/termux-packages/pull/31157" rel="noopener noreferrer"&gt;PR #31157&lt;/a&gt; (the original 9-font attempt) and &lt;a href="https://github.com/termux/termux-packages/pull/31115" rel="noopener noreferrer"&gt;PR #31115&lt;/a&gt; (the prebuilt-binary &lt;code&gt;bun&lt;/code&gt; attempt) both closed on policy grounds, and &lt;a href="https://github.com/termux/termux-packages/pull/31013" rel="noopener noreferrer"&gt;PR #31013&lt;/a&gt; closed as a duplicate of a fix that landed first. All three have their full story in the earlier posts — I'm only listing them here so the trail is complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  TUR: the fonts grow up
&lt;/h2&gt;

&lt;p&gt;TUR has been quieter in terms of headline moments, but not idle. The eight Nerd Fonts packages from &lt;a href="https://github.com/termux/termux-user-repository/pull/2743" rel="noopener noreferrer"&gt;PR #2743&lt;/a&gt; started out as eight independent &lt;code&gt;ttf-*-nerd&lt;/code&gt; packages. In &lt;a href="https://github.com/termux/termux-user-repository/pull/2755" rel="noopener noreferrer"&gt;PR #2755&lt;/a&gt; I refactored all eight into a single unified &lt;code&gt;nerd-fonts&lt;/code&gt; parent/subpackage structure and bumped everything to v3.5.1 — one source package, eight subpackages, instead of eight nearly-identical &lt;code&gt;build.sh&lt;/code&gt; files to keep in sync by hand. It hit the usual round of CI friction along the way (a missing cache directory variable, an overlength description, a license auto-detection miss, an array-vs-scalar source URL, a subpackage dependency value that needed correcting) — nothing dramatic, just the kind of thing that shows up once you actually run a consolidation through CI instead of just planning it.&lt;/p&gt;

&lt;p&gt;Right after that, &lt;a href="https://github.com/termux/termux-user-repository/pull/2762" rel="noopener noreferrer"&gt;PR #2762&lt;/a&gt; added a dependency on the main repo's pre-existing &lt;code&gt;ttf-nerd-fonts-symbols&lt;/code&gt; package, to match how Arch handles the same font set — closing a small naming/parity gap that had been sitting there since the original PR.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bun&lt;/code&gt; also has a bump sitting as &lt;a href="https://github.com/termux/termux-user-repository/pull/2749" rel="noopener noreferrer"&gt;a draft&lt;/a&gt; — 1.4.0, not merged yet. And there's an early-stage &lt;code&gt;docker-qemu&lt;/code&gt; package in draft too, at &lt;a href="https://github.com/termux/termux-user-repository/pull/2708" rel="noopener noreferrer"&gt;PR #2708&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where things stand
&lt;/h2&gt;

&lt;p&gt;Two repos, two different sets of rules, and by now a pattern I'm used to: try the main repo first, and if the policy genuinely doesn't fit — package count discipline, source-build requirements — TUR is where it goes instead, not a consolation prize.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://repology.org/maintainer/gouranga.das.khulna%40gmail.com" rel="noopener noreferrer"&gt;Repology maintainer page&lt;/a&gt; is the easiest way to see the live total, since PR merges don't always mean immediate publish. As of today it lists &lt;strong&gt;15 distinct projects&lt;/strong&gt; across both repos — &lt;strong&gt;13&lt;/strong&gt; on Termux's main repo, &lt;strong&gt;2&lt;/strong&gt; on TUR — every single one at &lt;strong&gt;100% newest&lt;/strong&gt;, with nothing outdated, problematic, or flagged as potentially vulnerable:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;air-live-reload&lt;/code&gt;, &lt;code&gt;bun&lt;/code&gt;, &lt;code&gt;fonts:nerd-fonts&lt;/code&gt;, &lt;code&gt;git-absorb&lt;/code&gt;, &lt;code&gt;git-cliff&lt;/code&gt;, &lt;code&gt;go:migrate&lt;/code&gt;, &lt;code&gt;goimports&lt;/code&gt;, &lt;code&gt;golangci-lint&lt;/code&gt;, &lt;code&gt;goreleaser&lt;/code&gt;, &lt;code&gt;gotests&lt;/code&gt;, &lt;code&gt;gotestsum&lt;/code&gt;, &lt;code&gt;govulncheck&lt;/code&gt;, &lt;code&gt;gtrash&lt;/code&gt;, &lt;code&gt;proton-pass-cli&lt;/code&gt;, &lt;code&gt;sqlc&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Two entries are worth a second look because the names don't match the PR titles: Repology tracks &lt;code&gt;air&lt;/code&gt; under its upstream project name &lt;code&gt;air-live-reload&lt;/code&gt;, and &lt;code&gt;golang-migrate&lt;/code&gt; as &lt;code&gt;go:migrate&lt;/code&gt;. The other one is &lt;code&gt;fonts:nerd-fonts&lt;/code&gt; showing up as a single TUR project instead of eight — which is exactly what &lt;a href="https://github.com/termux/termux-user-repository/pull/2755" rel="noopener noreferrer"&gt;PR #2755&lt;/a&gt; was for. The consolidation into one parent/subpackage structure didn't just clean up the &lt;code&gt;build.sh&lt;/code&gt; files, it collapsed the whole set into one tracked project on Repology too.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>rust</category>
      <category>go</category>
      <category>android</category>
    </item>
    <item>
      <title>The Day I Merged 6 Go Packages Into Termux Before Dinner 🐹</title>
      <dc:creator>Gouranga Das Samrat</dc:creator>
      <pubDate>Mon, 24 Aug 2026 04:27:22 +0000</pubDate>
      <link>https://dev.to/gouranga-das-khulna/the-day-i-merged-6-go-packages-into-termux-before-dinner-47ne</link>
      <guid>https://dev.to/gouranga-das-khulna/the-day-i-merged-6-go-packages-into-termux-before-dinner-47ne</guid>
      <description>&lt;p&gt;I didn't wake up planning to get six pull requests merged. I just wanted to add one missing Go tool to &lt;a href="https://github.com/termux/termux-packages" rel="noopener noreferrer"&gt;termux/termux-packages&lt;/a&gt;. Then I figured, while I'm in here, why not bring the rest of my Go toolkit along too? A few hours and six PRs later, all of them were sitting in &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's how it went down — and what actually shipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Landed
&lt;/h2&gt;

&lt;p&gt;Termux users can now &lt;code&gt;pkg install&lt;/code&gt; a proper Go developer toolkit, straight from the official repo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/termux/termux-packages/pull/31270" rel="noopener noreferrer"&gt;goimports 0.49.0&lt;/a&gt;&lt;/strong&gt; — the tool that quietly fixes your import block, adding what's missing and trimming what isn't used anymore. &lt;a href="https://pkg.go.dev/golang.org/x/tools/cmd/goimports" rel="noopener noreferrer"&gt;Learn more →&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/termux/termux-packages/pull/31271" rel="noopener noreferrer"&gt;golangci-lint 2.13.1&lt;/a&gt;&lt;/strong&gt; — dozens of Go linters bundled into one fast binary. At 45 MB installed, it's the heavyweight of the group, but worth every megabyte. &lt;a href="https://golangci-lint.run/" rel="noopener noreferrer"&gt;Learn more →&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/termux/termux-packages/pull/31272" rel="noopener noreferrer"&gt;air 1.67.4&lt;/a&gt;&lt;/strong&gt; — live reload for Go apps, so your binary rebuilds and restarts the second you save a file. &lt;a href="https://github.com/air-verse/air" rel="noopener noreferrer"&gt;Learn more →&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/termux/termux-packages/pull/31273" rel="noopener noreferrer"&gt;gotests 1.9.0&lt;/a&gt;&lt;/strong&gt; — generates table-driven test scaffolding straight from your source, so you stop typing the same boilerplate. &lt;a href="https://github.com/cweill/gotests" rel="noopener noreferrer"&gt;Learn more →&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/termux/termux-packages/pull/31274" rel="noopener noreferrer"&gt;goreleaser 2.17.1&lt;/a&gt;&lt;/strong&gt; — packages and ships Go binaries in one command, cross-compilation included. &lt;a href="https://goreleaser.com" rel="noopener noreferrer"&gt;Learn more →&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/termux/termux-packages/pull/31276" rel="noopener noreferrer"&gt;govulncheck 1.1.4&lt;/a&gt;&lt;/strong&gt; — scans your code and dependencies for known vulnerabilities. &lt;a href="https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck" rel="noopener noreferrer"&gt;Learn more →&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Six packages, one theme: making Termux a real place to write Go.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Curveball: A Last-Minute Rebase
&lt;/h2&gt;

&lt;p&gt;Right as review was wrapping up, maintainer &lt;a href="https://github.com/robertkirkman" rel="noopener noreferrer"&gt;@robertkirkman&lt;/a&gt; asked for something I hadn't planned for: rebase and force-push all six PRs so they'd recompile against the just-released &lt;strong&gt;Go 1.27.0&lt;/strong&gt;. You can see the ask &lt;a href="https://github.com/termux/termux-packages/pull/31270" rel="noopener noreferrer"&gt;in the goimports thread&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;No arguing with that logic — better to ship on the latest compiler than patch it later. I rebased all six branches, force-pushed, and got a quick "Thank you!" in response. Not long after, the merges started coming in one by one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proof It's Real
&lt;/h2&gt;

&lt;p&gt;Once everything landed, I ran the most satisfying command in the Termux workflow — &lt;code&gt;pkg show&lt;/code&gt; — against all six:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Package: goimports        | 0.49.0  | https://pkg.go.dev/golang.org/x/tools/cmd/goimports
Package: golangci-lint    | 2.13.1  | https://golangci-lint.run/
Package: air              | 1.67.4  | https://github.com/air-verse/air
Package: gotests           | 1.9.0   | https://github.com/cweill/gotests
Package: goreleaser        | 2.17.1  | https://goreleaser.com
Package: govulncheck       | 1.1.4   | https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six for six. All installable right now with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pkg &lt;span class="nb"&gt;install &lt;/span&gt;goimports golangci-lint air gotests goreleaser govulncheck
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I'd Tell Someone Trying This Themselves
&lt;/h2&gt;

&lt;p&gt;Batch related packages together. Reviewers can apply one piece of feedback — like a compiler bump — across the whole set instead of repeating themselves six times.&lt;/p&gt;

&lt;p&gt;Move fast on review feedback. The gap between "please rebase" and "rebased and pushed" is often the difference between a PR that merges today and one that sits for a week.&lt;/p&gt;

&lt;p&gt;Don't underestimate the boring packages. None of these tools are glamorous. Together, though, they turn a phone into a legitimate Go dev environment.&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://github.com/robertkirkman" rel="noopener noreferrer"&gt;@robertkirkman&lt;/a&gt; and the rest of the &lt;a href="https://github.com/termux/termux-packages" rel="noopener noreferrer"&gt;termux/termux-packages&lt;/a&gt; team for the fast, thorough reviews. More packages coming soon.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Working on a Go tool you think Termux is missing? Tell me about it in the comments — I'm always looking for the next PR.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>android</category>
      <category>termux</category>
      <category>opensource</category>
    </item>
    <item>
      <title>HLD: Notification System</title>
      <dc:creator>Gouranga Das Samrat</dc:creator>
      <pubDate>Sun, 23 Aug 2026 02:00:00 +0000</pubDate>
      <link>https://dev.to/gouranga-das-khulna/hld-notification-system-cdl</link>
      <guid>https://dev.to/gouranga-das-khulna/hld-notification-system-cdl</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Design a notification system that sends push notifications, emails, and SMS to users based on events in the platform.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1️⃣ Clarify Requirements
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Functional Requirements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Send notifications via: Push (iOS/Android), Email, SMS&lt;/li&gt;
&lt;li&gt;Trigger types: real-time (someone liked your post) and scheduled (weekly digest)&lt;/li&gt;
&lt;li&gt;Users can set preferences: opt-in/out per channel and notification type&lt;/li&gt;
&lt;li&gt;Support for &lt;strong&gt;templated&lt;/strong&gt; notifications (not hardcoded)&lt;/li&gt;
&lt;li&gt;Delivery guarantee — important notifications must not be dropped&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Non-Functional Requirements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High throughput&lt;/strong&gt; — 10M notifications/day&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low latency&lt;/strong&gt; for real-time notifications (&amp;lt; 5 seconds)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability&lt;/strong&gt; — retry on failure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability&lt;/strong&gt; — handle spikes (breaking news, product launches)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt; — track delivered/failed/opened&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2️⃣ Estimate Scale
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Notifications/day: 10M
Notifications/sec: 10M / 86,400 ≈ 116/sec (avg)
Peak: 1,000/sec (event spikes)

Breakdown:
  Push: 60% → 6M/day
  Email: 30% → 3M/day
  SMS: 10% → 1M/day

Storage (notification log):
  1 record ≈ 500 bytes
  10M/day × 500B = 5 GB/day
  Retention: 90 days → 450 GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3️⃣ API Design
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Send&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;notification&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(internal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;call)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/notifications/send&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Body:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"like_received"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"channels"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"push"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;leave&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;empty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;→&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;preferences&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"actorName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Rahul"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"postTitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"My Design Notes"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;User&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;preference&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;management&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;GET&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;/users/:userId/notification-preferences&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;PUT&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;/users/:userId/notification-preferences&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Body:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"push"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"like_received"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"new_follower"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"marketing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"weekly_digest"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"marketing"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Scheduled&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;notifications&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;POST&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/notifications/schedule&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Body:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"templateId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"weekly_digest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scheduledAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-01-15T09:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"userSegment"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"active_users"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  4️⃣ High-Level Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Event Sources]
  - User Service ("user A liked post")
  - Order Service ("your order shipped")
  - Scheduler ("send weekly digest")
        │
        ▼
[Notification Service API]
        │
        ├─► Check User Preferences → skip if opted out
        ├─► Build notification from template
        ├─► Route to appropriate channels
        │
        ▼
[Message Queue (Kafka / SQS)]
  ├──► [Push Queue]
  ├──► [Email Queue]
  └──► [SMS Queue]
        │
        ▼
[Channel Workers]
  ├──► [Push Worker] → FCM (Android) / APNs (iOS)
  ├──► [Email Worker] → SendGrid / SES / Mailgun
  └──► [SMS Worker] → Twilio / SNS
        │
        ▼
[Delivery Log DB] ← track status per notification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5️⃣ Component Deep Dives
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Preference Service
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# User preferences (stored in DB, cached in Redis)
&lt;/span&gt;&lt;span class="n"&gt;preferences&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;push&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;like_received&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new_follower&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marketing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;weekly_digest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;security_alert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marketing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;security_alert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marketing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;should_notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notifType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;prefs&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prefs:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;prefs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notifType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# default: on
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Template Service
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Template:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"like_received"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"templateId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"like_received"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"push"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{actorName}} liked your post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;{{postTitle}}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; is getting attention!"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{{actorName}} liked your post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"templateFile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"like-received.html"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Render: inject actual values, produce final notification content.&lt;/p&gt;

&lt;h3&gt;
  
  
  Push Notification Worker
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;device_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_device_tokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;device_tokens&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;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;android&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;fcm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ios&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;apns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deviceToken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aps&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;notification&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Retry &amp;amp; Dead Letter
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Worker fails to send → retry with exponential backoff
  Attempt 1: immediate
  Attempt 2: 30 seconds
  Attempt 3: 5 minutes
  Attempt 4: 30 minutes
  Attempt 5: DLQ (manual inspection or discard)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6️⃣ Database Schema
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Notification log (Cassandra — write-heavy, time-series)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;notifications&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;notification_id&lt;/span&gt;  &lt;span class="n"&gt;UUID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;          &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;type&lt;/span&gt;             &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;channel&lt;/span&gt;          &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;-- push/email/sms&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;           &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;-- pending/sent/failed/delivered/opened&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt;          &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;       &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;sent_at&lt;/span&gt;          &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;notification_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;CLUSTERING&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- User device tokens&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;device_tokens&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;    &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;token&lt;/span&gt;      &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;platform&lt;/span&gt;   &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;-- ios/android/web&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- User preferences (PostgreSQL, read-heavy)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;notification_preferences&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;     &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;preferences&lt;/span&gt; &lt;span class="n"&gt;JSONB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt;  &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7️⃣ Handling Scale Spikes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Event: Product Launch → 10M users notified
&lt;/h3&gt;

&lt;p&gt;Problem: Sudden flood of 10M notifications in seconds.&lt;/p&gt;

&lt;p&gt;Solution: &lt;strong&gt;Rate limiting the outbound queue&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Email: SendGrid allows 1000 emails/sec
→ Queue backs up → workers drain at 1000/sec → all 10M sent in ~3 hours
→ Users get it "within a few hours" → acceptable for marketing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Priority Queues
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;HIGH priority queue:   security alerts, OTPs → process immediately
MEDIUM priority queue: social notifications → within seconds
LOW priority queue:    marketing, digests → within hours
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Workers check HIGH first, then MEDIUM, then LOW.&lt;/p&gt;




&lt;h2&gt;
  
  
  8️⃣ Observability
&lt;/h2&gt;

&lt;p&gt;Track every notification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Events to log:
  - notification_queued     (timestamp, userId, type, channel)
  - notification_sent       (timestamp, provider response)
  - notification_delivered  (timestamp, from FCM/APNs delivery receipt)
  - notification_opened     (timestamp, from SDK tracking)
  - notification_failed     (timestamp, error, retryCount)

Metrics:
  - Delivery rate per channel
  - Average send latency (queued → sent)
  - Failure rate per provider
  - Open rate per notification type
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🎨 Diagram
&lt;/h2&gt;

&lt;p&gt;The diagram shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Event sources → Notification Service → Kafka queues → channel workers → external providers&lt;/li&gt;
&lt;li&gt;Preference check before queuing&lt;/li&gt;
&lt;li&gt;Template rendering step&lt;/li&gt;
&lt;li&gt;Retry with DLQ&lt;/li&gt;
&lt;li&gt;Priority queue lanes (high/medium/low)&lt;/li&gt;
&lt;li&gt;Delivery status tracking&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ✅ Trade-offs Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Rationale&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Queue type&lt;/td&gt;
&lt;td&gt;Kafka/SQS&lt;/td&gt;
&lt;td&gt;Decouple events from delivery, absorb spikes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Priority&lt;/td&gt;
&lt;td&gt;Multiple queues&lt;/td&gt;
&lt;td&gt;Don't let marketing delay OTPs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Retry&lt;/td&gt;
&lt;td&gt;Exponential backoff&lt;/td&gt;
&lt;td&gt;Avoid hammering failed providers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Preference storage&lt;/td&gt;
&lt;td&gt;Redis cache + DB&lt;/td&gt;
&lt;td&gt;Fast reads on every notification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delivery tracking&lt;/td&gt;
&lt;td&gt;Cassandra&lt;/td&gt;
&lt;td&gt;High-volume, time-series write pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>systemdesign</category>
      <category>webdev</category>
      <category>programming</category>
      <category>backend</category>
    </item>
    <item>
      <title>HLD: URL Shortener (like bit.ly)</title>
      <dc:creator>Gouranga Das Samrat</dc:creator>
      <pubDate>Sat, 22 Aug 2026 02:00:00 +0000</pubDate>
      <link>https://dev.to/gouranga-das-khulna/hld-url-shortener-like-bitly-3696</link>
      <guid>https://dev.to/gouranga-das-khulna/hld-url-shortener-like-bitly-3696</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Design a system that takes a long URL and returns a short URL, and redirects short URLs to their originals.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1️⃣ Clarify Requirements
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Functional Requirements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Given a long URL, generate a short URL&lt;/li&gt;
&lt;li&gt;Given a short URL, redirect to the original long URL&lt;/li&gt;
&lt;li&gt;(Optional) Custom aliases: &lt;code&gt;short.ly/my-brand&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;(Optional) URL expiration&lt;/li&gt;
&lt;li&gt;(Optional) Analytics: click count, referrer, location&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Non-Functional Requirements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High availability&lt;/strong&gt; — downtime = broken links everywhere&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low latency redirects&lt;/strong&gt; — &amp;lt; 10ms (cached)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Durability&lt;/strong&gt; — shortened URLs should work for years&lt;/li&gt;
&lt;li&gt;Reads &amp;gt;&amp;gt; Writes (100:1 read-to-write ratio typical)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  2️⃣ Estimate Scale
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Write QPS:
  10M URLs shortened per day
  = 10M / 86,400 ≈ 115 writes/sec

Read QPS (100:1 ratio):
  100 × 115 = 11,500 reads/sec
  Peak: ~50,000 reads/sec

Storage:
  Each record: shortURL(7B) + longURL(200B) + metadata(100B) ≈ 307 bytes
  10M URLs/day × 10 years = 36.5B URLs
  36.5B × 307 bytes ≈ 11 TB (very manageable)

Cache:
  80% reads on 20% of URLs (Pareto principle)
  Cache 20% of daily URLs: 10M × 0.2 × 307B ≈ 600 MB/day
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3️⃣ API Design
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;# Create short URL
POST /api/shorten
Body: { "longUrl": "https://...", "customAlias": "optional", "expiresAt": "optional" }
Response: { "shortUrl": "https://short.ly/abc1234" }

# Redirect
GET /:shortCode
Response: 301/302 Redirect to longUrl

# Analytics (optional)
GET /api/stats/:shortCode
Response: { clicks: 1000, topCountries: [...] }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;301 vs 302:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;301 Permanent&lt;/code&gt; → browser caches redirect → fewer server hits → can't update&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;302 Temporary&lt;/code&gt; → browser always checks server → can update/expire → use this for analytics&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4️⃣ Short Code Generation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option A: Hash + Truncate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MD5(longURL) = "1a79a4d60de6718e8e5b326e338ae533"
Take first 7 chars: "1a79a4d"
shortURL: short.ly/1a79a4d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Problem: Collisions — different URLs can produce same 7-char prefix.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option B: Base62 Encoding (Recommended)
&lt;/h3&gt;

&lt;p&gt;Character set: &lt;code&gt;[0-9a-zA-Z]&lt;/code&gt; = 62 characters&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;7 characters × 62^7 = 3.5 trillion unique codes ✅

ID: 123456789
Base62(123456789) = "8M0kX"
shortURL: short.ly/8M0kX
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where does the ID come from?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auto-increment DB ID&lt;/strong&gt; — simple, but reveals volume/sequence&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;UUID&lt;/strong&gt; — random, no collision, but long → truncate carefully&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake ID&lt;/strong&gt; — distributed, time-sortable, unique (Twitter's approach)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Option C: Counter Service
&lt;/h3&gt;

&lt;p&gt;A dedicated service generates unique IDs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Counter Service: ID = 1, 2, 3, ... (globally unique)
URL Service: Base62(ID) → short code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  5️⃣ High-Level Design
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                                    [Cache (Redis)]
                                          │
Client ──► [API Gateway + LB] ──► [URL Service] ──► [DB (PostgreSQL)]
                                          │
                                   [Analytics Queue]
                                          │
                                   [Analytics DB]

Redirect flow:
Client ──GET /abc1234──► [URL Service]
                              │
                    ┌─── Redis HIT → 302 Redirect ──► Client
                    │
                    └─── Redis MISS → DB query → cache → 302 Redirect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  6️⃣ Database Schema
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;           &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;short_code&lt;/span&gt;   &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;long_url&lt;/span&gt;     &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;      &lt;span class="nb"&gt;BIGINT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;   &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;expires_at&lt;/span&gt;   &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;click_count&lt;/span&gt;  &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_short_code&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;urls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;short_code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Analytics table&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;clicks&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;          &lt;span class="n"&gt;BIGSERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;short_code&lt;/span&gt;  &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;clicked_at&lt;/span&gt;  &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;country&lt;/span&gt;     &lt;span class="nb"&gt;VARCHAR&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;referrer&lt;/span&gt;    &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_agent&lt;/span&gt;  &lt;span class="nb"&gt;TEXT&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  7️⃣ Caching Strategy
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Key:   short_code ("abc1234")
Value: long_url
TTL:   24 hours (refresh on hit if needed)

Cache-aside:
1. Check Redis for short_code
2. MISS → query PostgreSQL → store in Redis with TTL
3. HIT → return long_url immediately

Cache hit rate target: &amp;gt;95% (80/20 rule — most traffic on top URLs)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  8️⃣ Scalability Deep Dive
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Read Scaling
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Redis cache handles ~95% of redirect traffic&lt;/li&gt;
&lt;li&gt;Read replicas for the remaining DB reads&lt;/li&gt;
&lt;li&gt;CDN for the redirect response itself (if 301)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Write Scaling
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Writes (new URLs) are much rarer — primary DB + async replication is fine&lt;/li&gt;
&lt;li&gt;If needed: hash short_code → route to shard&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  High Availability
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Redis Sentinel or Cluster for cache HA&lt;/li&gt;
&lt;li&gt;DB: Primary + read replicas + automated failover&lt;/li&gt;
&lt;li&gt;Multiple URL service instances behind LB&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  9️⃣ Custom Aliases &amp;amp; Expiry
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;shorten&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;long_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;custom_alias&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&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;custom_alias&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;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;custom_alias&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ConflictError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Alias taken&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;short_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;custom_alias&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;long_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;short_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;base62_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;short_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;long_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://short.ly/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;short_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;short_code&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;url_data&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="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;short_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;short_code&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;url_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;NotFoundError&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;url_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;url_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;GoneError&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# 410 Gone
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;url_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;long_url&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🎨 Diagram
&lt;/h2&gt;

&lt;p&gt;The diagram shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full system: Client → LB → URL Service → Redis → PostgreSQL&lt;/li&gt;
&lt;li&gt;Shorten flow vs redirect flow (separate paths)&lt;/li&gt;
&lt;li&gt;Cache hit vs miss paths&lt;/li&gt;
&lt;li&gt;Analytics async flow via queue&lt;/li&gt;
&lt;li&gt;DB schema with indexes highlighted&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ✅ Trade-offs Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decision&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;301 vs 302&lt;/td&gt;
&lt;td&gt;302&lt;/td&gt;
&lt;td&gt;More server hits, but analytics &amp;amp; expiry work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ID generation&lt;/td&gt;
&lt;td&gt;DB auto-increment + Base62&lt;/td&gt;
&lt;td&gt;Simple but sequential — use Snowflake for privacy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache TTL&lt;/td&gt;
&lt;td&gt;24hr&lt;/td&gt;
&lt;td&gt;Stale entries for expired/deleted URLs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL vs NoSQL&lt;/td&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;Simple queries, ACID for uniqueness&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>systemdesign</category>
      <category>webdev</category>
      <category>programming</category>
      <category>backend</category>
    </item>
    <item>
      <title>From a 4-Year-Old Feature Request to `pkg install bun`: My Second Merge on the Termux User Repository</title>
      <dc:creator>Gouranga Das Samrat</dc:creator>
      <pubDate>Fri, 21 Aug 2026 09:35:47 +0000</pubDate>
      <link>https://dev.to/gouranga-das-khulna/from-a-4-year-old-feature-request-to-pkg-install-bun-my-second-merge-on-the-termux-user-567</link>
      <guid>https://dev.to/gouranga-das-khulna/from-a-4-year-old-feature-request-to-pkg-install-bun-my-second-merge-on-the-termux-user-567</guid>
      <description>&lt;p&gt;Last post i wrote was about &lt;a href="https://dev.to/gouranga-das-khulna/rejected-on-main-accepted-on-tur-how-8-nerd-fonts-became-my-first-merge-on-the-termux-user-4e88"&gt;getting turned away from the main repo over a policy disagreement and landing my first TUR merge instead&lt;/a&gt; with 8 Nerd Fonts packages.&lt;/p&gt;

&lt;p&gt;This is the same shape of story again, except the "no" from the main repo wasn't about repo size this time — it was about something much harder to argue around: how Termux insists packages get built.&lt;/p&gt;

&lt;h2&gt;
  
  
  An issue older than some of the tools people used to work around it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/termux/termux-packages/issues/11188" rel="noopener noreferrer"&gt;Issue #11188&lt;/a&gt; asking for &lt;code&gt;bun&lt;/code&gt; in Termux goes back to July 2022, opened by &lt;a href="https://github.com/leap0x7b" rel="noopener noreferrer"&gt;@leap0x7b&lt;/a&gt;. Reading the whole thread back to front is basically a history of people improvising around a missing package for four straight years:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Early on, &lt;code&gt;bun&lt;/code&gt;'s Linux aarch64 build just didn't run on Android — &lt;code&gt;npm install -g bun&lt;/code&gt; failed outright with an &lt;code&gt;EBADPLATFORM&lt;/code&gt; error, because upstream hadn't shipped an Android target.&lt;/li&gt;
&lt;li&gt;The workaround that kept surfacing was &lt;code&gt;grun&lt;/code&gt;, a glibc compatibility shim, letting people run the Linux binary against a glibc environment layered on top of Termux's normal Bionic libc. It mostly worked, with recurring reports of install failures (&lt;code&gt;AccessDenied&lt;/code&gt; errors from &lt;code&gt;bun install&lt;/code&gt; trying to hardlink files) and one-off fixes like &lt;code&gt;bun install --backend=copyfile&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A few people tried building &lt;code&gt;bun&lt;/code&gt; from source directly. That hit its own wall: Bun's own docs state that Bun itself must be installed to compile Bun — you need an existing &lt;code&gt;bun&lt;/code&gt; binary just to build a new one — plus a strict Zig version dependency that broke against whatever Termux had packaged at the time.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/TomJo2000" rel="noopener noreferrer"&gt;@TomJo2000&lt;/a&gt; summed up the stall bluntly in mid-2025: nobody had written a build script, no other distro built it from source either, and it depended on a kernel version most Android devices didn't meet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The turn came in April 2026, when &lt;a href="https://github.com/licy183" rel="noopener noreferrer"&gt;@licy183&lt;/a&gt; flagged that upstream had finally &lt;a href="https://github.com/oven-sh/bun/commit/2ee9cad0ea26e051acb181bb3740e292757fdcf5" rel="noopener noreferrer"&gt;added an Android build target&lt;/a&gt;. &lt;a href="https://github.com/TomJo2000" rel="noopener noreferrer"&gt;@TomJo2000&lt;/a&gt; noted the obvious next step — someone still had to actually write the build script. By mid-May, &lt;a href="https://github.com/Jobians" rel="noopener noreferrer"&gt;@Jobians&lt;/a&gt; confirmed Bun was running natively using the official Android ARM64 build. The pieces were finally there. I decided to be the one to write the script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round one: prebuilt binaries, meet main-repo policy
&lt;/h2&gt;

&lt;p&gt;I opened &lt;a href="https://github.com/termux/termux-packages/pull/31115" rel="noopener noreferrer"&gt;&lt;code&gt;termux-packages&lt;/code&gt; PR #31115&lt;/a&gt; — &lt;code&gt;addpkg(main/bun): 1.3.14&lt;/code&gt; — built around the official prebuilt Android binaries that shipped in &lt;a href="https://bun.com/blog/bun-v1.3.14#freebsd-and-android-support" rel="noopener noreferrer"&gt;Bun v1.3.14&lt;/a&gt;. The binaries are Position-Independent Executables linked straight against standard Bionic libraries (&lt;code&gt;libc.so&lt;/code&gt;, &lt;code&gt;libm.so&lt;/code&gt;, &lt;code&gt;libdl.so&lt;/code&gt;), so no glibc shim needed at all — a real fix for the &lt;code&gt;grun&lt;/code&gt; workaround people had been leaning on for years. The only real limitation was architecture: upstream only builds Android targets for &lt;code&gt;aarch64&lt;/code&gt; and &lt;code&gt;x86_64&lt;/code&gt;, so I set those two as excluded-safe and expected &lt;code&gt;arm&lt;/code&gt;/&lt;code&gt;i686&lt;/code&gt; to just skip cleanly in CI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/robertkirkman" rel="noopener noreferrer"&gt;@robertkirkman&lt;/a&gt; stopped that approach at the door: &lt;code&gt;termux-packages&lt;/code&gt; requires compiling from source, so downloading upstream's prebuilt binaries wasn't going to fly. He floated a specific alternative — if Bun were published as an installable crate on crates.io, &lt;code&gt;cargo-binstall&lt;/code&gt; could fetch upstream binaries in a way the main repo's tooling already sanctions. Worth checking, but it didn't pan out: Bun isn't a simple Rust crate at all. It's a monorepo built through its own CMake pipeline, linking against JavaScriptCore/WebKit and LLVM — nothing &lt;code&gt;cargo build --release&lt;/code&gt; or &lt;code&gt;cargo-binstall&lt;/code&gt; can touch.&lt;/p&gt;

&lt;p&gt;There was a fun tangent buried in the same exchange — &lt;a href="https://github.com/robertkirkman" rel="noopener noreferrer"&gt;@robertkirkman&lt;/a&gt; noting Node.js is already built from source in Termux and reasoning Bun should in theory be possible too, then wondering out loud whether Bun is meaningfully faster than both Node and Deno (Deno also being a from-source build in Termux) — the kind of "is this actually a different tool or just a faster clone" curiosity that comes up naturally whenever a runtime like this gets proposed.&lt;/p&gt;

&lt;p&gt;I went and actually tried the from-source route properly, then came back and closed the PR myself on August 20 with a detailed writeup of exactly where it broke down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bun's build system does have official Android cross-compile support (&lt;code&gt;--abi=android&lt;/code&gt;, &lt;code&gt;--android-ndk=&amp;lt;path&amp;gt;&lt;/code&gt;), confirmed against the pattern in its &lt;code&gt;.buildkite/Dockerfile&lt;/code&gt; (NDK r27c, host-clang + sysroot).&lt;/li&gt;
&lt;li&gt;WebKit doesn't need a from-source build for Android — a prebuilt tarball gets fetched automatically, so that wasn't the blocker I expected.&lt;/li&gt;
&lt;li&gt;The actual wall was a statically, cross-compiled ICU for Android (&lt;code&gt;$BUN_ANDROID_ICU_ROOT&lt;/code&gt;) with no public build recipe I could find anywhere — not in &lt;code&gt;oven-sh/bun&lt;/code&gt;, not in &lt;code&gt;oven-sh/bun-development-docker-image&lt;/code&gt;, not in the actual &lt;code&gt;.buildkite/Dockerfile&lt;/code&gt;, despite a comment in &lt;code&gt;webkit.ts&lt;/code&gt; pointing at a "Dockerfile.android" that doesn't seem to exist publicly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add a strictly pinned LLVM 21.1.8 and a pinned Rust nightly on top of that, and it was more toolchain archaeology than I could commit to finishing in one PR. So I closed it, thanked &lt;a href="https://github.com/robertkirkman" rel="noopener noreferrer"&gt;@robertkirkman&lt;/a&gt; for the guidance, and left the door open to revisit if upstream's Android/ICU story ever gets easier to build from source.&lt;/p&gt;

&lt;h2&gt;
  
  
  The TUR side already had its own long-running Bun request
&lt;/h2&gt;

&lt;p&gt;There was already a parallel thread for this on TUR: &lt;a href="https://github.com/termux-user-repository/tur/issues/571" rel="noopener noreferrer"&gt;&lt;code&gt;tur&lt;/code&gt; issue #571&lt;/a&gt;, opened back in 2023 by &lt;a href="https://github.com/earningpoints" rel="noopener noreferrer"&gt;@earningpoints&lt;/a&gt;. &lt;a href="https://github.com/licy183" rel="noopener noreferrer"&gt;@licy183&lt;/a&gt; had kept it open rather than closing it outright, citing two blockers at the time — Bun's kernel 5.1 requirement, which most Android devices didn't meet, and the fact that it didn't compile against non-glibc libcs like musl. &lt;a href="https://github.com/jothi-prasath" rel="noopener noreferrer"&gt;@jothi-prasath&lt;/a&gt; tried getting it running via &lt;code&gt;grun&lt;/code&gt; and shared a working build script, but &lt;a href="https://github.com/licy183" rel="noopener noreferrer"&gt;@licy183&lt;/a&gt; was clear about where TUR draws its own line: binaries in TUR packages need to link against Bionic libc, not GNU libc, with no plan to support a custom-loader GNU libc setup.&lt;/p&gt;

&lt;p&gt;That distinction turned out to matter a lot for round two — because the whole reason the official Bun v1.3.14 Android binaries worked for Termux at all is that they're linked directly against Bionic. No &lt;code&gt;grun&lt;/code&gt;, no glibc shim, no custom loader path. They fit TUR's actual requirement perfectly; they just didn't fit the main repo's from-source rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round two: TUR PR #2746
&lt;/h2&gt;

&lt;p&gt;I opened &lt;a href="https://github.com/termux-user-repository/tur/pull/2746" rel="noopener noreferrer"&gt;&lt;code&gt;tur&lt;/code&gt; PR #2746&lt;/a&gt; — same package, same version, same prebuilt-binary approach — explicitly closing both threads at once: &lt;code&gt;Fixes termux/termux-packages#11188 #571&lt;/code&gt;, with a note that it was migrated from &lt;a href="https://github.com/termux/termux-packages/pull/31115" rel="noopener noreferrer"&gt;PR #31115&lt;/a&gt; specifically because of the main repo's build-from-source policy. No repeat of the source-vs-prebuilt argument — TUR's policy is built for exactly this kind of package.&lt;/p&gt;

&lt;p&gt;Review was quick and purely technical. &lt;a href="https://github.com/licy183" rel="noopener noreferrer"&gt;@licy183&lt;/a&gt; caught an inefficiency in &lt;code&gt;build.sh&lt;/code&gt;: rather than my approach, the more common pattern is to download binaries for both &lt;code&gt;aarch64&lt;/code&gt; and &lt;code&gt;x86_64&lt;/code&gt; up front and only install the one matching the build architecture — pointing me at how &lt;a href="https://github.com/termux/termux-packages/blob/220210c49cd71d6ace137861136ca7a2d0c1a035/packages/pypy3/build.sh#L8" rel="noopener noreferrer"&gt;&lt;code&gt;pypy3&lt;/code&gt;'s build script&lt;/a&gt; already does it. I made the change same day. A second review comment flagged that my binary-path handling was split oddly across build steps — it only needed to run in &lt;code&gt;termux_step_make_install&lt;/code&gt;, while &lt;code&gt;termux_step_post_get_source&lt;/code&gt; should just be fetching the &lt;code&gt;LICENSE&lt;/code&gt; file. I moved the logic accordingly.&lt;/p&gt;

&lt;p&gt;Then: a simple &lt;em&gt;"Thanks!"&lt;/em&gt; from &lt;a href="https://github.com/licy183" rel="noopener noreferrer"&gt;@licy183&lt;/a&gt;, and that was that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed, twice over
&lt;/h2&gt;

&lt;p&gt;The version string says &lt;code&gt;1.3.14&lt;/code&gt;, same as the PR I closed on the main repo, but what actually shipped is bigger than the number:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A four-year-old feature request that survived &lt;code&gt;grun&lt;/code&gt; hacks, glibc shims, Zig version breakage, and a genuine "you need Bun to build Bun" chicken-and-egg problem, finally resolved with &lt;code&gt;pkg install bun&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A real technical answer, on the record, for why &lt;code&gt;termux-packages&lt;/code&gt; couldn't take it as-is: a from-source Android build is blocked on a static cross-compiled ICU with no public recipe — useful for whoever eventually revisits it.&lt;/li&gt;
&lt;li&gt;Confirmation from TUR's side of exactly which binaries are acceptable there: Bionic-linked, no glibc shim required — which is exactly what upstream Bun now ships.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two PRs in a row now where the front door said no for a real, defensible reason, and TUR turned out to be exactly the right back door. Different reasons each time — repo-size policy for the fonts, source-build policy for Bun — but the same shape of resolution. I'm starting to think that's just what TUR is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get it
&lt;/h2&gt;

&lt;p&gt;If you're on Termux and TUR isn't already added as a repo, three commands and &lt;code&gt;bun&lt;/code&gt; is installed with no &lt;code&gt;grun&lt;/code&gt;, no glibc shim, none of the workarounds from that four-year thread:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pkg &lt;span class="nb"&gt;install &lt;/span&gt;tur-repo
pkg update
pkg &lt;span class="nb"&gt;install &lt;/span&gt;bun
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Proof it's live:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~ &lt;span class="nv"&gt;$ &lt;/span&gt;pkg show bun
Package: bun
Version: 1.3.14-1
Maintainer: Gouranga Das Samrat &amp;lt;gouranga.das.khulna@gmail.com&amp;gt;
Installed-Size: 89.8 MB
Homepage: https://bun.com
Download-Size: 22.8 MB
APT-Sources: https://tur.kcubeterm.com tur-packages/tur aarch64 Packages
Description: Incredibly fast JavaScript runtime, bundler, &lt;span class="nb"&gt;test &lt;/span&gt;runner, and package manager
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The full trail:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/termux/termux-packages/issues/11188" rel="noopener noreferrer"&gt;termux-packages #11188 — the 4-year-old feature request&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/termux/termux-packages/pull/31115" rel="noopener noreferrer"&gt;termux-packages #31115 — my prebuilt-binary PR, closed on policy grounds&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/termux-user-repository/tur/issues/571" rel="noopener noreferrer"&gt;tur #571 — the parallel TUR request, open since 2023&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/termux-user-repository/tur/pull/2746" rel="noopener noreferrer"&gt;tur #2746 — the PR that merged&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bun.com/blog/bun-v1.3.14#freebsd-and-android-support" rel="noopener noreferrer"&gt;Bun v1.3.14 release notes — the Android support that made this possible&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/gouranga-das-khulna/rejected-on-main-accepted-on-tur-how-8-nerd-fonts-became-my-first-merge-on-the-termux-user-4e88"&gt;My first TUR merge, on 8 Nerd Fonts packages&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>termux</category>
      <category>opensource</category>
      <category>rust</category>
      <category>android</category>
    </item>
    <item>
      <title>Rejected on Main, Accepted on TUR: How 8 Nerd Fonts Became My First Merge on the Termux User Repository</title>
      <dc:creator>Gouranga Das Samrat</dc:creator>
      <pubDate>Thu, 20 Aug 2026 16:57:28 +0000</pubDate>
      <link>https://dev.to/gouranga-das-khulna/rejected-on-main-accepted-on-tur-how-8-nerd-fonts-became-my-first-merge-on-the-termux-user-4e88</link>
      <guid>https://dev.to/gouranga-das-khulna/rejected-on-main-accepted-on-tur-how-8-nerd-fonts-became-my-first-merge-on-the-termux-user-4e88</guid>
      <description>&lt;p&gt;I wrote before about the day &lt;a href="https://dev.to/gouranga-das-khulna/from-a-high-school-termux-user-to-a-package-maintainer-the-story-behind-my-first-merged-pr-on-kgc"&gt;&lt;code&gt;proton-pass-cli&lt;/code&gt; merged into &lt;code&gt;termux-packages&lt;/code&gt;&lt;/a&gt; — my first merge into the main repo, full stop. This post is about what happened right after: I tried to do it again with nine fonts, got talked out of it on the main repo, and ended up with my first merge on &lt;a href="https://github.com/termux-user-repository" rel="noopener noreferrer"&gt;TUR&lt;/a&gt; instead — 8 packages, all in one shot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round one: nine fonts, one PR, the main repo
&lt;/h2&gt;

&lt;p&gt;I opened &lt;a href="https://github.com/termux/termux-packages/pull/31157" rel="noopener noreferrer"&gt;&lt;code&gt;termux-packages&lt;/code&gt; PR #31157&lt;/a&gt; — &lt;code&gt;addpkg(nerd-fonts): add 9 Nerd Fonts packages (v3.5.0)&lt;/code&gt;. The plan: pull nine fonts from the &lt;a href="https://github.com/ryanoasis/nerd-fonts" rel="noopener noreferrer"&gt;v3.5.0 release of ryanoasis/nerd-fonts&lt;/a&gt;, package each as its own opt-in &lt;code&gt;.deb&lt;/code&gt; following the same pattern as the existing &lt;code&gt;ttf-jetbrains-mono&lt;/code&gt;, and ship only the plain Regular/Bold/Italic/BoldItalic styles to keep things lean.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/robertkirkman" rel="noopener noreferrer"&gt;@robertkirkman&lt;/a&gt; was first to comment, and it wasn't about the packages themselves — it was naming. Termux already had &lt;code&gt;ttf-nerd-fonts-symbols&lt;/code&gt;, which mirrors &lt;a href="https://archlinux.org/groups/any/nerd-fonts/" rel="noopener noreferrer"&gt;Arch Linux's naming convention&lt;/a&gt;, so he asked me to rename all nine of mine to match it too. Fair catch — I'd shipped them as &lt;code&gt;ttf-&amp;lt;font&amp;gt;-nerd-font&lt;/code&gt;, and I renamed everything to &lt;code&gt;ttf-&amp;lt;font&amp;gt;-nerd&lt;/code&gt; the same day.&lt;/p&gt;

&lt;p&gt;That turned out to be the easy part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round one, continued: the argument that actually mattered
&lt;/h2&gt;

&lt;p&gt;Somewhere in review, &lt;a href="https://github.com/TomJo2000" rel="noopener noreferrer"&gt;@TomJo2000&lt;/a&gt; raised a concern about repo size — enough that my next comment opened with thanking him for it directly, even though he hadn't posted publicly in the thread yet. I made the case for keeping all nine anyway: Homebrew and Arch both ship every Nerd Font as its own opt-in package, nothing else in Termux would depend on mine so there's zero cost to anyone who skips them, and &lt;code&gt;ttf-jetbrains-mono&lt;/code&gt; already set the precedent. I even offered to trim styles further if size was the real issue.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/robertkirkman" rel="noopener noreferrer"&gt;@robertkirkman&lt;/a&gt; didn't disagree with the utility — he laid out why Termux draws the line differently than Homebrew or Arch in the first place. Termux sits at a little over 3,000 packages; those other ecosystems are past 10,000. The project's focus, as he put it, is packages that are otherwise inconvenient to get running on Android — not "niche, static cosmetic assets that never require patching to work directly copied and pasted into Termux, like wallpapers, fonts, themes and icon packs." He did concede the fonts had a genuinely high &lt;a href="https://repology.org/project/fonts%3Anerd-fonts/versions" rel="noopener noreferrer"&gt;Repology score&lt;/a&gt;, and offered a condition: add them once at least one other person actually asks.&lt;/p&gt;

&lt;p&gt;I pushed back on that condition specifically, because it's a trap by design — once a PR like mine is sitting open and visible, anyone who wants the package just watches the PR instead of filing a separate request, so the bar never gets cleared. I tried a subset compromise (trim the heaviest fonts, keep the rest), pointed to &lt;a href="https://github.com/termux/termux-packages/pull/29412" rel="noopener noreferrer"&gt;PR #29412 (OBS Studio)&lt;/a&gt; as a case where real demand never shows up as comments on an open PR, and floated moving everything to TUR if it stayed stuck.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/robertkirkman" rel="noopener noreferrer"&gt;@robertkirkman&lt;/a&gt; held the line on the condition, and also corrected my OBS comparison — that PR wasn't stalled from lack of interest, it was blocked on an unresolved Android 7 compatibility bug being discussed mostly on Discord, not GitHub. &lt;a href="https://github.com/TomJo2000" rel="noopener noreferrer"&gt;@TomJo2000&lt;/a&gt; weighed in too, rejecting the "ship a subset" middle ground outright and pointing out that &lt;code&gt;termux-styling&lt;/code&gt; has its own &lt;a href="https://github.com/termux/termux-styling/blob/v0.32.1/setup-nerd-fonts.sh" rel="noopener noreferrer"&gt;version of this exact problem&lt;/a&gt; already.&lt;/p&gt;

&lt;p&gt;I tried one more angle — comparing font packages to &lt;code&gt;-static&lt;/code&gt; libraries that already sit in the main repo with zero reverse dependencies — which pulled the thread into a genuinely interesting tangent about why Termux ships &lt;code&gt;-static&lt;/code&gt; packages at all. &lt;a href="https://github.com/robertkirkman" rel="noopener noreferrer"&gt;@robertkirkman&lt;/a&gt; pointed to &lt;code&gt;portaudio-static&lt;/code&gt; easing a &lt;a href="https://github.com/ComposersDesktop/CDP8/issues/17#issuecomment-3240791623" rel="noopener noreferrer"&gt;real packaging headache for CDP8&lt;/a&gt; and to &lt;a href="https://github.com/termux/termux-packages/pull/29787" rel="noopener noreferrer"&gt;PR #29787&lt;/a&gt; as an example of the maintenance cost involved; &lt;a href="https://github.com/TomJo2000" rel="noopener noreferrer"&gt;@TomJo2000&lt;/a&gt; admitted he wasn't even sure why they're split out in the first place, guessing it predates his time on the project. Somewhere in there I also brought up that &lt;code&gt;sqlcipher&lt;/code&gt; itself had zero reverse dependencies in the main repo right up until I shipped &lt;a href="https://github.com/termux/termux-packages/pull/30987" rel="noopener noreferrer"&gt;&lt;code&gt;proton-pass-cli&lt;/code&gt; #30987&lt;/a&gt; — which &lt;a href="https://github.com/robertkirkman" rel="noopener noreferrer"&gt;@robertkirkman&lt;/a&gt; settled with a technical clarification: the no-reverse-dependency rule mainly targets packages that only drop files into &lt;code&gt;lib&lt;/code&gt;, &lt;code&gt;libexec&lt;/code&gt;, &lt;code&gt;share&lt;/code&gt;, or &lt;code&gt;opt&lt;/code&gt;. &lt;code&gt;sqlcipher&lt;/code&gt; always shipped an executable in &lt;code&gt;bin&lt;/code&gt; too, so it was never actually in violation.&lt;/p&gt;

&lt;p&gt;None of that moved the font policy itself. So I closed the PR myself and did what I'd already floated: took it to TUR.&lt;/p&gt;

&lt;h2&gt;
  
  
  Round two: TUR PR #2743
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/termux-user-repository" rel="noopener noreferrer"&gt;TUR&lt;/a&gt; exists precisely for software that's useful and wanted but doesn't fit the main repo's policy — so I opened &lt;a href="https://github.com/termux-user-repository/tur/pull/2743" rel="noopener noreferrer"&gt;&lt;code&gt;tur&lt;/code&gt; PR #2743&lt;/a&gt;, explicitly noting in the description that it was migrated from termux/termux-packages#31157 "to respect main repo's policies." Iosevka got dropped along the way, so this round shipped eight fonts, not nine — though the PR title, copy-pasted from the original, still says "9 Nerd Fonts packages" even with a table listing exactly eight underneath it. Nobody caught it, and honestly, neither did I until just now.&lt;/p&gt;

&lt;p&gt;This time there was no policy fight — just infrastructure. &lt;a href="https://github.com/robertkirkman" rel="noopener noreferrer"&gt;@robertkirkman&lt;/a&gt; flagged that a &lt;a href="https://github.com/termux-user-repository/dists/issues/9" rel="noopener noreferrer"&gt;GitHub race condition&lt;/a&gt; was currently blocking the creation of new package repos in TUR, so merging would have to wait. A couple hours later, &lt;a href="https://github.com/licy183" rel="noopener noreferrer"&gt;@licy183&lt;/a&gt; stepped in, offering to manually set up a &lt;code&gt;nerd-fonts&lt;/code&gt; repository in &lt;code&gt;tur-dists&lt;/code&gt; in the meantime, with a longer-term fix — retrying the upload script up to five times with a one-second sleep between attempts — planned for the following weekend.&lt;/p&gt;

&lt;p&gt;It worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proof it's real
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~ &lt;span class="nv"&gt;$ &lt;/span&gt;pkg search &lt;span class="s1"&gt;'^ttf-.*nerd'&lt;/span&gt;
ttf-cascadia-code-nerd/tur-packages 3.5.0 all
  Cascadia Code patched with Nerd Fonts icons/glyphs &lt;span class="o"&gt;(&lt;/span&gt;Font Awesome, Devicons, Octicons, Powerline, etc&lt;span class="o"&gt;)&lt;/span&gt;

ttf-hack-nerd/tur-packages 3.5.0 all
  Hack patched with Nerd Fonts icons/glyphs &lt;span class="o"&gt;(&lt;/span&gt;Font Awesome, Devicons, Octicons, Powerline, etc&lt;span class="o"&gt;)&lt;/span&gt;

ttf-inconsolata-nerd/tur-packages 3.5.0 all
  Inconsolata patched with Nerd Fonts icons/glyphs &lt;span class="o"&gt;(&lt;/span&gt;Font Awesome, Devicons, Octicons, Powerline, etc&lt;span class="o"&gt;)&lt;/span&gt;

ttf-jetbrains-mono-nerd/tur-packages 3.5.0 all
  JetBrains Mono patched with Nerd Fonts icons/glyphs &lt;span class="o"&gt;(&lt;/span&gt;Font Awesome, Devicons, Octicons, Powerline, etc&lt;span class="o"&gt;)&lt;/span&gt;

ttf-meslo-nerd/tur-packages 3.5.0 all
  Meslo &lt;span class="o"&gt;(&lt;/span&gt;MesloLGS&lt;span class="o"&gt;)&lt;/span&gt; patched with Nerd Fonts icons/glyphs &lt;span class="o"&gt;(&lt;/span&gt;Font Awesome, Devicons, Octicons, Powerline, etc&lt;span class="o"&gt;)&lt;/span&gt;, commonly used with Powerlevel10k

ttf-roboto-mono-nerd/tur-packages 3.5.0 all
  Roboto Mono patched with Nerd Fonts icons/glyphs &lt;span class="o"&gt;(&lt;/span&gt;Font Awesome, Devicons, Octicons, Powerline, etc&lt;span class="o"&gt;)&lt;/span&gt;

ttf-sourcecodepro-nerd/tur-packages 3.5.0 all
  Source Code Pro patched with Nerd Fonts icons/glyphs &lt;span class="o"&gt;(&lt;/span&gt;Font Awesome, Devicons, Octicons, Powerline, etc&lt;span class="o"&gt;)&lt;/span&gt;

ttf-victor-mono-nerd/tur-packages 3.5.0 all
  Victor Mono patched with Nerd Fonts icons/glyphs &lt;span class="o"&gt;(&lt;/span&gt;Font Awesome, Devicons, Octicons, Powerline, etc&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight of those nine lines are mine — every one tagged &lt;code&gt;tur-packages&lt;/code&gt; with my name on &lt;code&gt;Maintainer:&lt;/code&gt;. The odd one out is &lt;code&gt;ttf-nerd-fonts-symbols/stable&lt;/code&gt;, which just happens to match the same search pattern: it's the pre-existing package from the main repo, maintained by &lt;code&gt;@termux&lt;/code&gt;, not by me. Easy to mix up in a grep, so worth saying plainly here.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;~ &lt;span class="nv"&gt;$ &lt;/span&gt;pkg show ttf-cascadia-code-nerd
Package: ttf-cascadia-code-nerd
Version: 3.5.0
Maintainer: Gouranga Das Samrat &amp;lt;gouranga.das.khulna@gmail.com&amp;gt;
Installed-Size: 11.4 MB
Homepage: https://www.nerdfonts.com/
Download-Size: 1550 kB
APT-Sources: https://tur.kcubeterm.com tur-packages/tur aarch64 Packages
Description: Cascadia Code patched with Nerd Fonts icons/glyphs &lt;span class="o"&gt;(&lt;/span&gt;Font Awesome, Devicons, Octicons, Powerline, etc&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same story across all eight — &lt;code&gt;ttf-hack-nerd&lt;/code&gt;, &lt;code&gt;ttf-inconsolata-nerd&lt;/code&gt;, &lt;code&gt;ttf-jetbrains-mono-nerd&lt;/code&gt;, &lt;code&gt;ttf-meslo-nerd&lt;/code&gt;, &lt;code&gt;ttf-roboto-mono-nerd&lt;/code&gt;, &lt;code&gt;ttf-sourcecodepro-nerd&lt;/code&gt;, and &lt;code&gt;ttf-victor-mono-nerd&lt;/code&gt; all show up with my &lt;code&gt;Maintainer:&lt;/code&gt; line and &lt;code&gt;tur.kcubeterm.com&lt;/code&gt; as the source. One PR, one merge, eight packages live at once — the biggest single contribution I've shipped to Termux so far, and my first time landing anything on TUR specifically.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually stuck with me
&lt;/h2&gt;

&lt;p&gt;Getting held to a stricter bar on the main repo didn't feel great mid-thread — three-thousand-package discipline running headfirst into nine fonts I was sure people wanted. But &lt;a href="https://github.com/robertkirkman" rel="noopener noreferrer"&gt;@robertkirkman&lt;/a&gt; and &lt;a href="https://github.com/TomJo2000" rel="noopener noreferrer"&gt;@TomJo2000&lt;/a&gt; weren't wrong to hold it, and the rejection wasn't a dead end so much as a redirect to the tool built for exactly this situation. &lt;code&gt;proton-pass-cli&lt;/code&gt; taught me how to get something into &lt;code&gt;termux-packages&lt;/code&gt;. This one taught me that knowing where a package &lt;em&gt;doesn't&lt;/em&gt; belong — and having TUR as somewhere to actually put it — is just as much a part of contributing as the packaging itself.&lt;/p&gt;

&lt;p&gt;If you're on Termux and tired of manually curling &lt;code&gt;.ttf&lt;/code&gt; files into &lt;code&gt;~/.termux/font.ttf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pkg &lt;span class="nb"&gt;install &lt;/span&gt;tur-repo
pkg update
pkg &lt;span class="nb"&gt;install &lt;/span&gt;ttf-jetbrains-mono-nerd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(swap in &lt;code&gt;hack&lt;/code&gt;, &lt;code&gt;cascadia-code&lt;/code&gt;, &lt;code&gt;meslo&lt;/code&gt;, &lt;code&gt;sourcecodepro&lt;/code&gt;, &lt;code&gt;inconsolata&lt;/code&gt;, &lt;code&gt;roboto-mono&lt;/code&gt;, or &lt;code&gt;victor-mono&lt;/code&gt; for whichever prompt you're patching.)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The full trail:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/termux/termux-packages/pull/31157" rel="noopener noreferrer"&gt;termux-packages #31157 — the 9-font PR I closed on the main repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/termux-user-repository/tur/pull/2743" rel="noopener noreferrer"&gt;tur #2743 — the 8-font PR that merged on TUR&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/termux-user-repository/dists/issues/9" rel="noopener noreferrer"&gt;termux-user-repository/dists #9 — the race condition that briefly blocked publishing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ryanoasis/nerd-fonts" rel="noopener noreferrer"&gt;ryanoasis/nerd-fonts — the upstream project behind all nine packages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/termux/termux-packages/pull/30987" rel="noopener noreferrer"&gt;proton-pass-cli #30987 — my first merge, on the main repo&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/gouranga-das-khulna/from-a-high-school-termux-user-to-a-package-maintainer-the-story-behind-my-first-merged-pr-on-kgc"&gt;My post on how that one happened&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>termux</category>
      <category>community</category>
      <category>android</category>
    </item>
  </channel>
</rss>
