The bug inside the fix
A payout race, the fix for it, the bug inside that fix, and why it is the best argument I have for one person owning the whole path.
3 min readGoldVault · Payout stage
A customer cashes out. The platform asks the processor to send the money. Two requests for the same payout arrive together, as they do when someone double-taps or a client retries, and nothing stops both from reaching the processor. That is a double payment, in real money, and it is the kind of bug that decides whether a payments platform gets to exist.
The fix
The redemption row is flipped to an initiated state atomically, before the processor is called. An update with a condition: set the state to initiated where the state is still pending, or failed from an earlier attempt, and return the row. Two requests race; the database serialises them; exactly one update matches and returns a row; the other returns nothing and is told the payout is already in progress. Only the winner calls the processor, and every failure branch after that point compensates by flipping the row back. Textbook, and correct.
The bug inside it
It shipped, and every payout started failing as a conflict. Every single one, including the first request with no race at all. The update was matching, the state was flipping, the processor was never being called, and the customer was being told someone else got there first.
The cause was in the HTTP data layer between the client library and the database, a layer below the code that had been written. When you ask that library to update rows and return them, it re-applies your filter to the rows that come back. The filter said state equals pending. The returned row's state was now initiated. So the library, helpfully, filtered the winning row out of its own result, the code saw an empty result, and an empty result meant somebody else had won.
Nothing in the documentation says this. It was found by running payouts end to end, the commit says so, and explaining it meant reading below the code that had been written until the behaviour was visible, then restructuring the query so the returned row was fetched in a way the filter could not touch. The fix to the fix was a few lines. The fix landed twenty-six hours after the previous commit to that file, and two weeks after the race fix it was inside.
Why this is the argument
Consider how this goes on a team of specialists. A backend engineer writes the atomic flip and is right. A frontend engineer sees every payout return a conflict and files a bug against the backend. The backend engineer reproduces it, confirms the update works in SQL, and sends it back. Somebody eventually suspects the client library, but nobody owns the client library, and nobody's job description includes reading its source. The bug lives for a sprint, then two, while payouts are manual.
When one person owns the checkout, the route handler, the query, the database function, the data layer's behaviour and the row the customer sees afterwards, there is nowhere for the bug to hide between people. There is nobody to hand it to. You read the source because it is the next thing on the path, and the path is yours.
That is what the line at the top of this site means. Not that one person types faster than five. That there is a kind of bug, and a kind of decision, that only gets found by somebody who is standing on every layer at once. This was one of them.