Backend & Full-Stack Engineer — building high-performance systems and clean APIs. Go · TypeScript · React · PostgreSQL · Redis · Docker
Nothing here yet.
Appreciate the careful read, Hamed. You're right on lesson 5 — an unexported struct type as the context key is the right call over a plain string, and the staticcheck warning is a good reminder. I'll tighten that section. And the ReadHeaderTimeout suggestion is a genuinely good addition — most handlers think about request timeouts but forget slowloris-style clients can tie up the listener before a request ever forms. I'll add a lesson on server timeouts.
Good explainer. The operational side I'd add: set autovacuum_vacuum_scale_factor (and threshold) per-table for your hottest tables instead of relying on the global defaults — defaults are tuned for average workloads, not hundreds of millions of rows. Two other classics: fillfactor below 100 on heavily-updated tables is what makes HOT updates possible in the first place (no free space, no HOT), and a single long-running transaction can block vacuum for the whole table — pg_stat_activity is the first place I look when n_dead_tup keeps climbing. Monitoring pg_stat_user_tables.n_dead_tup + last_autovacuum per table turns this from mystery into routine.
Strong post — RLS is the right answer for exactly the reason you give. Three production gotchas worth adding: (1) RLS doesn't apply to table owners or superusers, so the app role must not own the tables, and watch out for BYPASSRLS; (2) the tenant context (e.g. SET app.current_tenant) must be reset per checkout when you're behind PgBouncer — a leaked tenant setting is a data leak; (3) policy expressions run on every row, so keep them sargable and index-friendly on large tables or RLS becomes a performance tax. Miss any of the three and the enforcement story quietly breaks.
Useful walkthrough. One sharpening: pair go mod graph with go mod why <module> — the graph shows you the edges, why tells you which of your imports actually pulls a module in, which is what you need when deciding what to cut. Also worth noting for readers: since Go 1.17 module graph pruning, the graph is shallower than it used to be, and a "cycle" in require directives is a version-selection quirk — true import cycles are still rejected by the compiler. Different problems, different fixes.
Great framing — "what correct means" is exactly the hard part. The mechanisms I'd want to see named: (1) idempotency keys on every transfer, so retries never double-move money; (2) an immutable double-entry ledger — never update balances in place, balances are a derived projection over the entry log; (3) debit/credit pairs inside a single DB transaction with proper isolation; (4) settlement retries keyed off the transfer record's state machine, not blind re-submission. Get those four right and most "ghost money" bugs disappear.
The sharpest question in this thread is how much of the 65% is the language vs the rewrite itself. A useful way to decompose it: language effects (binary size, idle memory footprint, GC pause profile, goroutine-per-connection vs worker processes) vs architectural effects (connection pooling, added caching, fewer instances from better utilization). The honest test is measuring the same architecture in both languages, which rewrites never do. In my experience the instance-count reduction — running fewer, smaller containers — usually dominates the savings. The language enables it, but the architecture cashes it.
Nice walkthrough. Two things worth adding for anyone taking this to production: (1) fixed-window counters have a boundary stampede problem — a burst at the end of one window plus a burst at the start of the next can double the intended rate. Sliding-window log or token bucket fixes it. (2) The in-memory approach breaks the moment you run more than one replica — each instance gets its own budget. The standard production answer is a Redis-backed token bucket (a small Lua script keeps the check-and-decrement atomic). Happy to sketch it if useful.