Field notes from the review team
Learn to ship code that survives
The mistakes every team makes — in the code we write and the code we generate — and how to catch them before they ship. Friendly, concrete, ten minutes at a time.
Backend
Correctness, data access, and the services behind the API
Duplication Is Cheaper Than the Wrong Abstraction
We built a beautiful, flexible exporter framework to avoid writing three similar classes. Two years later, everyone was afraid to touch it. On the rule of three and earning your abstractions.
When Your AI Assistant Invents an API
The method looked perfect: well-named, idiomatic, exactly what the task needed. It also didn't exist. On hallucinated APIs, wrong-version code, and verifying what you merge.
Two Requests Walk Into a Bar: Race Conditions in Ordinary Web Code
The coupon was single-use. It got used 217 times in one weekend. On check-then-act bugs, why your code has more concurrency than you think, and the fixes that actually hold.
Where Does Business Logic Actually Live? A Field Guide
The create action was 180 lines and touched four models, two APIs, and a mailer. Nobody wrote it that way — it grew. On thin controllers, honest service objects, and gods.
Every External Call Will Fail. The Only Question Is How Gracefully.
A payment provider's slow Tuesday took down our whole app — not because they errored, but because they didn't. On timeouts, bounded retries, and circuit breakers.
Your API Is Leaking: Over-Serialization and the Contracts You Didn't Mean to Sign
render json: @user feels harmless until a column you added for internal bookkeeping shows up in a partner's integration. On accidental contracts and serializing on purpose.
rescue Exception and Other Ways to Hide the Body
The scariest bugs aren't the ones that crash — they're the ones that get rescued into silence. On swallowed errors, overly broad rescues, and failing in ways you can debug.
Migrations That Don't Take Production Down With Them
A migration that runs instantly on your laptop can lock a hot table for minutes in production. How to ship schema changes that coexist with running code.
Your Background Job Will Run Twice. Plan for It.
Queues promise at-least-once delivery, and 'at least' is doing heavy lifting. How to design jobs — especially the ones that touch money — to be safe to run again.
The N+1 Query: Death by a Thousand SELECTs
The page worked fine with ten records and fell over with ten thousand. A field guide to spotting the N+1 query hiding in your loops, serializers, and helpers.
Frontend
Components, UX states, and interfaces that tell the truth
When AI Writes Your UI: Reviewing for the Design System It Doesn't Know You Have
AI assistants write plausible components at astonishing speed — and quietly invent a second button, a third modal, and a parallel spacing scale. How to review UI for drift.
Two Kinds of State: Why Copying Server Data into useState Breeds Bugs
Half the 'state management' pain in React apps comes from treating a server cache like client state. On the two-state model, sync bugs, and single sources of truth.
Forms People Can Actually Finish
Watch five users fill out your signup form and you'll rewrite it by Friday. On validation timing, error placement, preserving input, and the disabled-button trap.
Performance Is a Feature (and It Has a Budget)
Nobody ships a slow app on purpose — it happens 40 kilobytes at a time. On bundle budgets, memoizing with evidence, and why measuring comes before optimizing.
Optimistic UI Without the Data Loss: A Practical Guide
Optimistic updates make apps feel instant — and, done carelessly, make them lie. Reversibility, race conditions, and how to reconcile with the server without losing user data.
key={index} Will Bite You: A Field Guide to React Reconciliation
The bug report said 'I deleted one row and a different row's data changed.' The cause was one character: using the array index as a key. Here's what's actually going on.
Your Component Is Doing Too Much: Finding the Seams
The 600-line component isn't a size problem, it's a responsibility problem. How to split data from presentation, and why 'reusable' components are often welded to one screen.
useEffect Is Not a Lifecycle Hook (and Other Things I Wish I'd Learned Sooner)
Most useEffect bugs come from one mental model error: treating effects as lifecycle events. Here's how to spot infinite loops, stale closures, and effects that shouldn't exist.
The Three UI States Everyone Forgets (Until a User Finds Them First)
Loading, empty, and error states are where real users actually live. Here's how happy-path-only components sneak into production, and how to catch them in review.
Security
Trust boundaries, and the code attackers read more carefully than we do
Why AI-Generated Code Needs a Security Pass
AI assistants write code that looks finished — and that polish is exactly what lowers a reviewer's guard. The blind spots I keep finding in generated code, and a routine that catches them.
Sessions, Cookies, and the Requests You Didn't Send
Your browser attaches cookies to requests you never meant to make. A working tour of CSRF, httpOnly and SameSite, and why a state-changing GET is a trap waiting to spring.
The Parameter You Didn't Mean to Accept
Mass assignment turns a convenient framework feature into a privilege-escalation path. How over-permissive param lists happen, and how to review the permit list like it matters.
Hiding the Button Is Not Security
If the only thing stopping a user from deleting the project is that they can't see the Delete button, nothing is stopping them. Why authorization must live on the server.
It's 2026 and Injection Still Happens. Here's Why.
SQL injection is old enough to rent a car, yet it keeps shipping — often via string interpolation that looks harmless. Why parameterization is non-negotiable, especially in generated code.
Multi-Tenant Apps Live or Die on Scoping
In a multi-tenant system, one missing WHERE clause is a data breach. Where tenant isolation actually breaks — jobs, exports, admin paths — and how to build scoping you can trust.
The Most Common API Bug I See in Review: IDOR
Insecure direct object references are boring, ancient, and still everywhere. Why find(params[:id]) needs a scope, and how to review for object-level authorization.
Secrets Don't Belong in Repos (Even Private Ones)
A hardcoded API key feels harmless right up until it isn't. How secrets leak through repos, logs, and error trackers — and the habits that keep them out.
Database
Schemas, constraints, and data that outlives the code around it
You'll Query It More Than You Write It
A row is written once and read thousands of times, by dashboards, debuggers, and support tickets. Designing schemas for the questions you'll ask, not just the data you'll store.
A Schema Someone Can Read at 2am
During an incident, your column names are the documentation. Conventions for ids, timestamps, and booleans that make a schema legible to a tired stranger, and to you in five years.
Soft Deletes Aren't Free: The True Cost of deleted_at
One nullable timestamp, and suddenly every query needs a WHERE clause, unique constraints stop working, and 'deleted' data haunts every join. What soft deletes really cost, and honest alternatives.
The Migration That Locked the Table (and How to Never Write One)
A one-line migration can take down production for eleven minutes. Understanding locks, lock queues, and zero-downtime patterns for changing big tables while they're being used.
Primary Keys Are Forever: Choosing Between Ints, UUIDs, and Regret
You'll change frameworks, languages, and clouds before you change a primary key. How to choose between sequential ints and UUIDs, and what happens when keys leak into URLs.
What a Nullable Column Is Really Telling You
NULL isn't a value, it's a question mark. Most nullable columns are unfinished modeling decisions in disguise, and three-valued logic makes sure you pay for them later.
Constraints Are Documentation That Can't Go Stale
NOT NULL, unique indexes, and foreign keys aren't red tape. They're the only documentation of your data rules that is enforced, tested, and impossible to ignore.
Indexes Are Not Magic: Build Them for the Queries You Actually Run
Adding an index feels like flipping a speed switch, until it doesn't. How indexes really work, why column order matters, and why indexing everything quietly punishes every write.
Accessibility
Interfaces every person — and every input device — can actually use
AI Writes Divs by Default: Reviewing Generated Markup for Accessibility
Generated UI code looks polished and renders beautifully — and its accessibility tree is often empty. Here are the four smells I hunt for when reviewing AI-assisted frontend PRs.
Tables a Screen Reader Can Actually Navigate
A well-marked-up table lets a screen reader user move through data like a spreadsheet — every cell announced with its headers. A div-built one reads as a stream of loose numbers.
The Route Changed and Nobody Noticed: Focus Management in Single-Page Apps
In an SPA, navigation is something you simulate — and if you don't move focus when the view changes, screen reader users are left sitting on a button that seems to do nothing.
When Red Is the Only Signal: Color, Contrast, and the Users Your Palette Forgets
Our deploy dashboard was a wall of red and green dots — until a colorblind teammate asked which ones were broken. Color can decorate meaning, but it can't be the only carrier.
Forms Everyone Can Fill: Labels, Errors, and the Autocomplete Attribute You Keep Forgetting
Most form accessibility bugs aren't exotic — they're a label that isn't wired to its input and an error message nobody's screen reader ever announces. Both are cheap to fix.
The Div With a Click Handler: Why Semantic HTML Beats ARIA Every Time
A styled div can look exactly like a button and still be invisible to half the ways people interact with your app. Native elements do the heavy lifting — if you let them.
Unplug Your Mouse: What Keyboard-First Testing Teaches You About Your App
A broken wrist turned one of my teammates into our best accessibility tester overnight. Here's why a keyboard-only pass catches half your a11y bugs before anyone files them.
Principles
The heuristics and laws behind durable engineering
When the Measure Becomes the Target
A team hits 90% coverage and the bugs keep coming. Velocity doubles and delivery doesn't. Goodhart's law explains both — and what it costs to ignore it.
Your New Teammate Types Very Fast
Every team has quietly hired a colleague who writes confident, fluent code at inhuman speed and never remembers yesterday. The question is whether you review them like one.
Vibe Coding, Engineering, and the Debt Nobody Invoices
Shipping code you don't understand now takes one afternoon. The bill arrives later, in a currency called comprehension. On Naur, vibe coding, and why 'it works' isn't 'it's done'.
The Boy Scout Rule Has a Speed Limit
Leave the code better than you found it — yes. But the noblest rule in software has a failure mode, and it looks like a one-line bugfix that arrives as a 600-line PR.
The Science of the Small Pull Request
A 40-line PR gets three thoughtful comments. An 1,800-line PR gets 'LGTM'. That's not laziness — it's arithmetic. Here's what batch size does to review quality, and how to slice.
Naming Is Design: The Name Comes First
We treat naming as the finishing touch on code. It's the opposite: a name is your first abstraction, and a bad one is usually a design decision you haven't made yet.
Your Working Memory Has a Budget, and Your Code Is Spending It
The bottleneck in software isn't the compiler or the network. It's the four or so things a human can hold in mind at once. Good code is memory management for people.
Code Is Read Far More Than It Is Written
Every line of code is written once and read dozens of times. The economics are lopsided, and once you see them, you never optimize for the writer again.