<?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>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>
    <item>
      <title>The Bug I Shipped, Then Had to Fix Myself: Disabling proton-pass-cli's Keyring on Android</title>
      <dc:creator>Gouranga Das Samrat</dc:creator>
      <pubDate>Thu, 20 Aug 2026 02:47:44 +0000</pubDate>
      <link>https://dev.to/gouranga-das-khulna/the-bug-i-shipped-then-had-to-fix-myself-disabling-proton-pass-clis-keyring-on-android-4l73</link>
      <guid>https://dev.to/gouranga-das-khulna/the-bug-i-shipped-then-had-to-fix-myself-disabling-proton-pass-clis-keyring-on-android-4l73</guid>
      <description>&lt;p&gt;A little over a week after &lt;a href="https://github.com/termux/termux-packages/pull/30987" rel="noopener noreferrer"&gt;&lt;code&gt;proton-pass-cli&lt;/code&gt; merged into &lt;code&gt;termux-packages&lt;/code&gt;&lt;/a&gt;, I was still riding the high of seeing my name next to &lt;code&gt;Maintainer:&lt;/code&gt;. Then someone actually &lt;em&gt;used&lt;/em&gt; the package the way a normal person would — fresh install, no tribal knowledge, straight to &lt;code&gt;pass-cli login&lt;/code&gt; — and it broke.&lt;/p&gt;

&lt;p&gt;That's the moment a package stops being "done" and starts being &lt;em&gt;maintained&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The comment that started it
&lt;/h2&gt;

&lt;p&gt;It came in on the original PR thread, from &lt;a href="https://github.com/EvanTechDev" rel="noopener noreferrer"&gt;@EvanTechDev&lt;/a&gt;, the same person who'd cheered the package on back when it was just an issue:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Hi there! Is this a bug?" — with a screenshot of &lt;code&gt;pass-cli login&lt;/code&gt; blowing up.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The error underneath the screenshot was this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Error creating client features
Caused by:
    0: Failed to get encryption key for database
    1: Could not get local key from keyring
    2: Error accessing credential [...]: NoDefaultStore
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My first reply was the fast one, the kind you type from your phone between other things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PROTON_PASS_KEY_PROVIDER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;fs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That workaround was correct, and it unblocked Evan. But a one-line env var you have to know to paste into your shell config isn't a fix — it's a rumor that gets passed around in comment threads. I opened &lt;a href="https://github.com/termux/termux-packages/pull/31137" rel="noopener noreferrer"&gt;PR #31137 — &lt;code&gt;fix(main/proton-pass-cli): disable keyring provider on Android&lt;/code&gt;&lt;/a&gt; to actually close the gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it was broken in the first place
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;pass-cli&lt;/code&gt;'s default key storage, &lt;code&gt;PROTON_PASS_KEY_PROVIDER=keyring&lt;/code&gt;, doesn't talk to D-Bus or Secret Service at all — it goes straight for the Linux &lt;strong&gt;kernel keyring&lt;/strong&gt;, via the &lt;code&gt;linux_keyutils&lt;/code&gt; crate. Android's kernel doesn't expose that the way a normal desktop Linux kernel does, so the backend fails to even initialize. &lt;code&gt;NoDefaultStore&lt;/code&gt; isn't a config problem, it's a "this API doesn't exist here" problem.&lt;/p&gt;

&lt;p&gt;And installing &lt;code&gt;gnome-keyring&lt;/code&gt; doesn't rescue you, either — &lt;code&gt;pass-cli&lt;/code&gt; only speaks to a Secret Service provider like GNOME Keyring when you explicitly set &lt;code&gt;PROTON_PASS_LINUX_KEYRING=dbus&lt;/code&gt;. There's no automatic fallback. On Termux, the default provider was simply never going to work, for anyone, out of the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two philosophies, one thread
&lt;/h2&gt;

&lt;p&gt;I opened the PR with a shim: intercept the env var, force it to something sane before &lt;code&gt;pass-cli&lt;/code&gt; ever saw the broken default. It was the smaller diff.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/robertkirkman" rel="noopener noreferrer"&gt;@robertkirkman&lt;/a&gt;, who'd reviewed nearly every line of the original &lt;code&gt;proton-pass-cli&lt;/code&gt; PR too, wasn't on board:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I prefer not to use this solution. Please patch the Rust code to completely erase &lt;code&gt;PROTON_PASS_KEY_PROVIDER=keyring&lt;/code&gt; as an option from the codebase, forcing the default to something else that way."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Meanwhile &lt;a href="https://github.com/TomJo2000" rel="noopener noreferrer"&gt;@TomJo2000&lt;/a&gt; had actually been the one who nudged me toward the shim approach in the first place, drawing a comparison to how Termux handles &lt;code&gt;just&lt;/code&gt;'s env var. So I had two respected reviewers, two different directions, and I did the only sane thing: I stopped coding and asked them to agree with each other first.&lt;/p&gt;

&lt;p&gt;They did — and TomJo2000 talked himself out of his own suggestion in the process:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If &lt;code&gt;keyring&lt;/code&gt; is entirely broken then removing the codepath is probably the better solution. [...] the difference being here that the options form a set and the default selection doesn't work on Android. Whereas &lt;code&gt;just&lt;/code&gt; provides its env var as an additional configuration mechanism with no default, and we set it to a sane value for our context."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That distinction stuck with me. A shim makes sense when you're &lt;em&gt;adding&lt;/em&gt; a knob Termux needs. It doesn't make sense when the knob already exists and one of its settings is just categorically broken on the platform — at that point you patch the source, not the environment around it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patching Rust, then patching it properly
&lt;/h2&gt;

&lt;p&gt;So I went into &lt;code&gt;pass-cli/src/features/mod.rs&lt;/code&gt; and rewrote the provider match: an empty key provider (&lt;code&gt;""&lt;/code&gt;) now maps to &lt;code&gt;fs&lt;/code&gt;, and anyone who explicitly asks for &lt;code&gt;keyring&lt;/code&gt; gets a clear error explaining that the kernel keyring is unsupported on Termux/Android, instead of a cryptic &lt;code&gt;NoDefaultStore&lt;/code&gt; three layers deep in a stack trace.&lt;/p&gt;

&lt;p&gt;First pass, I did it as inline &lt;code&gt;sed&lt;/code&gt; edits in &lt;code&gt;build.sh&lt;/code&gt;. Robertkirkman flagged that immediately — three separate instances of the same file path, code being replaced with no context preserved, and lines getting long inside &lt;code&gt;build.sh&lt;/code&gt; itself. He also mentioned, not unkindly, that I'd never actually written a &lt;code&gt;.patch&lt;/code&gt; file for a Termux package before and offered to walk me through it if I needed it. I moved everything into a proper patch file instead, per &lt;a href="https://github.com/termux/termux-packages/blob/master/CONTRIBUTING.md#creating-patch-files" rel="noopener noreferrer"&gt;the contributing guide's instructions on creating patch files&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Even inside the patch file, the review kept sharpening things. Robertkirkman spotted that my new &lt;code&gt;""&lt;/code&gt; case and the existing &lt;code&gt;"fs"&lt;/code&gt; case were doing the same thing and could just be combined:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="s"&gt;"fs"&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* filesystem-based local key provider */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I made the change, pushed what I thought was the update — and then sat through a slightly awkward round where he was still looking at the old version because I'd merged locally but hadn't actually pushed yet. Nothing dramatic, just the very normal rhythm of an async review: confirm, fix, confirm again.&lt;/p&gt;

