Between 6:30pm and 1am one night we iterated on a checkout redesign across 23 commits. The new presentation sat behind a ?prototype=1 query string, and the default page kept serving the old flow the whole time. That gave us a clean seam for internal review. It did not tell us the prototype was safe to expose publicly or safe to run against shared backend state.
The setup
The job was a from-scratch redesign of how an event organizer pays us. The old checkout put two different costs on one screen: the recurring monthly platform membership, and the one-time cost of running a single event. The new two-layer flow separates them, with a v2 event cart sitting alongside the legacy membership purchase.
Done the obvious way, that is a long-lived branch that drifts for a week and lands as one 3,000-line diff nobody can review. We wanted to build on the real routes, see it render with real data, and leave the live page alone.
So the first commit of the night, “prototype V2 checkout bar”, did exactly one thing. It gated the new bar behind ?prototype=1. Visit the page normally and you got the production flow. Append the flag and you got the new one.
Review happened on a preview branch with the flag on. The branch names show how it evolved: feature branch, then feature-prototype, then feature-prototype-v2. Both presentation paths lived in the same files and went through the same deployment pipeline. That cut the risk on the presentation side.
One visible change per commit
Once the flag was in, the log looked like this. Each line is its own commit, most a few minutes apart, none more than about half an hour:
- MOST POPULAR ribbon
- card hover lift
- graceful image fallback (show the category label instead of a broken icon)
- readable badge font, fix the monthly/annually toggle
- softer unpaid badges, empty state
Each commit is one decision you can see on screen. If the card hover felt wrong, we could revert that and nothing else. If the image fallback broke something, git bisect would land on it in one step instead of somewhere inside a 40-file mega-commit.
When you tune a UI by feel, you normally throw away the path and keep the final state. Committing every visible improvement keeps that exploration as a trail of small decisions. Three weeks later, when someone asks why the toggle is laid out the way it is, the answer is a commit, not a shrug.
A visual change still needs normal review and validation, though. “Looks right” was never our release criterion.
The shared total
The ribbon and the hover lift were the fun part and the easy part. The hard part was a commit we titled “hybrid checkout: legacy membership path + v2 event cart, real proration math, combined grand total.”
The new event cart could not replace the old membership purchase. It had to coexist with it. A customer might buy a monthly membership and pay for one event in the same transaction, and the grand total had to be right across both, with real proration math. That math fed the same shared total the legacy path computed. Whatever the new cart produced also had to hand off to the existing checkout endpoint, because that is where payment actually happens, and we were not going to reimplement the payment leg.
A query flag gives you a clean way to show two flows. It does nothing to make two flows share state correctly underneath. The new cart and the legacy path both feed one order, one total and one POST. Getting the combined grand total right while the old path kept working untouched is where the hours went. Everything visual took minutes.
When a prototype reaches the same endpoint as the default path, that endpoint is the risk. Define the expected writes and totals, test the relevant account states, and review the payment and billing boundary on purpose.
Ending the burst with an ADR
A prototype behind a flag has a known failure mode: it never graduates and never gets removed. A temporary path can become load-bearing without a decision record, an owner or an exit condition.
So we ended the burst by writing the product rules and an ADR. It covers account-state handling, capacity warnings, downgrade behavior and the architecture rationale. It also records the context, the decision, the alternatives considered, the rollout assumptions, and the conditions that would make us revisit the temporary path or the model.
Writing it while the reasoning was fresh turned exploratory work into a choice someone can review. The record should also name an owner, the exit conditions and a removal plan for the prototype path, so the flag does not outlive the memory of why it exists.
A query-string flag controls what customers see. The totals and writes that both versions share are on you. Before a prototype touches the same endpoint as the live flow, pin down the expected totals and writes, run the account states that matter, and give the flag an owner and a way out.