Last write wins, and the customer loses
A defect came in that, on the surface, looked like an edge case. A customer had moved through a journey in more than one browser tab, and the details they’d entered in one place didn’t match what they saw later in the flow.
The obvious response is to treat it as a bug: reproduce it, patch the code path, close the ticket. That would have been reasonable. But when I looked at how the state was stored, it wasn’t really a bug in one code path. It was the design working exactly as it was built, for a product that had changed underneath it.
An assumption the product outgrew
The session model assumed that one browser meant one journey. Everything a customer was doing lived in a single session record, keyed to their browser. In the early product that was true, and it was simple and sensible.
But customers don’t behave like that any more. They open a second tab to compare. They start one journey, then another related one, then come back to the first. Every tab shares the same browser, so every tab reads and writes the same record. Whichever tab saved last wins, and the other tab’s state is quietly overwritten.
Looking further, it wasn’t the only symptom. Another investigation found a save that built a fresh object and replaced whatever was already in the session, without checking what was there first. Different trigger, same root cause: the model treated state as one thing to replace, when it had become several things that needed to live side by side.
Fixing the bug vs fixing the model
Patching each defect would have made each defect go away, and left the next one waiting. The question I wanted the team to answer was different: what should the state actually look like, given how customers really use the product?
The direction I proposed was to stop storing one current journey and store a collection, with each product or journey kept as its own keyed entry, and the page you’re on selecting the right one. It’s the same idea as a shopping cart: several things in progress at once, none of them overwriting the others. As a side effect, it also opens the door to features like comparison, which the old model couldn’t support.
We also looked at other options: going back to separate sessions per journey type, as an older part of the platform did, or merging updates into the existing record rather than replacing it. Merging reduces the damage but doesn’t remove the shared-record problem. The keyed model fixes the assumption itself. The final implementation is still being worked through, but the conversation moved from “fix this bug” to “change the model”, which is the part that mattered.
Raising a structural risk
Getting a team to invest in a model change is harder than getting a bug fixed. A few things helped:
- Connect the defects. One bug is an edge case. Two bugs with the same root cause is a pattern. Showing that link is what turns a patch into a design conversation.
- Walk through the sequence. “There might be concurrency issues” gets nodded at and forgotten. Stepping through exactly what a customer does in two tabs until the data ends up wrong gets attention.
- Bring options, not just a problem. Putting forward concrete alternatives with their trade-offs moves the discussion straight to choosing, instead of debating whether there’s a problem at all.
- Don’t blame the original design. It was right for the product it was built for. Saying that out loud keeps the people who built it on side.
The question that finds these
The question I now ask of any shared state is: what happens if two of these happen at once? Two tabs, two devices, two requests, two journeys. If the honest answer is “they share a record and we hope they don’t collide”, the next defect is already written.