&lt;p&gt;The last review comment wasn't even about my patch — it was about the older &lt;code&gt;0001-fix-protoc-path.patch&lt;/code&gt; from the original PR. Robertkirkman asked me to preserve a short comment above &lt;code&gt;--- a/pass-domain/build.rs&lt;/code&gt; explaining that the patch exists to point the build at the &lt;code&gt;protoc&lt;/code&gt; binary from &lt;code&gt;termux_setup_protobuf&lt;/code&gt;, so the next person reading the patch file doesn't have to reconstruct &lt;em&gt;why&lt;/em&gt; it exists from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it landed
&lt;/h2&gt;

&lt;p&gt;By the end there were two clean patch files sitting in &lt;code&gt;packages/proton-pass-cli/&lt;/code&gt;: the existing protoc path fix, now with context preserved at the top, and a new &lt;code&gt;0002-disable-android-keyring.patch&lt;/code&gt; that removes the broken default at the source instead of working around it downstream.&lt;/p&gt;

&lt;p&gt;Robertkirkman closed the thread with the kind of message that makes the whole review cycle worth it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Thank you for fixing it!"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What actually changed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pass-cli login&lt;/code&gt; now works on a fresh Termux install, no env var required, no tribal knowledge to pass around in comment threads.&lt;/li&gt;
&lt;li&gt;Anyone who does explicitly reach for &lt;code&gt;PROTON_PASS_KEY_PROVIDER=keyring&lt;/code&gt; gets told exactly why it won't work on Android, instead of &lt;code&gt;NoDefaultStore&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The fix lives in a real &lt;code&gt;.patch&lt;/code&gt; file with context preserved, which was a first for me on this project — and something I now know how to do for the next bug.&lt;/li&gt;
&lt;li&gt;The original shim idea didn't survive review, and that's fine. Getting talked out of the smaller diff by two people who understood the platform better than I did is exactly what review is for.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The part that keeps repeating
&lt;/h2&gt;

&lt;p&gt;This is the second time in a month that shipping a Termux package taught me more than writing it did. First it was a broken symlink chain in &lt;code&gt;sqlcipher&lt;/code&gt; that nobody had ever tripped over. This time it was a keyring backend that simply doesn't exist on Android, hiding behind an env var nobody knew to set. Neither bug showed up until a real person ran a real command and hit a wall.&lt;/p&gt;

&lt;p&gt;Maintaining a package, it turns out, is mostly this: someone posts a screenshot, and you go find out why.&lt;/p&gt;

&lt;p&gt;If you want the trail:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/termux/termux-packages/pull/31137" rel="noopener noreferrer"&gt;PR #31137 — the keyring fix&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/termux/termux-packages/pull/30987" rel="noopener noreferrer"&gt;PR #30987 — the original &lt;code&gt;proton-pass-cli&lt;/code&gt; package&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/gouranga-das-khulna/the-ghost-in-the-symlink-how-i-hunted-down-a-broken-lsqlcipher-bug-in-termux-and-got-ninjad-2hmg"&gt;My last post — the sqlcipher symlink bug&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;How the package got merged in the first place&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if this is the first you're hearing of any of it:&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;proton-pass-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It'll actually log in now.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>termux</category>
      <category>debugging</category>
      <category>rust</category>
    </item>
    <item>
      <title>From a High School Termux User to a Package Maintainer: The Story Behind My First Merged PR on termux-packages</title>
      <dc:creator>Gouranga Das Samrat</dc:creator>
      <pubDate>Tue, 18 Aug 2026 16:35:01 +0000</pubDate>
      <link>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</link>
      <guid>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</guid>
      <description>&lt;p&gt;There's a specific kind of disbelief that hits when you run a command on your own phone and the output has your own name in it.&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 proton-pass-cli
