A 9-line fix, 13 minutes to prod

Orr Yakobi
The pull request that stopped a luxury-watch marketplace platform from publishing watch serial numbers on public eBay listings changed 9 lines and deleted 2.
It went from opened to merged in 13 minutes, from 08:25 to 08:38 UTC.
SWARECO runs the engineering team that shipped it. The speed was not bravado. For luxury goods, a published serial number is a theft and fraud vector, so every hour the listing generator kept emitting them had a real cost.
This post is about that emergency hotfix, two of its siblings, and what actually made 13 minutes safe. The same team took two days to merge a different marketplace fix, on purpose.
What was leaking
The platform generates eBay listings from a dealer's inventory. These listings carry "item aspects," which are structured attribute pairs like case size and dial color. The builder that assembled them contained this:
'Dial Color' => [watch.dial],
'Bezel Type' => [watch.bezel],
'Band Material' => [watch.bracelet],
'Serial Number' => [watch.secret_serial_number]
Read the last line twice.
The model attribute is literally named secret_serial_number. The schema knew this field was confidential. The aspects builder published it to one of the largest public marketplaces on the internet, labeled, structured, and searchable.
The codebase even knew better in a different corner. The AI prompt that helps fill listing attributes carried an explicit rule, "Do NOT fabricate serial numbers, certifications, or exact measurements."
The sensitivity was understood at the prompt layer and unenforced one service over.
Systems do not leak what nobody knew was secret. They leak what everybody knew was secret and one code path forgot.
Why serials are different for luxury goods
A watch serial number uniquely identifies one physical object worth a car.
When published, it lets a stranger do things dealers spend real effort preventing. They can quote "their" serial in a fraudulent warranty or insurance claim. They can manufacture provenance for a counterfeit, since fakes carry real serials copied from somewhere, and a public listing is a catalogue of them. They can also socially engineer a dealer with convincing knowledge of a specific watch in their inventory.
None of this requires the watch to be stolen. The number alone has value. That is why the field was named secret_serial_number in the first place.
The anatomy of the 13 minutes
The fix deleted the aspect line. The other seven changed lines are a spec:
it "Not leak serial number in aspects" do
watch.update!(serial_number: '123456789')
aspects = service.build_aspects(watch)
expect(aspects).not_to have_key('Serial Number')
end
That test shape is worth naming. A leak fix's regression test is an absence assertion. You cannot pin "the page looks right," you pin that this key must never appear again. This ensures the next engineer who helpfully re-adds the aspect gets a red build instead of a production incident. The suite already had a cluster of not_to have_key assertions on this builder, and the fix joined it.
The 13 minutes also depended on the change being legible. The entire blast radius fits on one screen. It was a hash literal in one builder method, exercised by one spec file, feeding one outbound payload. A reviewer can hold every consequence of deleting that line in their head before their scroll wheel stops.
That legibility is not luck. It is what a builder pattern buys you on the day it matters.
If the aspects had been assembled across four callbacks and a decorator, the same one-line deletion would have needed an hour of tracing to approve. The serial numbers would have spent that hour on eBay.
Opened 08:25, merged 08:38, on the main branch and out.
What an emergency hotfix process actually needs
A hotfix is a targeted fix that bypasses the regular release cycle because the problem cannot wait for the next scheduled update — a security vulnerability, a data leak, a critical bug putting downtime on the meter. Urgent issues create pressure to skip steps, and the textbook worry about emergency fixes is exactly that: skipping the full regression pass will introduce new bugs. The textbook answer is a standing process: keep the code changes small and targeted, ship the regression test inside the same pull request, run the deployment to production through the same pipeline as any other change, and know your rollback before you merge.
The serial-number fix is what that looks like in practice.
- Small and targeted. 9 lines added, 2 deleted, one builder method. A quick fix is not the same as a careless one, and it is not a refactor with a fix hidden inside it.
- Tested in the same PR. Seven of the nine lines are the spec asserting the leak stays gone. Testing hotfixes cannot mean a full regression suite; automated testing aimed at the exact defect is what stands in for it.
- Same pipeline, no side door. The fix merged to the main branch and shipped the way every other change ships. A hotfix workflow that deploys around the pipeline trades one emergency for a standing one, and the team keeps no long-lived hotfix branches to drift.
- Rollback understood in advance. The change is a pure deletion of output, so the worst case of being wrong is a missing attribute and the rollback is one revert. That is also why it could be reviewed in minutes: the risk calculation fits in a sentence.
What kept 13 minutes from being reckless was not urgency. It was that every part of the process — pipeline, test suite, review — already existed and was applied at the smallest size the incident allowed. That is what actually reduces risk in an emergency: not more ceremony, less surface.
The siblings: same class, same shape
Two more fixes from the same integration, in the same year, make this a pattern rather than an anecdote.
- The warranty-date leak (June). The listing pipeline mapped a field named
yearto the watch's full warranty date, formatted%d/%m/%Y. This meant listings could carry a precise dealer-side warranty date where a manufacture year belonged. A warranty date is commercially revealing — it dates the sale — and useful raw material for fraudulent claims. The fix was +5/−4, a mapping correction, opened at 12:04 and merged at 15:16 the same day. - The double-post recovery (August). Listings could post twice to eBay after a sold sale was cancelled and the item re-entered posting criteria. That fix was +179/−15 plus a 43-line data migration. It added a unique index on active listings per item, a reconcile-before-create on conflict, and a cleanup of existing duplicates. It spent two days in review, including a round of requested changes.
The variable that sets review speed is not diff size
Lining the three fixes up shows a clear pattern.
| Fix | Diff | Open → merge |
|---|---|---|
| Serial number leak | +9/−2 | 13 minutes |
| Warranty date leak | +5/−4 | ~3 hours |
| Duplicate listings | +179/−15 (+43-line migration) | 2 days |
The tempting reading is "small diffs merge fast."
The accurate reading is this: review time followed the cost of being wrong, not the size of the change.
The serial fix is a pure removal of output. If it has unintended consequences, listings are missing one attribute. This outcome is strictly better than the incident it stops, and it is trivially reversible. Merging it in 13 minutes is not skipping review. It is correctly pricing a change whose worst case is milder than its own status quo.
The warranty fix changed a mapping, which created a slightly larger surface area and took a few hours.
The duplicate-listing fix changed concurrency behaviour and added a database invariant. Being wrong there creates incidents rather than trimming output. It earned two days and a requested-changes round from the same reviewers who merged the serial fix before their coffee cooled.
That asymmetry is the reusable rule. SWARECO's shorthand for it is to ask what the worst honest outcome of the change being wrong is. If the answer is "less data leaves the system," review for correctness of intent and ship. If the answer is "state gets created or mutated differently," slow down proportionally.
Teams that give every PR the same ceremony are over-reviewing their leak fixes, the one category of hotfix where hours genuinely matter, and under-thinking their invariant changes.
One closing habit from this incident: grep your integrations for fields your own schema calls secret. The listing builder did not defeat any protection to leak the serial, it just asked for the attribute, and the attribute answered. A field named secret_*, internal_* or private_* that appears anywhere in an outbound payload builder is a finding, every time, before any bug report exists.
Other Articles
We build the engineering. You build the business.
If you are trying to figure out whether SWARECO is the right fit for what you are building, the best way to find out is to talk. Tell us what you have. We will be direct about what we can do and how we would approach it.

.png)
