A payment looked successful. The wallet never updated.
On a mobile money flow the UI showed a spinner, then a success state. The provider timed out on their side. The user believed money had moved.
Support tickets followed. The bug was not bad code alone. We had designed for the happy path on a network that rarely offers one.
Mobile money on uneven networks
I have shipped fintech and mobility products in Ghana. Users rely on mobile data, shared devices, and payment rails that can stall without a clear failure signal.
Small payloads and fast first paint matter. So do honest transaction states.
Timeout looked like success
We treated idempotency as a backend detail. The user saw binary loading screens. Timeout and failure looked the same as success for too long.
Retries without keys risked duplicate charges. Support could not reconcile what the app had shown.
Status screens are product work
Idempotency keys and clear status transitions are product features. The user needs to know pending, failed, and succeeded without guessing.
Persist enough context to reconcile. Keep payloads small. Degrade when the network drops instead of pretending all is well.
Retry key and status payload
We moved to explicit states and a client-generated idempotency key on every charge attempt.
{
"idempotencyKey": "chg_8f2a9c",
"amount": 1200,
"currency": "GHS",
"status": "pending"
}Design for the network you have
Build for the network and payment conditions you have, not the diagram in a slide deck. Resilience shows up in UX first.