Package: proton-pass-cli
Version: 2.3.2
Maintainer: Gouranga Das Samrat &amp;lt;gouranga.das.khulna@gmail.com&amp;gt;
Installed-Size: 26.5 MB
Depends: openssl, protobuf, sqlcipher, zlib
Homepage: https://protonpass.github.io/pass-cli/
Download-Size: 6346 kB
APT-Sources: https://packages-cf.termux.dev/apt/termux-main stable/main aarch64 Packages
Description: Proton Pass Command Line Interface &lt;span class="o"&gt;(&lt;/span&gt;CLI&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I've been using &lt;a href="https://github.com/termux/termux-packages" rel="noopener noreferrer"&gt;Termux&lt;/a&gt; since high school — mostly as the terminal I didn't have on a laptop, running scripts on a phone screen, poking at packages other people maintained. I never expected to end up on the other side of &lt;code&gt;Maintainer:&lt;/code&gt;. But that's exactly what happened this week, and I want to write down how it went, because the journey mattered more than the merge.&lt;/p&gt;

&lt;h2&gt;
  
  
  It started with a gap I kept running into
&lt;/h2&gt;

&lt;p&gt;I use &lt;a href="https://protonpass.github.io/pass-cli/" rel="noopener noreferrer"&gt;Proton Pass&lt;/a&gt; for my passwords, and its official CLI client, &lt;code&gt;pass-cli&lt;/code&gt;, is exactly the kind of tool Termux users like — no browser, no Electron GUI, just a fast terminal client for managing vault items over SSH sessions and scripts. Termux already ships &lt;code&gt;pass&lt;/code&gt; and &lt;code&gt;gopass&lt;/code&gt;. &lt;code&gt;pass-cli&lt;/code&gt; was conspicuously missing.&lt;/p&gt;

&lt;p&gt;So I opened &lt;a href="https://github.com/termux/termux-packages/issues/30966" rel="noopener noreferrer"&gt;issue #30966&lt;/a&gt;, laid out the case for why it belonged in &lt;code&gt;termux-packages&lt;/code&gt;, and went through the packaging policy checklist. A day after I filed it, &lt;a href="https://github.com/EvanTechDev" rel="noopener noreferrer"&gt;@EvanTechDev&lt;/a&gt; dropped a comment saying they were looking forward to it too — which, small as it was, made the whole thing feel less like a solo request and more like something worth actually building.&lt;/p&gt;

&lt;h2&gt;
  
  
  Opening my first real PR
&lt;/h2&gt;

&lt;p&gt;That turned into &lt;a href="https://github.com/termux/termux-packages/pull/30987" rel="noopener noreferrer"&gt;PR #30987&lt;/a&gt; — &lt;code&gt;addpkg(main/proton-pass-cli): 2.3.2&lt;/code&gt;. On paper it sounds simple: package a Rust CLI, point it at system libraries instead of vendoring everything, ship it. In practice it was my first real PR to &lt;code&gt;termux-packages&lt;/code&gt;, and it showed almost immediately.&lt;/p&gt;

&lt;p&gt;The build script needed &lt;code&gt;protoc&lt;/code&gt; patched in via &lt;code&gt;sed&lt;/code&gt; because the crate's usual vendored-binary downloader has no prebuilt for &lt;code&gt;aarch64-unknown-linux-android&lt;/code&gt;. I wired up &lt;code&gt;OPENSSL_NO_VENDOR&lt;/code&gt;, &lt;code&gt;LIBSQLITE3_SYS_USE_PKG_CONFIG&lt;/code&gt;, and &lt;code&gt;LIBZ_SYS_TEXT_LINK&lt;/code&gt; to keep everything on system libraries instead of source builds. Small, mechanical fixes — the kind you make while you're still learning where the edges of the build system are.&lt;/p&gt;

&lt;p&gt;Then &lt;a href="https://github.com/robertkirkman" rel="noopener noreferrer"&gt;@robertkirkman&lt;/a&gt;, who reviewed nearly every line of this PR, pointed out something I hadn't clocked at all: &lt;code&gt;proton-pass-cli&lt;/code&gt; was about to become &lt;strong&gt;the first true reverse dependency of &lt;code&gt;sqlcipher&lt;/code&gt;&lt;/strong&gt; in the entire &lt;code&gt;termux-packages&lt;/code&gt; repo. Nothing had ever linked against it before. That single fact is what turned a routine "add a package" PR into a two-week deep dive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that only existed because I showed up
&lt;/h2&gt;

&lt;p&gt;Right on cue, the final link step died:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;ld.lld: error: unable to find library -lsqlcipher
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;libsqlcipher.so&lt;/code&gt; was sitting right there in &lt;code&gt;find&lt;/code&gt; output. The linker didn't care. It turned out to be a broken symlink chain, left behind by a &lt;code&gt;mv&lt;/code&gt; on a symlink that renamed the link's name but not what it pointed to — a bug that had been quietly sitting in &lt;code&gt;sqlcipher&lt;/code&gt;'s &lt;code&gt;build.sh&lt;/code&gt; for who knows how long, invisible because nothing had ever needed to resolve that symlink before.&lt;/p&gt;

&lt;p&gt;I actually wrote up the full investigation as its own post if you want the blow-by-blow: &lt;strong&gt;&lt;a href="https://dev.to/gouranga-das-khulna/the-ghost-in-the-symlink-how-i-hunted-down-a-broken-lsqlcipher-bug-in-termux-and-got-ninjad-2hmg"&gt;The Ghost in the Symlink: How I Hunted Down a Broken &lt;code&gt;-lsqlcipher&lt;/code&gt; Bug in Termux (and Got Ninja'd on the Fix)&lt;/a&gt;&lt;/strong&gt;. Short version: I patched around it locally first, then opened &lt;a href="https://github.com/termux/termux-packages/pull/31013" rel="noopener noreferrer"&gt;PR #31013&lt;/a&gt; to fix it properly in &lt;code&gt;sqlcipher&lt;/code&gt; itself — and &lt;a href="https://github.com/robertkirkman" rel="noopener noreferrer"&gt;@robertkirkman&lt;/a&gt; had already opened &lt;a href="https://github.com/termux/termux-packages/pull/31010" rel="noopener noreferrer"&gt;PR #31010&lt;/a&gt; fixing the exact same root cause about two hours earlier. I closed mine as a duplicate, no hard feelings, and rebased &lt;code&gt;proton-pass-cli&lt;/code&gt; on top of his fix instead.&lt;/p&gt;

&lt;p&gt;I figured that was the hard part done. It was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The edge case nobody had hit before
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;sqlcipher&lt;/code&gt; and &lt;code&gt;proton-pass-cli&lt;/code&gt; being fixed separately still left an open question: what happens when &lt;code&gt;sqlcipher&lt;/code&gt; and its reverse dependency are modified &lt;strong&gt;in the same PR&lt;/strong&gt;, so both build fresh in the same CI run? &lt;a href="https://github.com/robertkirkman" rel="noopener noreferrer"&gt;@robertkirkman&lt;/a&gt; asked me to actually test that scenario, since it changes build ordering in ways that don't show up when &lt;code&gt;sqlcipher&lt;/code&gt; is just pulled pre-built from the apt repo.&lt;/p&gt;

&lt;p&gt;That request turned into its own small saga — a merge-commit accidentally slipping into my branch that CI correctly rejected, a stale local copy of &lt;code&gt;sqlcipher/build.sh&lt;/code&gt; that silently reintroduced the very bug we'd just fixed, and finally a real rebase that let the &lt;code&gt;ld.lld: unable to find library -lsqlcipher&lt;/code&gt; error resurface on purpose, in the exact edge case it was supposed to test. Once it did, &lt;code&gt;TERMUX_PKG_EXTRA_MAKE_ARGS&lt;/code&gt; overriding &lt;code&gt;libsqlite3.DLL.basename&lt;/code&gt; turned out to be the clean fix, and CI went green on &lt;code&gt;aarch64&lt;/code&gt;, &lt;code&gt;arm&lt;/code&gt;, &lt;code&gt;i686&lt;/code&gt;, and &lt;code&gt;x86_64&lt;/code&gt; at once, with &lt;code&gt;sqlcipher&lt;/code&gt; and &lt;code&gt;proton-pass-cli&lt;/code&gt; both built fresh in the same run.&lt;/p&gt;

&lt;p&gt;Then we found a second, related gap: the headers and &lt;code&gt;.pc&lt;/code&gt; file only get their &lt;code&gt;sqlcipher&lt;/code&gt; names inside the final packaged &lt;code&gt;.deb&lt;/code&gt;, not in the live prefix during a same-run build — meaning a future C/C++ reverse dependency of &lt;code&gt;sqlcipher&lt;/code&gt; could hit the same class of bug at &lt;code&gt;#include &amp;lt;sqlcipher.h&amp;gt;&lt;/code&gt; or &lt;code&gt;pkg-config sqlcipher&lt;/code&gt; time instead of at the linker. I confirmed it with a fresh build and a handful of &lt;code&gt;ls&lt;/code&gt;/&lt;code&gt;pkg-config&lt;/code&gt; checks against the live &lt;code&gt;$TERMUX_PREFIX&lt;/code&gt;. Since it didn't block a Rust package like mine, we split it off as its own follow-up rather than holding up &lt;code&gt;#30987&lt;/code&gt; for it, and &lt;a href="https://github.com/robertkirkman" rel="noopener noreferrer"&gt;@robertkirkman&lt;/a&gt; landed a proper fix for it with a &lt;code&gt;tcl/main.mk&lt;/code&gt; patch that removes the fragile rename step entirely. He was even generous enough to file &lt;a href="https://github.com/sqlcipher/sqlcipher/issues/607" rel="noopener noreferrer"&gt;an upstream issue on sqlcipher itself&lt;/a&gt; about why this pattern exists across so many distros in the first place.&lt;/p&gt;

&lt;p&gt;Along the way &lt;a href="https://github.com/TomJo2000" rel="noopener noreferrer"&gt;@TomJo2000&lt;/a&gt; reviewed too — alphabetizing dependencies, fixing the &lt;code&gt;Maintainer:&lt;/code&gt; field format, spacing conventions before functions. Small things, but the kind of small things that add up to a package that looks like it belongs in the repo instead of one that was just dropped in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Squash, rebase, merge
&lt;/h2&gt;

&lt;p&gt;By the time everything was resolved — sqlcipher's &lt;code&gt;.so&lt;/code&gt; symlinks fixed upstream in the package, the header/pkgconfig gap fixed in a follow-up, six commits squashed into one clean commit at &lt;a href="https://github.com/robertkirkman" rel="noopener noreferrer"&gt;@robertkirkman&lt;/a&gt;'s request — there was one last exchange that made me smile: he asked whether there should also be a plain &lt;code&gt;proton-pass&lt;/code&gt; package to match the AUR naming, and once I explained that one's the full Electron GUI and this one is CLI-only, he agreed the Electron build wasn't worth packaging for Termux.&lt;/p&gt;

&lt;p&gt;Then: &lt;em&gt;"Thank you for making this!"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/termux/termux-packages/pull/30987" rel="noopener noreferrer"&gt;PR #30987&lt;/a&gt; merged. &lt;code&gt;proton-pass-cli&lt;/code&gt; &lt;code&gt;2.3.2&lt;/code&gt; is now a real Termux package. Anyone can run &lt;code&gt;pkg install proton-pass-cli&lt;/code&gt; and get it — no vendored binaries, no Electron footprint, just a clean CLI backed by the system's &lt;code&gt;openssl&lt;/code&gt;, &lt;code&gt;protobuf&lt;/code&gt;, &lt;code&gt;sqlcipher&lt;/code&gt;, and &lt;code&gt;zlib&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually changed
&lt;/h2&gt;

&lt;p&gt;The version string says &lt;code&gt;2.3.2&lt;/code&gt;, but that's not really what shipped. What shipped was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A real, upstream fix to a &lt;code&gt;sqlcipher&lt;/code&gt; symlink bug that had existed silently for as long as nobody linked against it — now fixed for every reverse dependency that comes after mine, not just this one.&lt;/li&gt;
&lt;li&gt;A same-PR/same-run build ordering edge case tested and fixed, so the next person adding a &lt;code&gt;sqlcipher&lt;/code&gt; reverse dependency doesn't have to rediscover any of this.&lt;/li&gt;
&lt;li&gt;A second, related header/pkgconfig gap found and fixed before a C/C++ package ever had the chance to hit it blind.&lt;/li&gt;
&lt;li&gt;An &lt;a href="https://github.com/sqlcipher/sqlcipher/issues/607" rel="noopener noreferrer"&gt;upstream issue&lt;/a&gt; opened against &lt;code&gt;sqlcipher&lt;/code&gt; itself, so this isn't just patched around in Termux forever.&lt;/li&gt;
&lt;li&gt;One more terminal-native way to manage Proton Pass vaults on Android, for anyone who'd rather not touch a browser extension.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The part that actually stuck with me
&lt;/h2&gt;

&lt;p&gt;I've been a Termux user since I was in high school, running other people's packages without a second thought about how they got there. What I didn't expect from filing one feature request is that it would turn into weeks of testing build orderings, reading symlink resolution behavior, and getting pulled into a genuinely upstream sqlcipher discussion — all because I happened to be the first person to ask for a package that needed to link against &lt;code&gt;sqlcipher&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;None of that was in the plan. I just wanted a password manager CLI on my phone. But that's apparently how you become a maintainer: not by planning to, but by being the first person to actually need something, and staying in the thread long enough to fix what breaks.&lt;/p&gt;

&lt;p&gt;If you want the full trail, it's all public:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/termux/termux-packages/issues/30966" rel="noopener noreferrer"&gt;#30966 — the feature request that started it&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/termux/termux-packages/pull/30987" rel="noopener noreferrer"&gt;#30987 — addpkg(main/proton-pass-cli): 2.3.2, the PR that merged&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/termux/termux-packages/pull/31013" rel="noopener noreferrer"&gt;#31013 — my sqlcipher fix attempt, closed as a duplicate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/termux/termux-packages/pull/31010" rel="noopener noreferrer"&gt;#31010 — the sqlcipher fix that actually landed, by @robertkirkman&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/gouranga-das-khulna/the-ghost-in-the-symlink-how-i-hunted-down-a-broken-lsqlcipher-bug-in-termux-and-got-ninjad-2hmg"&gt;The full symlink bug write-up, on dev.to&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if you're on Termux and want a terminal-native way into your Proton Pass vault:&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;proton-pass-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>opensource</category>
      <category>rust</category>
      <category>termux</category>
      <category>android</category>
    </item>
    <item>
      <title>One Line, One NULL, One Noisy Error: Fixing usql's Tab Completion Crash</title>
      <dc:creator>Gouranga Das Samrat</dc:creator>
      <pubDate>Mon, 17 Aug 2026 04:00:00 +0000</pubDate>
      <link>https://dev.to/gouranga-das-khulna/one-line-one-null-one-noisy-error-fixing-usqls-tab-completion-crash-3fmn</link>
      <guid>https://dev.to/gouranga-das-khulna/one-line-one-null-one-noisy-error-fixing-usqls-tab-completion-crash-3fmn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Not every open-source contribution needs a saga. Sometimes the whole story is: hit an annoying error, find the one missing &lt;code&gt;COALESCE&lt;/code&gt;, ship a one-line diff, done. This is one of those — a small bug in &lt;a href="https://github.com/xo/usql" rel="noopener noreferrer"&gt;&lt;code&gt;usql&lt;/code&gt;&lt;/a&gt;, the universal command-line SQL client, that turned into a satisfying little fix with a slightly bumpy landing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The bug: tab completion that complains before it works
&lt;/h2&gt;

&lt;p&gt;While using &lt;code&gt;usql&lt;/code&gt; against PostgreSQL, hitting &lt;strong&gt;Tab&lt;/strong&gt; for autocompletion threw this at me every single time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error getting selectables sql: Scan error on column index 6, name "routine_definition": converting NULL to string is unsupported
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frustrating part: completion still &lt;em&gt;worked&lt;/em&gt; afterward. The error was purely noise — but noise you'd see on every single tab-press, which gets old fast. I filed it as &lt;a href="https://github.com/xo/usql/issues/554" rel="noopener noreferrer"&gt;issue #554&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root cause: not every function has a definition
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;usql&lt;/code&gt; builds its completion suggestions by querying &lt;code&gt;information_schema.routines&lt;/code&gt;, and one of the columns it scans is &lt;code&gt;routine_definition&lt;/code&gt;. That column holds the SQL body of a function or procedure — but only for functions written &lt;em&gt;in&lt;/em&gt; SQL. For external or C-language functions, PostgreSQL simply has nothing to put there, so the column comes back &lt;code&gt;NULL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Go's &lt;code&gt;database/sql&lt;/code&gt; scanning is strict about this: you can't scan a &lt;code&gt;NULL&lt;/code&gt; straight into a &lt;code&gt;string&lt;/code&gt;. You need a &lt;code&gt;sql.NullString&lt;/code&gt;, a pointer type, or — the simplest fix when you're just displaying the value — coerce it at the SQL level.&lt;/p&gt;

&lt;p&gt;Digging into &lt;code&gt;drivers/metadata/informationschema/metadata.go&lt;/code&gt;, the surrounding query already knew this. Neighboring nullable columns were wrapped defensively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="s"&gt;"COALESCE(routine_type, '')"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="s"&gt;"COALESCE(data_type, '')"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;routine_definition&lt;/code&gt; was just the one column that had been missed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;One line, same pattern as its neighbors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- "routine_definition",
&lt;/span&gt;&lt;span class="gi"&gt;+ "COALESCE(routine_definition, '')",
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. &lt;code&gt;COALESCE&lt;/code&gt; swaps the &lt;code&gt;NULL&lt;/code&gt; for an empty string before it ever reaches the scanner, consistent with how the query already treated &lt;code&gt;routine_type&lt;/code&gt; and &lt;code&gt;data_type&lt;/code&gt;. No new imports, no new types, no behavior change for the common case — just one more nullable column brought in line with its siblings.&lt;/p&gt;

&lt;p&gt;I opened &lt;a href="https://github.com/xo/usql/pull/555" rel="noopener noreferrer"&gt;PR #555&lt;/a&gt; with the fix, referencing &lt;a href="https://github.com/xo/usql/issues/554" rel="noopener noreferrer"&gt;#554&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Merged — sort of
&lt;/h2&gt;

&lt;p&gt;Maintainer &lt;a href="https://github.com/kenshaw" rel="noopener noreferrer"&gt;@kenshaw&lt;/a&gt; reviewed it quickly and closed out the issue, and a few days later the PR itself was closed with a note: &lt;em&gt;"Added to main. Thanks!"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Except the PR wasn't merged in the GitHub sense — the change had been applied manually rather than through a merge commit. GitHub was straightforward about it too, flagging the PR as &lt;strong&gt;"Closed with unmerged commits"&lt;/strong&gt; since my branch still had commits that were never actually merged into &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I asked about it, since authorship tracking matters for contribution history:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I noticed the changes were added manually instead of merging the PR. Would it be possible to preserve authorship in such cases (e.g., via merge or co-author)?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://github.com/kenshaw" rel="noopener noreferrer"&gt;@kenshaw&lt;/a&gt;'s answer cleared it up immediately — this wasn't a snub, it was branch juggling:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I used cherry-pick here as I needed to add it to the release-21 branch as well as main. I should have used rebase onto main, and then cherry-picked it to the release branch."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which makes complete sense once you think about it: a one-line fix like this isn't just useful on &lt;code&gt;main&lt;/code&gt;, it's useful on the active release branch too, and cherry-picking is the standard way to land the same commit in two places without a merge tangling up both histories. The trade-off is that GitHub's PR UI doesn't recognize a cherry-picked commit as "merged," even though the commit — author, committer, and all — is sitting right there in the Git history on both branches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;A few small things worth remembering from a fix this size:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;COALESCE&lt;/code&gt; is your friend for optional metadata columns.&lt;/strong&gt; Anywhere you're joining against &lt;code&gt;information_schema&lt;/code&gt; or similar catalogs, assume any column &lt;em&gt;can&lt;/em&gt; be &lt;code&gt;NULL&lt;/code&gt; unless you know otherwise, and handle it at the query level if you're just going to stringify it anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency is a debugging shortcut.&lt;/strong&gt; The fix here wasn't found through deep investigation — it was found by noticing the query already had a pattern for handling this exact problem on two other columns, and one column had been left out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Closed, not merged" isn't always a red flag.&lt;/strong&gt; If a maintainer needs a fix on multiple branches (like a release branch &lt;em&gt;and&lt;/em&gt; main), cherry-picking is often the cleaner tool, even though it means GitHub won't show your PR as formally merged. Your authorship still travels with the commit either way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full trail if you want to see it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/xo/usql/issues/554" rel="noopener noreferrer"&gt;Issue #554 — Tab completion error: Scan error on routine_definition NULL column&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/xo/usql/pull/555" rel="noopener noreferrer"&gt;PR #555 — fix: wrap routine_definition in COALESCE to handle NULL&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>go</category>
      <category>sql</category>
      <category>debugging</category>
    </item>
    <item>
      <title>The Ghost in the Symlink: How I Hunted Down a Broken `-lsqlcipher` Bug in Termux (and Got Ninja'd on the Fix)</title>
      <dc:creator>Gouranga Das Samrat</dc:creator>
      <pubDate>Sun, 16 Aug 2026 12:27:43 +0000</pubDate>
      <link>https://dev.to/gouranga-das-khulna/the-ghost-in-the-symlink-how-i-hunted-down-a-broken-lsqlcipher-bug-in-termux-and-got-ninjad-2hmg</link>
      <guid>https://dev.to/gouranga-das-khulna/the-ghost-in-the-symlink-how-i-hunted-down-a-broken-lsqlcipher-bug-in-termux-and-got-ninjad-2hmg</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Some bugs announce themselves loudly. Segfaults, stack traces, five-hundred-line panics. This one didn't. This one just sat there quietly for who knows how long, waiting for the first person unlucky (or lucky?) enough to actually need it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That person turned out to be me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup: packaging &lt;code&gt;proton-pass-cli&lt;/code&gt; for Termux
&lt;/h2&gt;

&lt;p&gt;I've been working on getting the &lt;a href="https://protonpass.github.io/pass-cli/" rel="noopener noreferrer"&gt;Proton Pass CLI&lt;/a&gt; packaged for &lt;a href="https://github.com/termux/termux-packages" rel="noopener noreferrer"&gt;&lt;code&gt;termux-packages&lt;/code&gt;&lt;/a&gt;, tracked in &lt;a href="https://github.com/termux/termux-packages/pull/30987" rel="noopener noreferrer"&gt;PR #30987&lt;/a&gt;. It's a Rust project, cross-compiled for Android, pulling in system libraries instead of vendored ones wherever possible — &lt;code&gt;openssl&lt;/code&gt;, &lt;code&gt;zlib&lt;/code&gt;, &lt;code&gt;protobuf&lt;/code&gt;, and &lt;code&gt;sqlcipher&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Everything was going fine until the final link step, when the build died with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;ld.lld: error: unable to find library -lsqlcipher
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is a delightfully unhelpful error when you're staring at &lt;code&gt;find&lt;/code&gt; output that clearly shows &lt;code&gt;libsqlcipher.so&lt;/code&gt; sitting right there in &lt;code&gt;$PREFIX/lib&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chasing a library that "exists" but doesn't
&lt;/h2&gt;

&lt;p&gt;My first instinct was the usual suspects: wrong &lt;code&gt;PKG_CONFIG_PATH&lt;/code&gt;, missing &lt;code&gt;PKG_CONFIG_ALLOW_CROSS&lt;/code&gt; for cross-compilation, wrong &lt;code&gt;SQLCIPHER_LIB_DIR&lt;/code&gt;. I set all of those. Still broken.&lt;/p&gt;

&lt;p&gt;So I went straight to the filesystem. &lt;code&gt;ls -la&lt;/code&gt; on the lib directory told a different story than &lt;code&gt;find&lt;/code&gt; had:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;libsqlcipher.so   -&amp;gt; libsqlite3.so.0          (broken)
libsqlcipher.so.0 -&amp;gt; libsqlite3.so.&amp;lt;version&amp;gt;  (broken)
libsqlcipher.so.&amp;lt;version&amp;gt;                     (the only real file)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There it was. &lt;code&gt;libsqlcipher.so&lt;/code&gt; existed as a &lt;em&gt;name&lt;/em&gt;, but it was a symlink pointing at &lt;code&gt;libsqlite3.so.0&lt;/code&gt; — a file that no longer existed, because it had also been renamed away. The linker doesn't care that a file named &lt;code&gt;libsqlcipher.so&lt;/code&gt; is present; it cares whether the symlink actually &lt;em&gt;resolves&lt;/em&gt;. &lt;code&gt;find&lt;/code&gt; and &lt;code&gt;ls&lt;/code&gt; will happily list a broken symlink by its entry name and let you assume everything's fine. The linker knows better.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root cause: &lt;code&gt;mv&lt;/code&gt; on a symlink doesn't do what you think
&lt;/h2&gt;

&lt;p&gt;The actual bug lived one layer upstream, in the &lt;code&gt;sqlcipher&lt;/code&gt; package's own &lt;code&gt;termux_step_post_massage()&lt;/code&gt;, where Termux renames the sqlite3 build artifacts to sqlcipher names to avoid collisions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mv &lt;/span&gt;lib/lib&lt;span class="o"&gt;{&lt;/span&gt;sqlite3,sqlcipher&lt;span class="o"&gt;}&lt;/span&gt;.so
&lt;span class="nb"&gt;mv &lt;/span&gt;lib/lib&lt;span class="o"&gt;{&lt;/span&gt;sqlite3,sqlcipher&lt;span class="o"&gt;}&lt;/span&gt;.so.0
&lt;span class="nb"&gt;mv &lt;/span&gt;lib/lib&lt;span class="o"&gt;{&lt;/span&gt;sqlite3,sqlcipher&lt;span class="o"&gt;}&lt;/span&gt;.so.&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$sql_version&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the trap: &lt;code&gt;libsqlite3.so&lt;/code&gt; and &lt;code&gt;libsqlite3.so.0&lt;/code&gt; aren't regular files, they're symlinks. &lt;code&gt;mv&lt;/code&gt; on a symlink only renames &lt;em&gt;the link itself&lt;/em&gt; — it does not rewrite the target path stored inside it. So after this rename:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The link now named &lt;code&gt;libsqlcipher.so&lt;/code&gt; still internally points at &lt;code&gt;libsqlite3.so.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The link now named &lt;code&gt;libsqlcipher.so.0&lt;/code&gt; still internally points at &lt;code&gt;libsqlite3.so.&amp;lt;version&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Both of those target names no longer exist, because the real versioned file got renamed to &lt;code&gt;libsqlcipher.so.&amp;lt;version&amp;gt;&lt;/code&gt; in the same step&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A broken symlink chain, hiding in plain sight, for what appears to have been the entire life of the &lt;code&gt;sqlcipher&lt;/code&gt; package — because &lt;strong&gt;nothing had ever linked against it before&lt;/strong&gt;. &lt;code&gt;proton-pass-cli&lt;/code&gt; was about to become the first true reverse dependency of &lt;code&gt;sqlcipher&lt;/code&gt; in &lt;code&gt;termux-packages&lt;/code&gt;, and reverse dependencies are exactly the kind of thing that surfaces bugs nobody could have found by testing the package in isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The temporary fix
&lt;/h2&gt;

&lt;p&gt;I didn't own the &lt;code&gt;sqlcipher&lt;/code&gt; package, and I needed my own PR unblocked, so I patched around it locally in &lt;code&gt;proton-pass-cli&lt;/code&gt;'s own &lt;code&gt;termux_step_pre_configure()&lt;/code&gt; — detect the real installed &lt;code&gt;.so.&amp;lt;version&amp;gt;&lt;/code&gt; file at build time and recreate the symlinks pointing at it correctly:&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;_sqlcipher_real_so&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;find &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TERMUX_PREFIX&lt;/span&gt;&lt;span class="s2"&gt;/lib"&lt;/span&gt; &lt;span class="nt"&gt;-maxdepth&lt;/span&gt; 1 &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s1"&gt;'libsqlcipher.so.*'&lt;/span&gt; &lt;span class="nt"&gt;-print&lt;/span&gt; &lt;span class="nt"&gt;-quit&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_sqlcipher_real_so&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;_sqlcipher_real_so&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;basename&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_sqlcipher_real_so&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-sf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_sqlcipher_real_so&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TERMUX_PREFIX&lt;/span&gt;&lt;span class="s2"&gt;/lib/libsqlcipher.so.0"&lt;/span&gt;
    &lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-sf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$_sqlcipher_real_so&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TERMUX_PREFIX&lt;/span&gt;&lt;span class="s2"&gt;/lib/libsqlcipher.so"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It worked. The build went green on all four architectures. Problem solved — for my package, at least.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Fix it properly, in the right place"
&lt;/h2&gt;

&lt;p&gt;When &lt;a href="https://github.com/robertkirkman" rel="noopener noreferrer"&gt;@robertkirkman&lt;/a&gt; reviewed &lt;a href="https://github.com/termux/termux-packages/pull/30987" rel="noopener noreferrer"&gt;PR #30987&lt;/a&gt;, he pointed out the obvious: papering over a broken symlink in every downstream consumer of &lt;code&gt;sqlcipher&lt;/code&gt; doesn't scale. The correct fix belongs in &lt;code&gt;sqlcipher&lt;/code&gt;'s own &lt;code&gt;build.sh&lt;/code&gt;, not duplicated in mine and every future reverse dependency.&lt;/p&gt;

&lt;p&gt;Fair. So I opened &lt;a href="https://github.com/termux/termux-packages/pull/31013" rel="noopener noreferrer"&gt;PR #31013&lt;/a&gt; against &lt;code&gt;packages/sqlcipher/build.sh&lt;/code&gt;, fixing it at the source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; lib/libsqlite3.so lib/libsqlite3.so.0
&lt;span class="nb"&gt;mv &lt;/span&gt;lib/lib&lt;span class="o"&gt;{&lt;/span&gt;sqlite3,sqlcipher&lt;span class="o"&gt;}&lt;/span&gt;.so.&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$sql_version&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-sf&lt;/span&gt; libsqlcipher.so.&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$sql_version&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; lib/libsqlcipher.so.0
&lt;span class="nb"&gt;ln&lt;/span&gt; &lt;span class="nt"&gt;-sf&lt;/span&gt; libsqlcipher.so.0 lib/libsqlcipher.so
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of trying to &lt;code&gt;mv&lt;/code&gt; symlinks and hoping the target updates itself, remove the stale links entirely and recreate them fresh, pointing at the correctly renamed versioned file — with the version pulled from &lt;code&gt;$sql_version&lt;/code&gt; so it stays correct across future bumps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beaten to the finish line by two hours
&lt;/h2&gt;

&lt;p&gt;Here's the part that made me laugh instead of sigh: by the time I pushed &lt;a href="https://github.com/termux/termux-packages/pull/31013" rel="noopener noreferrer"&gt;PR #31013&lt;/a&gt;, &lt;a href="https://github.com/robertkirkman" rel="noopener noreferrer"&gt;@robertkirkman&lt;/a&gt; had &lt;em&gt;already&lt;/em&gt; opened &lt;a href="https://github.com/termux/termux-packages/pull/31010" rel="noopener noreferrer"&gt;PR #31010&lt;/a&gt; fixing the exact same root cause — roughly two hours earlier. Same diagnosis, functionally the same fix, just arrived at independently and slightly faster.&lt;/p&gt;

&lt;p&gt;No hard feelings there. It happens constantly in open source — two people staring at the same broken symlink chain from different angles, both reaching for &lt;code&gt;ln -sf&lt;/code&gt;. I closed &lt;a href="https://github.com/termux/termux-packages/pull/31013" rel="noopener noreferrer"&gt;#31013&lt;/a&gt; as a duplicate and updated &lt;a href="https://github.com/termux/termux-packages/pull/30987" rel="noopener noreferrer"&gt;#30987&lt;/a&gt; to track &lt;a href="https://github.com/termux/termux-packages/pull/31010" rel="noopener noreferrer"&gt;#31010&lt;/a&gt; instead. Once that lands, my temporary workaround in &lt;code&gt;proton-pass-cli&lt;/code&gt; comes back out, and the fix lives exactly where it should: upstream, in &lt;code&gt;sqlcipher&lt;/code&gt; itself, benefiting every reverse dependency that comes after — not just mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;p&gt;A few things stuck with me from this one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;mv&lt;/code&gt; on a symlink is not the same as &lt;code&gt;mv&lt;/code&gt; on a file.&lt;/strong&gt; It renames the link, not the target it points to. If you're renaming a &lt;em&gt;pair&lt;/em&gt; of symlink + real file in the same operation, do the real file first and rebuild the links from scratch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;find&lt;/code&gt; and &lt;code&gt;ls&lt;/code&gt; lie by omission.&lt;/strong&gt; They'll show you a symlink's name happily, broken or not. If you actually need to know whether something resolves, check that explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First reverse dependencies are bug detectors.&lt;/strong&gt; A library that's never been linked against can hide structural issues indefinitely. The moment something actually depends on it, all bets are off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix bugs where they live, not where you found them.&lt;/strong&gt; My instinct was to patch around the symptom in my own package. The maintainer's instinct — fix it in &lt;code&gt;sqlcipher&lt;/code&gt; itself — was the right one, even though it meant more work and, as it turned out, a friendly race I didn't win.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to follow the whole trail, it's all public:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/termux/termux-packages/pull/30987" rel="noopener noreferrer"&gt;#30987 — addpkg(main/proton-pass-cli): 2.3.2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/termux/termux-packages/pull/31013" rel="noopener noreferrer"&gt;#31013 — my fix attempt (closed as duplicate)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/termux/termux-packages/pull/31010" rel="noopener noreferrer"&gt;#31010 — the fix that actually landed, by @robertkirkman&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>rust</category>
      <category>debugging</category>
      <category>termux</category>
    </item>
    <item>
      <title>Bloom Filters</title>
      <dc:creator>Gouranga Das Samrat</dc:creator>
      <pubDate>Sun, 16 Aug 2026 02:00:00 +0000</pubDate>
      <link>https://dev.to/gouranga-das-khulna/bloom-filters-4j30</link>
      <guid>https://dev.to/gouranga-das-khulna/bloom-filters-4j30</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;One-liner:&lt;/strong&gt; A probabilistic data structure that tells you if an element is &lt;strong&gt;definitely not&lt;/strong&gt; in a set, or &lt;strong&gt;possibly&lt;/strong&gt; in a set — using very little memory.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  📌 The Problem
&lt;/h2&gt;

&lt;p&gt;You have 1 billion URLs in a database. Before adding a new URL, you want to check if it already exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Naive approach:&lt;/strong&gt; Query the database every time.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cost: 1 DB query per URL check → slow, expensive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Bloom Filter approach:&lt;/strong&gt; Check the bloom filter first (microseconds, no DB hit).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If filter says NO → URL definitely not in DB → safe to insert&lt;/li&gt;
&lt;li&gt;If filter says YES → URL might be in DB → query DB to confirm&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  💡 How Bloom Filters Work
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Structure
&lt;/h3&gt;

&lt;p&gt;An array of &lt;code&gt;m&lt;/code&gt; bits, all initialized to 0.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bit array: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]  (m=10 bits)
            0  1  2  3  4  5  6  7  8  9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Insertion (k hash functions)
&lt;/h3&gt;

&lt;p&gt;To insert element &lt;code&gt;x&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hash &lt;code&gt;x&lt;/code&gt; with &lt;code&gt;k&lt;/code&gt; different hash functions&lt;/li&gt;
&lt;li&gt;Each hash function returns a position in the bit array&lt;/li&gt;
&lt;li&gt;Set those positions to 1
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Insert "google.com":
  hash1("google.com") = 2 → bit[2] = 1
  hash2("google.com") = 5 → bit[5] = 1
  hash3("google.com") = 8 → bit[8] = 1

Bit array: [0, 0, 1, 0, 0, 1, 0, 0, 1, 0]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Lookup
&lt;/h3&gt;

&lt;p&gt;To check if &lt;code&gt;y&lt;/code&gt; is in the set:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hash &lt;code&gt;y&lt;/code&gt; with the same &lt;code&gt;k&lt;/code&gt; functions&lt;/li&gt;
&lt;li&gt;Check if ALL those positions are 1
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Check "google.com":
  hash1 → bit[2] = 1 ✅
  hash2 → bit[5] = 1 ✅
  hash3 → bit[8] = 1 ✅
  → POSSIBLY in set (correct! it was inserted)

Check "bing.com":
  hash1 → bit[1] = 0 ❌
  → DEFINITELY NOT in set (100% accurate)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ⚠️ False Positives (Not False Negatives)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  False Positive Example
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Insert "amazon.com":
  hash1 = 2, hash2 = 7, hash3 = 3

Bit array (after inserting google.com + amazon.com):
[0, 0, 1, 1, 0, 1, 0, 1, 1, 0]

Check "yahoo.com":
  hash1("yahoo.com") = 2 → bit[2] = 1 ✅
  hash2("yahoo.com") = 7 → bit[7] = 1 ✅
  hash3("yahoo.com") = 3 → bit[3] = 1 ✅
  → "POSSIBLY in set" ← FALSE POSITIVE! (yahoo.com was never inserted)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bits were set by OTHER elements, creating a false positive.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Guarantees
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Guarantee&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DEFINITELY NOT in set&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;100% accurate (no false negatives)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;POSSIBLY in set&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Might be wrong (false positives possible)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cannot delete&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Once a bit is set, you can't safely unset it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  📊 Tuning Bloom Filters
&lt;/h2&gt;

&lt;p&gt;Two parameters control accuracy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;m&lt;/strong&gt; = size of bit array (more bits → fewer false positives)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;k&lt;/strong&gt; = number of hash functions (optimal k depends on m and n)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;n&lt;/strong&gt; = number of expected elements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;False positive rate formula:&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;p ≈ (1 - e^(-kn/m))^k
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Practical sizing:&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;1% false positive rate → ~10 bits per element
0.1% false positive rate → ~15 bits per element

For 1 billion URLs, 1% FP rate:
→ 10 × 1B bits = 10 billion bits = 1.25 GB
vs
→ Storing 1B URLs as strings = ~50-100 GB
→ 40-80× memory savings!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🌍 Real-World Use Cases
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Google Chrome&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Malicious URL check (local bloom filter)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Apache Cassandra&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Skip SSTables that don't contain a key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Bitcoin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SPV wallets filter transactions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Akamai CDN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Avoid caching one-hit-wonder URLs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Medium&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;"Have you seen this article?" check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Email spam filters&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fast first-pass spam detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HBase/BigTable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Reduce disk reads for non-existent rows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Example: Web Crawler (Avoid Revisiting URLs)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pybloom_live&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BloomFilter&lt;/span&gt;

&lt;span class="n"&gt;bf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BloomFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1_000_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.01&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_crawl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&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&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;bf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;  &lt;span class="c1"&gt;# probably already crawled (or false positive)
&lt;/span&gt;    &lt;span class="n"&gt;bf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;  &lt;span class="c1"&gt;# definitely not crawled yet
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example: Username Availability
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User types username "rahul123"
→ Check bloom filter (microseconds)
→ Filter says NO → username definitely available ✅ (no DB query needed)
→ Filter says YES → query DB to confirm (might be false positive)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🆚 Bloom Filter vs Hash Set
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Bloom Filter&lt;/th&gt;
&lt;th&gt;Hash Set&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;Very small (bits)&lt;/td&gt;
&lt;td&gt;Large (full values)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lookup&lt;/td&gt;
&lt;td&gt;O(k) very fast&lt;/td&gt;
&lt;td&gt;O(1) fast&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;False positives&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;False negatives&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;td&gt;Never&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deletion&lt;/td&gt;
&lt;td&gt;Not supported&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Count elements&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🔄 Variants
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variant&lt;/th&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Counting Bloom Filter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Supports deletions (use counters instead of bits)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Scalable Bloom Filter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Grows dynamically as elements are added&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cuckoo Filter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Supports deletion, similar performance&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;Bit array with positions labeled&lt;/li&gt;
&lt;li&gt;Three hash functions pointing to different positions&lt;/li&gt;
&lt;li&gt;Insertion of "google.com" setting 3 bits&lt;/li&gt;
&lt;li&gt;Lookup showing definite miss vs possible hit&lt;/li&gt;
&lt;li&gt;False positive scenario with two elements colliding&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔑 Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Bloom filters use &lt;strong&gt;tiny memory&lt;/strong&gt; to check set membership&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No false negatives&lt;/strong&gt; — if it says NO, it's definitely NO&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;False positives possible&lt;/strong&gt; — if it says YES, verify with DB&lt;/li&gt;
&lt;li&gt;Perfect for &lt;strong&gt;pre-filtering&lt;/strong&gt; expensive DB/disk lookups&lt;/li&gt;
&lt;li&gt;Tune with bits-per-element to control false positive rate&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>systemdesign</category>
      <category>devops</category>
      <category>programming</category>
    </item>
    <item>
      <title>CAP Theorem</title>
      <dc:creator>Gouranga Das Samrat</dc:creator>
      <pubDate>Sat, 15 Aug 2026 02:00:00 +0000</pubDate>
      <link>https://dev.to/gouranga-das-khulna/cap-theorem-539k</link>
      <guid>https://dev.to/gouranga-das-khulna/cap-theorem-539k</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;One-liner:&lt;/strong&gt; In a distributed system, you can only guarantee two of three properties — Consistency, Availability, and Partition Tolerance — at the same time.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  📌 The Three Properties
&lt;/h2&gt;

&lt;h3&gt;
  
  
  C — Consistency
&lt;/h3&gt;

&lt;p&gt;Every read returns the &lt;strong&gt;most recent write&lt;/strong&gt; (or an error). All nodes see the same data at the same time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Node A: Write x=5
Node B: Read x → must return 5 (not an old value)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A — Availability
&lt;/h3&gt;

&lt;p&gt;Every request receives a &lt;strong&gt;non-error response&lt;/strong&gt; (but the data might be stale).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Node B is out of sync but still responds:
Read x → returns 3 (old value, but NOT an error)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  P — Partition Tolerance
&lt;/h3&gt;

&lt;p&gt;The system &lt;strong&gt;continues to operate&lt;/strong&gt; even when network partitions cause nodes to be unable to communicate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Node A] ~~~ NETWORK PARTITION ~~~ [Node B]
System still works (doesn't go down)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🔺 The Triangle
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;         Consistency
             /\
            /  \
           /    \
          / CP   \
         /        \
        /____  ____\
   CA  /      \/     \ AP
      /    PICK 2    \
     /________________\
  Availability      Partition
                    Tolerance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;In practice: Partitions happen. You must choose P.&lt;/strong&gt;&lt;br&gt;
So the real choice is: &lt;strong&gt;CP&lt;/strong&gt; or &lt;strong&gt;AP&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  🔀 CP Systems — Consistency + Partition Tolerance
&lt;/h2&gt;

&lt;p&gt;When a network partition occurs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;System &lt;strong&gt;refuses to respond&lt;/strong&gt; rather than return stale data&lt;/li&gt;
&lt;li&gt;Prioritizes correctness over availability
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Node A (Primary) ~~~ partition ~~~ Node B (Replica)

Request to Node B → "I can't reach primary, refusing to serve" → Error/timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt; HBase, Zookeeper, etcd, MongoDB (by default config), Google Spanner&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Banking — wrong balance is worse than no balance&lt;/li&gt;
&lt;li&gt;Inventory — showing wrong stock can cause overselling&lt;/li&gt;
&lt;li&gt;Leader election — must have consistent view of who's the leader&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  🔀 AP Systems — Availability + Partition Tolerance
&lt;/h2&gt;

&lt;p&gt;When a network partition occurs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;System &lt;strong&gt;continues to serve requests&lt;/strong&gt; (possibly stale data)&lt;/li&gt;
&lt;li&gt;Prioritizes availability over correctness
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Node A (Primary) ~~~ partition ~~~ Node B (Replica)

Request to Node B → "I'll serve my stale data" → Responds (maybe stale)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Examples:&lt;/strong&gt; Cassandra, DynamoDB, CouchDB, DNS, Riak&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Social media likes/views — a few seconds lag is fine&lt;/li&gt;
&lt;li&gt;Product catalog — slightly stale price is acceptable&lt;/li&gt;
&lt;li&gt;DNS — serving cached records during failures is fine&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  📊 Real Database Classification
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Database&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL&lt;/td&gt;
&lt;td&gt;CA (single node) / CP (distributed)&lt;/td&gt;
&lt;td&gt;Single node: no partition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;CA (single node) / CP (with replication)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MongoDB&lt;/td&gt;
&lt;td&gt;CP&lt;/td&gt;
&lt;td&gt;Can configure for AP with lower write concern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cassandra&lt;/td&gt;
&lt;td&gt;AP&lt;/td&gt;
&lt;td&gt;Tunable consistency (ONE to ALL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DynamoDB&lt;/td&gt;
&lt;td&gt;AP (default) / CP (with strong reads)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis&lt;/td&gt;
&lt;td&gt;CP (Cluster mode)&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zookeeper&lt;/td&gt;
&lt;td&gt;CP&lt;/td&gt;
&lt;td&gt;Used for coordination&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HBase&lt;/td&gt;
&lt;td&gt;CP&lt;/td&gt;
&lt;td&gt;Strong consistency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CouchDB&lt;/td&gt;
&lt;td&gt;AP&lt;/td&gt;
&lt;td&gt;Conflict resolution&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  🔧 Tunable Consistency (Cassandra)
&lt;/h2&gt;

&lt;p&gt;Real systems aren't binary. Cassandra lets you tune per-query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CONSISTENCY ONE   → fastest, least consistent (1 node responds)
CONSISTENCY QUORUM → balanced (majority of nodes respond)
CONSISTENCY ALL   → slowest, most consistent (all nodes respond)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Quorum formula:&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;Nodes = 5
Quorum = floor(5/2) + 1 = 3

Write to 3 + Read from 3 → at least 1 node overlaps → strong consistency
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🔄 PACELC — Extension of CAP
&lt;/h2&gt;

&lt;p&gt;CAP only talks about partition scenarios. &lt;strong&gt;PACELC&lt;/strong&gt; extends it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If Partition (P): choose between Availability (A) or Consistency (C)
Else (E):         choose between Latency (L) or Consistency (C)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even without partitions, there's a trade-off:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Sync replication → strong consistency but &lt;strong&gt;higher latency&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Async replication → lower latency but &lt;strong&gt;weaker consistency&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;If P&lt;/th&gt;
&lt;th&gt;Else&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DynamoDB&lt;/td&gt;
&lt;td&gt;AP&lt;/td&gt;
&lt;td&gt;EL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cassandra&lt;/td&gt;
&lt;td&gt;AP&lt;/td&gt;
&lt;td&gt;EL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MongoDB&lt;/td&gt;
&lt;td&gt;CP&lt;/td&gt;
&lt;td&gt;EC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spanner&lt;/td&gt;
&lt;td&gt;CP&lt;/td&gt;
&lt;td&gt;EC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🏗️ Eventual Consistency in Practice
&lt;/h2&gt;

&lt;p&gt;AP systems promise &lt;strong&gt;eventual consistency&lt;/strong&gt; — given no new writes, all nodes will eventually converge.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;t=0: Write x=5 to Node A
t=1: Read from Node B → returns 3 (stale)
t=2: Replication happens
t=3: Read from Node B → returns 5 ✅ (converged)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How long does "eventually" take?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Same datacenter: milliseconds&lt;/li&gt;
&lt;li&gt;Cross-region: 100ms to seconds&lt;/li&gt;
&lt;li&gt;During partition: until partition heals&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔑 Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;In practice: &lt;strong&gt;Partition Tolerance is non-negotiable&lt;/strong&gt; → choose CP or AP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CP&lt;/strong&gt; = strong consistency, sacrifice availability during partition&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AP&lt;/strong&gt; = always available, sacrifice consistency (eventual)&lt;/li&gt;
&lt;li&gt;Most modern databases offer &lt;strong&gt;tunable consistency&lt;/strong&gt; — you choose per operation&lt;/li&gt;
&lt;li&gt;Match the trade-off to your business need: financial data → CP; social data → AP&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>systemdesign</category>
    </item>
  </channel>
</rss>
