StoreServices Techie CornerAbout
← All Techie Corner posts Techie Corner · 02

The phone should never decide who paid

6 min read · State & APIs · by Imane

The bug arrived as a single, polite message from a user: "I redeemed my code but the app still says I'm locked. I had to close it and reopen it, then it worked."

That last sentence is the kind of thing that's easy to wave away. It worked in the end, didn't it? But "turn it off and on again" is almost never a fix — it's a symptom. When restarting an app makes a problem disappear, it usually means the app was holding onto an answer it should have thrown away. And in this case, the answer it was holding onto was one of the most important answers an app can have: has this person paid?

What was actually happening

My app stored the user's premium status locally, on the device, and trusted it. That's a reasonable-sounding optimisation: checking "are you premium?" on every screen by phoning a server would be slow and wasteful, so you cache the answer once and read the cache. Fast, cheap, simple.

The problem is what happens when the answer changes. When the user redeemed their promo code, the payment system — the thing that actually owns the truth about who has access — updated correctly. They were now premium. But my app was still reading its old, cached answer from before the redemption. Reality said "premium"; the app said "locked." The two had quietly drifted apart, and the user was stuck in the gap between them.

Restarting the app forced it to fetch a fresh answer on launch, which is why closing and reopening appeared to fix it. The restart wasn't fixing anything. It was just the only moment my app bothered to ask the question again.

The principle hiding underneath

This is the part worth taking away, because it's far bigger than my app: cached client state is a convenience, not a fact.

The device is a wonderful place to keep a copy of something for speed. It is a terrible place to keep the authoritative version of anything that matters — who paid, who has access, what's allowed, how much is left. Those things live on a server you control, or in a system like a payment provider that owns that truth on your behalf. The phone is allowed to hold a photocopy. It is not allowed to be the original.

The trap is that the photocopy works perfectly right up until the original changes. In development and in the simulator, you set premium once and it stays put, so the cache is always right and the bug is invisible. It only surfaces in the real world, in the exact moment a real person does the thing you most want them to do — pay you — and the app tells them it didn't happen.

The fix wasn't clever

I didn't need a sophisticated solution. I needed to make the app ask the right question at the right time. The rule I settled on is simple: anything that can change access has to re-check the source of truth at the moment it changes, not whenever the app happens to restart.

So immediately after a successful redemption, the app now refreshes the user's status directly from the payment provider and updates what the rest of the app sees — without waiting for a relaunch. The cache is still there for speed on every other screen. It's just no longer trusted to be correct across the one event that invalidates it.

Put plainly: the cache answers the routine questions instantly, and the source of truth gets consulted whenever the answer could have moved.

How to spot this in your own code

You don't need a payments feature to have this bug. The same shape shows up anywhere a client holds onto an answer that something else can change:

A few questions I now ask myself whenever I cache anything that matters. Where does the real answer live — this device, or somewhere else? If it's somewhere else, the local copy can go stale. What events can change this answer? A payment, a subscription expiring, an admin flipping a setting, another device making a change. Each of those is a moment you have to re-sync. And the tell-tale test: if restarting the app fixes it, what is the restart actually refreshing — and why isn't that happening on its own?

That last question is the one that catches the most bugs. Whenever a relaunch "fixes" something, there's almost always a piece of stale state that should have refreshed itself and didn't.

Why a one-line message mattered

It would have been easy to file this under "user restarted, all good." But one person taking the time to describe exactly what they did — redeem, then restart — handed me the whole diagnosis. The friction they hit at the moment of paying is precisely the friction you can least afford, and it's the kind that quietly costs you customers who don't write in. They just leave.

The lesson I keep coming back to: trust the system that owns the truth, let the device keep a fast copy, and re-sync the copy the instant the truth can change. Boring, like most things that work.

— Imane

← All Techie Corner posts