← Writing

Trace every visible surface back to its data source

During a billing migration, one customer's balance widget showed -1 while their real balance was fine — because one endpoint still read the orphaned old column. The rules I now follow on every data migration.

On this page

A customer messaged support convinced that top-ups were broken. Their billing widget showed a balance of -1. Their actual balance — the one the system was really charging against — was healthy, over a thousand credits. Nothing about their money was wrong. The display was reading a column that no longer meant anything.

This happened during a billing-data migration I ran: the canonical balance was moving from a user-level column to a project-level one. Most read paths were updated. One endpoint wasn’t — it still read the orphaned user-level column, which the migration had left behind at a meaningless value. The customer’s dashboard faithfully rendered that corpse.

The bug itself took minutes to fix. What’s worth writing about is why it survived the migration, because every one of the reasons is a habit I had and you probably have too.

How a well-audited migration still missed a surface

I trusted a file’s label instead of its contents. The endpoint lived in a file I had mentally bucketed early in the audit as belonging to a different feature area — “not balance-related.” Having labeled it, I never re-examined whether the specific handler inside it read the migrating column. Route names and directory layout describe code organization; they are not evidence about what data a handler touches.

My greps searched for the code’s names, not the screen’s names. I searched for the service functions and fields I knew about on the old path. I never grepped for the variable names the rendered widget actually used — the hook, the response field, the label string on screen. The path that broke used none of my search terms.

I traced the paths I was asked about, not all the paths. The migration request focused on the enforcement side — where balances gate actions. I walked those carefully. I didn’t do the same exercise, unprompted, for the display side. Display paths feel harmless; they’re also the paths customers stare at all day.

My review agent’s coverage was bounded by my file list. I ran a self-review over the migration, but I fed it the files I considered relevant — which, by construction, excluded the file I had mislabeled. A reviewer, human or AI, cannot audit a surface you never put in front of it. The boring-looking files are exactly where migration bugs hide, because boring is why nobody re-reads them.

The rules I migrated to

1. For every user-visible field changing column ownership, walk all four hops. Rendered DOM → frontend hook → service function → API route → database column. Every hop, every field, even when it’s tedious. Don’t let file names or directory structure stand in for any hop.

2. Grep for what the user sees. Find the literal string on the screen — “Balance”, “Usable Credits” — and chase backward: every variable it renders, every hook feeding those variables, every endpoint feeding the hooks, every column feeding the endpoints. This search starts from the surface, so it can’t be fooled by your mental model of the backend.

3. “Out of scope” is a claim that needs verification. When you flag a file as unrelated, spend the extra sixty seconds grepping that file for the column you’re migrating away from. The label is a shortcut for organizing work, not a substitute for reading.

4. Give review agents the whole feature area, not the interesting parts. List every file the feature touches, explicitly including the ones you’ve already “cleared.” The review is only as complete as its inputs, and your inputs encode your blind spots.

5. A history that disagrees with a widget is a smoking gun, not a cache problem. In this incident, the transaction history’s most recent balanceAfter and the displayed balance disagreed. The reflex is to say “stale cache.” Resist it. When two surfaces show different numbers for the same fact, the first hypothesis should be that they read different columns — check which source each one queries before you blame caching.

The general shape

Migrations fail at the edges of your attention, not at the center. The core path — the one you designed the migration around — gets tested, reviewed, and verified. The stragglers are the read paths you didn’t know existed, in files you’d already classified, found only by tracing from the pixels backward.

So trace from the pixels backward. It’s mechanical, it’s boring, and it’s the only audit that starts from what the customer actually experiences instead of from what you remember building.

← All writing Book a call →
Book a call → WhatsApp