A mobile recharge form looks deceptively simple:
- choose a country;
- enter a phone number;
- choose an amount;
- pay.
From an engineering perspective, however, the interesting part is everything that has to happen between steps two and four.
A reliable recharge flow needs to answer several different questions:
- Is the phone number structurally valid?
- Which country context applies?
- Is the number eligible for recharge?
- Which products are available right now?
- Is the price still valid?
- What happens if the user clicks Pay twice?
- What happens if a provider times out after accepting the request?
- How do we show a useful status without pretending that an asynchronous operation is instantaneous?
This article uses a generic prepaid recharge system as the example. The same design principles apply to many transaction flows where the user selects a destination, receives a quote, confirms it, and triggers an external operation.
1. Treat phone-number input as data normalization, not a text field
A common first implementation stores whatever the user typed:
07700 900123
+44 7700 900123
0044 7700 900123
Those may represent the same logical number.
The application should therefore distinguish between:
- the user's display input;
- normalized number data used internally.
A reasonable internal representation might be:
{
"country": "GB",
"calling_code": "44",
"national_number": "7700900123",
"normalized": "+447700900123"
}
Normalization should happen before downstream eligibility checks.
Do not make a giant custom regex your only validation mechanism. Phone numbering plans are more complicated than a handful of prefixes, and they change over time.
A phone-number library can help with parsing and structural validation, but even a valid-looking number is not necessarily rechargeable.
That is a separate question.
2. Separate syntax validation from recharge eligibility
These are different checks.
A number can be:
- syntactically valid;
- plausible for a country;
- assigned to a real subscriber;
- connected to a supported network;
- eligible for a specific recharge product.
A frontend validator can answer only some of those questions.
For example:
"Does this look like a valid UK mobile number?"
is not equivalent to:
"Can our current recharge provider deliver this £10 product to this number?"
Eligibility belongs closer to the product/provider layer.
A useful flow is:
User input
↓
Normalize number
↓
Structural validation
↓
Eligibility lookup
↓
Available products
Each stage should have its own failure message.
Invalid phone number is useful when parsing fails.
It is misleading when the number is perfectly valid but the operator is unsupported.
3. Do not rely on prefixes as your source of truth for the current operator
It is tempting to map number prefixes directly to carriers.
That can work as a hint.
It should not be treated as authoritative.
Mobile number portability means a number may move between networks while keeping the same number.
This matters because recharge products are normally operator-specific.
If your provider offers a lookup or eligibility endpoint, prefer that over hard-coded prefix assumptions.
A better model is:
{
"number": "+447700900123",
"operator": {
"id": "provider-operator-id",
"name": "Example Mobile"
},
"lookup_source": "provider",
"checked_at": "2026-09-08T16:00:00Z"
}
The timestamp matters because catalogue information is not permanent.
4. Generate a quote, not just a product selection
Once the user chooses a recharge product, create a quote snapshot.
Do not assume that the product record currently stored in your database will still describe the transaction later.
A quote should capture the commercial state the user is about to accept.
For example:
{
"quote_id": "q_123",
"recipient": "+447700900123",
"product_id": "prod_456",
"product_name": "10 GBP Airtime",
"recipient_value": "10.00",
"recipient_currency": "GBP",
"fee": "0.49",
"charge_total": "10.49",
"charge_currency": "GBP",
"expires_at": "2026-09-08T16:05:00Z"
}
The confirmation screen should render from the quote.
Not from several unrelated API responses.
Not from frontend state assembled during the previous five minutes.
The quote is the contract between product selection and payment.
5. Put a real confirmation boundary before execution
A good confirmation screen answers:
- Which phone number will receive the recharge?
- Which operator was identified?
- What exactly will the recipient receive?
- How much will the sender pay?
- Which currency is used?
- What fee is included?
This is particularly important for irreversible or difficult-to-reverse operations.
The final button should mean:
Execute this exact quoted transaction.
It should not mean:
Recalculate everything and then do whatever the newest API response suggests.
If a quote has expired, request a new quote and ask the user to confirm again.
That is slightly less convenient.
It is much safer.
6. Protect the execution endpoint with idempotency
Double-clicks happen.
Mobile browsers retry requests.
Reverse proxies retry requests.
Users refresh pages when they think something is stuck.
A transactional endpoint should assume that the same logical request can arrive more than once.
For example:
POST /recharges
Idempotency-Key: 7fc14a8c-...
On the server:
if idempotency_key already completed:
return previous result
if idempotency_key currently processing:
return current transaction state
otherwise:
create transaction
begin processing
The database should enforce the uniqueness rule, not just application code.
Without that protection, an impatient double-click can become two real top-ups.
7. Model processing as a state machine
Avoid representing the whole transaction with a single boolean such as:
success = true / false
External transaction systems have intermediate and ambiguous states.
A more useful model is:
created
quoted
payment_authorized
submitted
processing
succeeded
failed
You may need additional states depending on the payment and provider architecture.
The important property is that transitions are deliberate.
For example:
submitted → processing
processing → succeeded
processing → failed
but perhaps not:
succeeded → processing
without an explicit reconciliation operation.
State transitions should be logged.
8. Treat timeout as “unknown,” not automatically “failed”
This is one of the most important external-API lessons.
Suppose your application submits a recharge.
The provider processes it successfully.
The response is lost because the request times out.
Your server sees:
TimeoutException
What happened?
You do not know.
Retrying immediately may submit the same recharge twice unless the provider also supports idempotency.
The correct flow is usually:
request timed out
↓
mark transaction as uncertain/processing
↓
query provider status or reconcile
↓
retry only when safe
A network failure describes the communication channel.
It does not necessarily describe the business transaction.
9. Design the UI around honest states
Users do not need every internal state.
They do need truthful ones.
For example:
Processing
We have submitted the recharge and are waiting for final confirmation.
Completed
The provider confirmed successful delivery.
Failed
The recharge was not completed.
Avoid showing “Failed” simply because one HTTP request timed out.
Likewise, avoid showing “Completed” because payment succeeded if the recharge itself is still processing.
Payment status and fulfilment status are separate dimensions.
10. Keep an audit trail
For a transaction system, debugging from the current database row is not enough.
Useful events include:
quote_created
payment_authorized
recharge_submitted
provider_response_received
status_reconciled
recharge_completed
Store timestamps and relevant external identifiers.
This helps with:
- customer support;
- reconciliation;
- debugging;
- duplicate detection;
- provider disputes;
- operational monitoring.
Sensitive payment data should obviously not be dumped into logs.
Log identifiers and state changes, not secrets.
A safer recharge flow in one diagram
Conceptually:
Phone input
↓
Normalize
↓
Validate structure
↓
Check eligibility/operator
↓
Load supported products
↓
Create expiring quote
↓
User confirms
↓
Authorize payment
↓
Submit once with idempotency
↓
Processing / reconciliation
↓
Final status
None of these ideas is unique to mobile recharge.
The same pattern works for many transactional applications.
The broader lesson is that a safe flow separates:
input validation, eligibility, quoting, confirmation, execution, and final settlement.
When those responsibilities get compressed into one “Submit” handler, edge cases become expensive very quickly.
AI disclosure: This article was prepared with AI assistance. The publishing editor should review the technical examples and factual accuracy before publication.
Top comments (0)