<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: MobileTopUP</title>
    <description>The latest articles on DEV Community by MobileTopUP (@mobiletopup).</description>
    <link>https://dev.to/mobiletopup</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4116101%2F4226e166-2ea9-4057-b119-5ecc9947abc0.png</url>
      <title>DEV Community: MobileTopUP</title>
      <link>https://dev.to/mobiletopup</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mobiletopup"/>
    <language>en</language>
    <item>
      <title>MobileTopUP: Designing a Reliable Recharge Transaction Workflow</title>
      <dc:creator>MobileTopUP</dc:creator>
      <pubDate>Mon, 28 Sep 2026 21:00:00 +0000</pubDate>
      <link>https://dev.to/mobiletopup/mobiletopup-designing-a-reliable-recharge-transaction-workflow-1m23</link>
      <guid>https://dev.to/mobiletopup/mobiletopup-designing-a-reliable-recharge-transaction-workflow-1m23</guid>
      <description>&lt;p&gt;Distributed transactions become interesting exactly when the happy path stops being reliable.&lt;/p&gt;

&lt;p&gt;A basic recharge implementation might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;charge card
    ↓
call recharge provider
    ↓
return success
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a local test environment, that can appear perfectly adequate.&lt;/p&gt;

&lt;p&gt;Production introduces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;duplicate requests;&lt;/li&gt;
&lt;li&gt;timeouts;&lt;/li&gt;
&lt;li&gt;delayed provider responses;&lt;/li&gt;
&lt;li&gt;asynchronous status updates;&lt;/li&gt;
&lt;li&gt;partial failures;&lt;/li&gt;
&lt;li&gt;process crashes;&lt;/li&gt;
&lt;li&gt;payment success followed by fulfilment uncertainty;&lt;/li&gt;
&lt;li&gt;users refreshing the page;&lt;/li&gt;
&lt;li&gt;workers retrying jobs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The underlying &lt;a href="https://mobilerings.net/mobile-top-up/" rel="noopener noreferrer"&gt;prepaid mobile top-up&lt;/a&gt; use case is a useful example because a single user action can involve payment, external fulfilment, asynchronous confirmation and an operation that should never be executed twice accidentally.&lt;/p&gt;

&lt;p&gt;The solution is not to add more &lt;code&gt;try/catch&lt;/code&gt; blocks.&lt;/p&gt;

&lt;p&gt;The transaction needs an explicit lifecycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a transaction record before external execution
&lt;/h2&gt;

&lt;p&gt;Do not wait until the provider responds successfully before creating a database record.&lt;/p&gt;

&lt;p&gt;Create the transaction first.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"txn_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recipient"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+447700900123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"quote_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"q_456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"idempotency_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every later operation has an internal identity.&lt;/p&gt;

&lt;p&gt;That identity can be used in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;logs;&lt;/li&gt;
&lt;li&gt;payment metadata;&lt;/li&gt;
&lt;li&gt;provider metadata;&lt;/li&gt;
&lt;li&gt;support tools;&lt;/li&gt;
&lt;li&gt;reconciliation jobs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A transaction that fails midway still exists.&lt;/p&gt;

&lt;p&gt;That is valuable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a state machine instead of a success flag
&lt;/h2&gt;

&lt;p&gt;A boolean cannot represent asynchronous processing well.&lt;/p&gt;

&lt;p&gt;This:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;success = null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;quickly becomes ambiguous.&lt;/p&gt;

&lt;p&gt;A state model is clearer.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;created
payment_pending
payment_authorized
submitted
processing
succeeded
failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depending on the architecture, you might also need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;payment_failed
cancelled
refunded
manual_review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact state names matter less than defining what each one means.&lt;/p&gt;

&lt;p&gt;For every state, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What must already have happened?&lt;/li&gt;
&lt;li&gt;Which transitions are allowed?&lt;/li&gt;
&lt;li&gt;Is the state terminal?&lt;/li&gt;
&lt;li&gt;Can a worker safely retry from here?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Idempotency belongs at the transaction boundary
&lt;/h2&gt;

&lt;p&gt;Imagine this request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /api/recharges
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user double-clicks.&lt;/p&gt;

&lt;p&gt;Or the frontend retries because the first response takes too long.&lt;/p&gt;

&lt;p&gt;Without protection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request 1 → recharge
Request 2 → recharge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The customer may pay twice or the recipient may receive duplicate value.&lt;/p&gt;

&lt;p&gt;Instead, require an idempotency key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Idempotency-Key: 70356c3a-...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store it with a unique database constraint.&lt;/p&gt;

&lt;p&gt;Pseudo-code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;existing = find_transaction(idempotency_key)

if existing:
    return existing

transaction = create_transaction(idempotency_key)
process(transaction)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The unique constraint matters because two concurrent application workers can otherwise both pass an application-level “does this exist?” check.&lt;/p&gt;

&lt;p&gt;Correctness should survive concurrency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not blindly retry an ambiguous provider request
&lt;/h2&gt;

&lt;p&gt;Suppose you send:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /provider/recharge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The provider completes the recharge.&lt;/p&gt;

&lt;p&gt;Your connection drops before the HTTP response arrives.&lt;/p&gt;

&lt;p&gt;From your application's perspective:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request timeout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the provider's perspective:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;recharge succeeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your retry policy says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;timeout → retry immediately
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you may create a duplicate.&lt;/p&gt;

&lt;p&gt;A timeout means:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We do not know the outcome.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is different from:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The operation failed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the provider supports idempotency, use it.&lt;/p&gt;

&lt;p&gt;If it provides transaction lookup, query the original request.&lt;/p&gt;

&lt;p&gt;If it sends asynchronous callbacks, wait for the callback within a reasonable reconciliation window.&lt;/p&gt;

&lt;p&gt;Retries need business context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give every provider operation a correlation identifier
&lt;/h2&gt;

&lt;p&gt;A useful integration keeps both sides traceable.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;internal transaction:
txn_123

provider request reference:
mobiletopup-txn_123

provider transaction:
ext_987
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store the provider transaction ID as soon as you receive it.&lt;/p&gt;

&lt;p&gt;Then support and reconciliation processes can move between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;your system ↔ provider system
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;without searching by amount and timestamp.&lt;/p&gt;

&lt;p&gt;Correlation IDs are cheap.&lt;/p&gt;

&lt;p&gt;Debugging without them is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate payment state from recharge state
&lt;/h2&gt;

&lt;p&gt;One of the most important modeling decisions is not to treat payment success as transaction success.&lt;/p&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment: authorized
Recharge: processing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a perfectly valid intermediate state.&lt;/p&gt;

&lt;p&gt;Likewise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment: failed
Recharge: not submitted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is very different from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Payment: captured
Recharge: provider rejected
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A transaction may therefore contain separate dimensions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"payment_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"captured"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fulfilment_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"processing"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The overall user-facing status can be derived from those states.&lt;/p&gt;

&lt;p&gt;Do not destroy useful information by compressing both into one column too early.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make asynchronous completion normal
&lt;/h2&gt;

&lt;p&gt;External fulfilment often works better as an asynchronous workflow.&lt;/p&gt;

&lt;p&gt;A simplified architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;API request
   ↓
create transaction
   ↓
authorize/capture payment
   ↓
enqueue recharge job
   ↓
provider submission
   ↓
processing
   ↓
webhook or polling
   ↓
final state
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API does not need to keep an HTTP connection open for the entire provider lifecycle.&lt;/p&gt;

&lt;p&gt;The frontend can poll:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /api/recharges/txn_123
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or receive updates through another mechanism.&lt;/p&gt;

&lt;p&gt;This also makes temporary provider slowness easier to absorb.&lt;/p&gt;

&lt;h2&gt;
  
  
  Webhooks need the same defensive engineering
&lt;/h2&gt;

&lt;p&gt;A webhook can arrive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;once;&lt;/li&gt;
&lt;li&gt;twice;&lt;/li&gt;
&lt;li&gt;out of order;&lt;/li&gt;
&lt;li&gt;much later than expected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat webhook processing as idempotent.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;provider event ID
+
unique constraint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the same event arrives twice, processing it twice should not produce duplicate side effects.&lt;/p&gt;

&lt;p&gt;Also validate the webhook's authenticity using the provider's supported verification mechanism.&lt;/p&gt;

&lt;p&gt;Do not trust an arbitrary public POST request simply because it contains a transaction ID.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add reconciliation even if webhooks exist
&lt;/h2&gt;

&lt;p&gt;Webhooks are useful.&lt;/p&gt;

&lt;p&gt;They are not magic.&lt;/p&gt;

&lt;p&gt;Events can be delayed or lost because of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;endpoint downtime;&lt;/li&gt;
&lt;li&gt;configuration mistakes;&lt;/li&gt;
&lt;li&gt;provider incidents;&lt;/li&gt;
&lt;li&gt;networking problems;&lt;/li&gt;
&lt;li&gt;internal processing bugs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A reconciliation worker can periodically find transactions such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status = processing
AND updated_at &amp;lt; now - threshold
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and query the provider.&lt;/p&gt;

&lt;p&gt;This gives the system a recovery path that does not depend on every asynchronous event arriving perfectly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store events, not only the latest state
&lt;/h2&gt;

&lt;p&gt;A row that currently says:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status = succeeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;does not explain how it got there.&lt;/p&gt;

&lt;p&gt;An event history might say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;12:00:01 transaction_created
12:00:05 payment_authorized
12:00:06 provider_submitted
12:00:07 provider_acknowledged
12:00:24 provider_processing
12:00:32 recharge_succeeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That timeline is enormously useful.&lt;/p&gt;

&lt;p&gt;You can implement it with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a dedicated transaction-events table;&lt;/li&gt;
&lt;li&gt;structured application events;&lt;/li&gt;
&lt;li&gt;or an appropriate event-sourcing approach where warranted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You do not need full event sourcing merely to maintain an audit trail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retries should be state-aware
&lt;/h2&gt;

&lt;p&gt;A retry policy such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;retry every exception three times
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is dangerous for financial or fulfilment operations.&lt;/p&gt;

&lt;p&gt;Instead, decide by operation type.&lt;/p&gt;

&lt;p&gt;A read-only provider lookup?&lt;/p&gt;

&lt;p&gt;Usually safe to retry.&lt;/p&gt;

&lt;p&gt;Submitting a recharge with provider idempotency?&lt;/p&gt;

&lt;p&gt;Potentially safe to retry using the same key.&lt;/p&gt;

&lt;p&gt;Submitting without provider idempotency after an ambiguous timeout?&lt;/p&gt;

&lt;p&gt;Reconcile first.&lt;/p&gt;

&lt;p&gt;Retry logic belongs to the business workflow, not just the HTTP client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design for support from day one
&lt;/h2&gt;

&lt;p&gt;Operational support will eventually need answers to questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did the payment complete?&lt;/li&gt;
&lt;li&gt;Was the recharge submitted?&lt;/li&gt;
&lt;li&gt;What external ID did the provider return?&lt;/li&gt;
&lt;li&gt;When was the last provider status check?&lt;/li&gt;
&lt;li&gt;Did a webhook arrive?&lt;/li&gt;
&lt;li&gt;Was the transaction retried?&lt;/li&gt;
&lt;li&gt;Which product snapshot was used?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If answering those questions requires reading raw production logs manually, the system is harder to operate than it needs to be.&lt;/p&gt;

&lt;p&gt;Build an internal transaction timeline.&lt;/p&gt;

&lt;p&gt;Even a simple one pays for itself quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reliability comes from reducing ambiguity
&lt;/h2&gt;

&lt;p&gt;The final architecture might resemble:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client
  ↓
Create transaction + idempotency key
  ↓
Payment
  ↓
Queue
  ↓
Provider submission
  ↓
Processing state
  ↓
Webhook / reconciliation
  ↓
Terminal status
  ↓
Audit history
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal is not to eliminate every failure.&lt;/p&gt;

&lt;p&gt;Distributed systems do not offer that luxury.&lt;/p&gt;

&lt;p&gt;The goal is to make every failure land in a state the system understands and can safely recover from.&lt;/p&gt;

&lt;p&gt;That is a much stronger guarantee than hoping the external API always returns &lt;code&gt;200 OK&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;AI disclosure:&lt;/strong&gt; This article was prepared with AI assistance. The publishing editor should review the implementation examples and factual accuracy before publication.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>architecture</category>
      <category>distributedsystems</category>
      <category>programming</category>
    </item>
    <item>
      <title>MobileTopUP: International Mobile Numbers — What Developers Should Validate and What They Shouldn’t Guess</title>
      <dc:creator>MobileTopUP</dc:creator>
      <pubDate>Mon, 21 Sep 2026 21:00:00 +0000</pubDate>
      <link>https://dev.to/mobiletopup/mobiletopup-international-mobile-numbers-what-developers-should-validate-and-what-they-shouldnt-10ab</link>
      <guid>https://dev.to/mobiletopup/mobiletopup-international-mobile-numbers-what-developers-should-validate-and-what-they-shouldnt-10ab</guid>
      <description>&lt;p&gt;Phone-number validation looks easy until an application becomes international.&lt;/p&gt;

&lt;p&gt;At first, the form may accept something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5551234567
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then international users arrive.&lt;/p&gt;

&lt;p&gt;Suddenly the system needs to understand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;07700 900123
+44 7700 900123
0044 7700 900123
(07700) 900 123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem becomes even more interesting when the number is not just a contact field but the destination of a transaction.&lt;/p&gt;

&lt;p&gt;A mobile recharge system is a good example because a syntactically valid number is not enough. The system may also need to determine whether the number is supported, which operator serves it, and which products can be delivered.&lt;/p&gt;

&lt;p&gt;The key engineering lesson is to avoid collapsing all of those checks into a single boolean called &lt;code&gt;valid&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  There is more than one kind of validity
&lt;/h2&gt;

&lt;p&gt;Consider these questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Can the input be parsed?&lt;/li&gt;
&lt;li&gt;Does it match a plausible numbering pattern?&lt;/li&gt;
&lt;li&gt;Is it possible within the selected country?&lt;/li&gt;
&lt;li&gt;Is it currently assigned?&lt;/li&gt;
&lt;li&gt;Which network currently serves it?&lt;/li&gt;
&lt;li&gt;Does the recharge provider support it?&lt;/li&gt;
&lt;li&gt;Which products are eligible?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Those are seven different questions.&lt;/p&gt;

&lt;p&gt;A typical frontend library may help answer the first few.&lt;/p&gt;

&lt;p&gt;It cannot necessarily answer the last four.&lt;/p&gt;

&lt;p&gt;This is why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is often a poor API model.&lt;/p&gt;

&lt;p&gt;A richer response is easier to reason about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"parseable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plausible"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"normalized"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+447700900123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"eligibility"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"supported"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"op_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Mobile"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Normalize once, format many times
&lt;/h2&gt;

&lt;p&gt;Internally, choose one canonical representation.&lt;/p&gt;

&lt;p&gt;For international mobile numbers, an E.164-style form is commonly useful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+447700900123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;E.164 numbers contain the country calling code and national significant number and are limited to 15 digits.&lt;/p&gt;

&lt;p&gt;The normalized form is useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;database uniqueness;&lt;/li&gt;
&lt;li&gt;provider APIs;&lt;/li&gt;
&lt;li&gt;logging identifiers;&lt;/li&gt;
&lt;li&gt;transaction records;&lt;/li&gt;
&lt;li&gt;comparisons.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The user-facing representation can be formatted differently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+44 7700 900123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is presentation.&lt;/p&gt;

&lt;p&gt;Do not store presentation formatting as identity.&lt;/p&gt;

&lt;p&gt;A good rule is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Normalize for machines. Format for humans.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Country context matters when parsing local input
&lt;/h2&gt;

&lt;p&gt;The input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;07700900123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;does not carry an explicit international country code.&lt;/p&gt;

&lt;p&gt;Your parser needs context.&lt;/p&gt;

&lt;p&gt;If the user selected United Kingdom, the application can interpret the local format in that context.&lt;/p&gt;

&lt;p&gt;If no country is known, guessing becomes dangerous.&lt;/p&gt;

&lt;p&gt;This is why many international phone forms work best with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Country selector
+
Phone number input
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of trying to infer everything from one text field.&lt;/p&gt;

&lt;p&gt;The selected country is parsing context. It should not automatically be treated as the user's physical location. This distinction becomes particularly important in &lt;a href="https://mobilerings.net/international-airtime/" rel="noopener noreferrer"&gt;international mobile top-up&lt;/a&gt;, where the sender may be in one country while the prepaid number being recharged belongs to another market.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not validate phone numbers with one homemade regex
&lt;/h2&gt;

&lt;p&gt;A regex can enforce simple syntax.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;^\+[1-9]\d{7,14}$
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can help check a normalized international representation.&lt;/p&gt;

&lt;p&gt;It cannot tell you whether a number is plausible for a specific country's numbering plan.&lt;/p&gt;

&lt;p&gt;It certainly cannot tell you whether the number currently belongs to a supported operator.&lt;/p&gt;

&lt;p&gt;A dedicated phone-number library is usually safer for parsing and country-specific structural checks.&lt;/p&gt;

&lt;p&gt;Regex still has a role.&lt;/p&gt;

&lt;p&gt;It just should not pretend to be a numbering-plan database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not guess the current operator from the prefix
&lt;/h2&gt;

&lt;p&gt;Historically, number ranges often indicated the original operator.&lt;/p&gt;

&lt;p&gt;That makes prefix tables tempting.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if number starts with X:
    operator = Carrier A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem is mobile number portability.&lt;/p&gt;

&lt;p&gt;A subscriber may move to another network while keeping the same number.&lt;/p&gt;

&lt;p&gt;A prefix can therefore be useful metadata without being authoritative current-routing information.&lt;/p&gt;

&lt;p&gt;If your business process needs the actual operator, use an authoritative lookup available through your provider or another appropriate source.&lt;/p&gt;

&lt;p&gt;This distinction matters in recharge systems because available products are commonly tied to the current receiving network.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parsing success does not prove ownership
&lt;/h2&gt;

&lt;p&gt;A perfectly formatted number can belong to someone else.&lt;/p&gt;

&lt;p&gt;Phone-number validation and phone-number verification are different processes.&lt;/p&gt;

&lt;p&gt;Validation asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Does this number look structurally valid?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Verification asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can this user prove control of this number?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Verification commonly requires a separate mechanism such as an OTP.&lt;/p&gt;

&lt;p&gt;Not every product flow requires ownership verification.&lt;/p&gt;

&lt;p&gt;For example, a user may legitimately be sending a recharge to a family member.&lt;/p&gt;

&lt;p&gt;The application should therefore decide intentionally whether it needs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;number validation;&lt;/li&gt;
&lt;li&gt;ownership verification;&lt;/li&gt;
&lt;li&gt;recipient eligibility.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not implement one and assume you have all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  A number can be valid but unsupported
&lt;/h2&gt;

&lt;p&gt;Suppose this number parses correctly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+XXXXXXXXXXXX
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The recharge provider may still respond:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"supported"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Possible reasons include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;unsupported country;&lt;/li&gt;
&lt;li&gt;unsupported operator;&lt;/li&gt;
&lt;li&gt;unsupported number type;&lt;/li&gt;
&lt;li&gt;provider coverage limitations;&lt;/li&gt;
&lt;li&gt;temporary catalogue availability.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The user message should reflect the actual problem.&lt;/p&gt;

&lt;p&gt;Bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Invalid phone number.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This number appears valid, but recharge is not currently available for this network.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Error semantics matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Store both normalized and relevant resolved data
&lt;/h2&gt;

&lt;p&gt;A recharge transaction might snapshot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recipient"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"input"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"07700 900123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"normalized"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+447700900123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"operator_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"op_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"operator_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Mobile"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do you need to store the original user input?&lt;/p&gt;

&lt;p&gt;Maybe.&lt;/p&gt;

&lt;p&gt;It can help with support and debugging.&lt;/p&gt;

&lt;p&gt;But downstream transaction logic should generally use the normalized form.&lt;/p&gt;

&lt;p&gt;Also consider whether the operator should be stored as a transaction snapshot.&lt;/p&gt;

&lt;p&gt;If operator metadata changes later, historical transactions should still describe what the application believed at execution time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate validation errors by layer
&lt;/h2&gt;

&lt;p&gt;A clean API can expose different failure classes.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NUMBER_PARSE_ERROR
NUMBER_NOT_PLAUSIBLE
COUNTRY_NOT_SUPPORTED
OPERATOR_NOT_SUPPORTED
NUMBER_NOT_ELIGIBLE
PROVIDER_LOOKUP_FAILED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes frontend messages clearer and observability much better.&lt;/p&gt;

&lt;p&gt;If every problem becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;400 Invalid number
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you lose information.&lt;/p&gt;

&lt;p&gt;Support teams lose information too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Never use client-side validation as the execution guard
&lt;/h2&gt;

&lt;p&gt;Frontend validation exists for user experience.&lt;/p&gt;

&lt;p&gt;Server-side validation exists for correctness.&lt;/p&gt;

&lt;p&gt;A malicious or simply outdated client can bypass browser checks.&lt;/p&gt;

&lt;p&gt;The execution path should validate the normalized number and current eligibility again before creating the final transaction.&lt;/p&gt;

&lt;p&gt;Especially when the number is the destination of something with monetary value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Think in layers
&lt;/h2&gt;

&lt;p&gt;A useful mental model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Raw input
   ↓
Parsing
   ↓
Normalization
   ↓
Structural plausibility
   ↓
Provider/operator lookup
   ↓
Recharge eligibility
   ↓
Product eligibility
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each layer answers a different question.&lt;/p&gt;

&lt;p&gt;Keeping those questions separate avoids both false confidence and confusing error messages.&lt;/p&gt;

&lt;p&gt;International phone numbers are not unusually difficult.&lt;/p&gt;

&lt;p&gt;They simply expose a broader software-design principle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do not use one validation flag to represent several independent business facts.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;&lt;strong&gt;AI disclosure:&lt;/strong&gt; This article was prepared with AI assistance. The publishing editor should verify the technical details and factual accuracy before publication.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>webdev</category>
      <category>mobile</category>
      <category>backend</category>
    </item>
    <item>
      <title>MobileTopUP: Modeling Airtime, Data, and Bundles Without a Messy Product Schema</title>
      <dc:creator>MobileTopUP</dc:creator>
      <pubDate>Mon, 14 Sep 2026 21:00:00 +0000</pubDate>
      <link>https://dev.to/mobilerings/mobiletopup-modeling-airtime-data-and-bundles-without-a-messy-product-schema-36p2</link>
      <guid>https://dev.to/mobilerings/mobiletopup-modeling-airtime-data-and-bundles-without-a-messy-product-schema-36p2</guid>
      <description>&lt;p&gt;One of the easiest mistakes in a recharge platform is assuming that every product can be represented as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;amount = 10
currency = GBP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works beautifully until the catalogue contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;£10 general airtime;&lt;/li&gt;
&lt;li&gt;5 GB valid for 7 days;&lt;/li&gt;
&lt;li&gt;20 GB valid for 30 days;&lt;/li&gt;
&lt;li&gt;100 local minutes;&lt;/li&gt;
&lt;li&gt;500 SMS;&lt;/li&gt;
&lt;li&gt;10 GB + 100 minutes;&lt;/li&gt;
&lt;li&gt;unlimited social data for 3 days;&lt;/li&gt;
&lt;li&gt;operator-specific promotional bundles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, &lt;code&gt;amount&lt;/code&gt; is no longer a product model.&lt;/p&gt;

&lt;p&gt;It is one attribute among many.&lt;/p&gt;

&lt;p&gt;This article looks at a practical way to model operator-provided prepaid products without creating either an enormous table full of nullable columns or an unstructured JSON blob that becomes impossible to query.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start by separating commercial value from included allowance
&lt;/h2&gt;

&lt;p&gt;Consider these two products:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Product A
Price: 10 EUR
Recipient receives: 10 EUR airtime

Product B
Price: 10 EUR
Recipient receives: 8 GB data for 14 days
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The purchase price is the same.&lt;/p&gt;

&lt;p&gt;The product value is not.&lt;/p&gt;

&lt;p&gt;A schema should therefore avoid assuming that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;purchase_amount == recipient_amount
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A useful base model might begin with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;products
--------
id
provider_id
external_product_id
operator_id
country_code
product_type
display_name
purchase_amount
purchase_currency
recipient_value
recipient_currency
active
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For general airtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"product_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"airtime"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"purchase_amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.49"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"purchase_currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recipient_value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recipient_currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a data bundle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"product_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"data_bundle"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"purchase_amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.49"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"purchase_currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EUR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recipient_value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recipient_currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bundle needs a different representation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use an explicit product type
&lt;/h2&gt;

&lt;p&gt;Do not infer product behaviour from its name.&lt;/p&gt;

&lt;p&gt;Bad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if product.name contains "GB":
    product is data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;product_type:
- airtime
- data_bundle
- voice_bundle
- sms_bundle
- combo_bundle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your exact taxonomy may differ, but it should be explicit.&lt;/p&gt;

&lt;p&gt;This improves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;filtering;&lt;/li&gt;
&lt;li&gt;validation;&lt;/li&gt;
&lt;li&gt;analytics;&lt;/li&gt;
&lt;li&gt;UI rendering;&lt;/li&gt;
&lt;li&gt;reporting;&lt;/li&gt;
&lt;li&gt;tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The product name remains presentation data.&lt;/p&gt;

&lt;p&gt;The type becomes application data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model allowances separately
&lt;/h2&gt;

&lt;p&gt;A combined bundle may contain more than one allowance. A practical example of why this distinction matters can be seen in &lt;a href="https://mobilerings.net/mobile-data/" rel="noopener noreferrer"&gt;mobile data bundles&lt;/a&gt;, where data allowance, validity and operator-specific conditions need to be represented separately from general airtime.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10 GB data
200 voice minutes
100 SMS
valid for 30 days
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trying to fit that into the &lt;code&gt;products&lt;/code&gt; table quickly becomes ugly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data_amount
data_unit
voice_minutes
sms_count
social_data_amount
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A normalized allowance model is more flexible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;product_allowances
------------------
id
product_id
allowance_type
amount
unit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;product_id | allowance_type | amount | unit
------------------------------------------------
123        | data           | 10     | GB
123        | voice          | 200    | minute
123        | sms            | 100    | message
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the application can render:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;10 GB data
200 minutes
100 SMS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;without adding a new database column for every future bundle type.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validity deserves its own fields
&lt;/h2&gt;

&lt;p&gt;Validity is core product behaviour.&lt;/p&gt;

&lt;p&gt;It should not be buried only inside description text such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Awesome 10GB package valid for 30 days!"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store it structurally.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;validity_value = 30
validity_unit = day
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"validity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"day"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This enables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sorting bundles by duration;&lt;/li&gt;
&lt;li&gt;warning users about short validity;&lt;/li&gt;
&lt;li&gt;consistent formatting;&lt;/li&gt;
&lt;li&gt;analytics;&lt;/li&gt;
&lt;li&gt;comparison logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some products may not provide a meaningful validity period.&lt;/p&gt;

&lt;p&gt;Make that state explicit rather than pretending every product has one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep provider identity separate from internal identity
&lt;/h2&gt;

&lt;p&gt;External providers often have their own IDs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ABC-UK-10
prod_847261
sku_2291
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not make those your primary application key.&lt;/p&gt;

&lt;p&gt;Your application should own its identity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;products.id = internal UUID
products.external_product_id = provider identifier
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because providers can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;rename SKUs;&lt;/li&gt;
&lt;li&gt;migrate APIs;&lt;/li&gt;
&lt;li&gt;reuse external conventions;&lt;/li&gt;
&lt;li&gt;return duplicates across environments;&lt;/li&gt;
&lt;li&gt;be replaced.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Internal IDs should remain stable even if the integration changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The operator is part of the product context
&lt;/h2&gt;

&lt;p&gt;A data package is usually not globally valid simply because it says “5 GB.”&lt;/p&gt;

&lt;p&gt;It belongs to an operator and market context.&lt;/p&gt;

&lt;p&gt;A basic relationship might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;countries
    ↓
operators
    ↓
products
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The operator itself should also have an external mapping.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;operators
---------
id
country_code
name
provider_id
external_operator_id
active
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets you distinguish internal operator identity from provider-specific representation.&lt;/p&gt;

&lt;p&gt;If two providers support the same operator, you can later model multiple provider mappings without duplicating your entire conceptual catalogue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoid using JSON for everything
&lt;/h2&gt;

&lt;p&gt;JSON columns are tempting.&lt;/p&gt;

&lt;p&gt;You can store:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"anything"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"the provider returns"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and ship the feature quickly.&lt;/p&gt;

&lt;p&gt;The problem appears later when you need queries such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Find all active data bundles
with at least 5 GB
valid for at least 14 days
for operator X.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Structured fields are much easier to work with.&lt;/p&gt;

&lt;p&gt;A good compromise is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;structured columns/tables for business-critical attributes;&lt;/li&gt;
&lt;li&gt;JSON for sparse provider-specific metadata.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;products.metadata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;might safely contain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider_label"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Promo Summer 10GB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"campaign_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SUMMER26"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while product type, allowance, currency and validity stay queryable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve the raw provider payload
&lt;/h2&gt;

&lt;p&gt;Structured data and raw data solve different problems.&lt;/p&gt;

&lt;p&gt;When ingesting a provider catalogue, it can be useful to retain the original payload separately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;provider_product_snapshots
--------------------------
id
provider_id
external_product_id
payload_json
received_at
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is valuable when debugging:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why did this product suddenly become inactive?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;or:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Did the provider actually send a different validity period yesterday?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The raw snapshot is evidence.&lt;/p&gt;

&lt;p&gt;Your normalized tables are application state.&lt;/p&gt;

&lt;p&gt;Do not confuse the two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catalogue ingestion should be idempotent too
&lt;/h2&gt;

&lt;p&gt;Provider catalogues are often refreshed repeatedly.&lt;/p&gt;

&lt;p&gt;An importer should not create a new product every time it sees the same external SKU.&lt;/p&gt;

&lt;p&gt;A typical upsert key might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(provider_id, external_product_id)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if exists:
    update normalized fields
else:
    create product
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You also need a strategy for products that disappear.&lt;/p&gt;

&lt;p&gt;Options include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;mark inactive after a successful full catalogue sync;&lt;/li&gt;
&lt;li&gt;use provider-specific deletion signals;&lt;/li&gt;
&lt;li&gt;expire products not seen for a defined period.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not simply delete records immediately.&lt;/p&gt;

&lt;p&gt;Historical transactions may still reference them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Transactions should snapshot the purchased product
&lt;/h2&gt;

&lt;p&gt;This is critical.&lt;/p&gt;

&lt;p&gt;Imagine a customer buys:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;5 GB
valid for 30 days
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tomorrow, the provider changes the same SKU to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4 GB
valid for 30 days
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your transaction history dynamically joins the current product table, yesterday's receipt may appear to change.&lt;/p&gt;

&lt;p&gt;Instead, snapshot the relevant product attributes at purchase time.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transaction_product"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5 GB Data"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"data_bundle"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"allowances"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GB"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"validity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"day"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Historical transactions should describe what was purchased then.&lt;/p&gt;

&lt;p&gt;Not what the catalogue says now.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical model
&lt;/h2&gt;

&lt;p&gt;The result can stay relatively small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;providers
countries
operators
products
product_allowances
provider_product_snapshots
transactions
transaction_product_snapshots
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You do not need fifty tables.&lt;/p&gt;

&lt;p&gt;You also do not need to store the entire business model inside one &lt;code&gt;products.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The core idea is to separate:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;identity&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;from&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;commercial price&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;from&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;recipient value&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;from&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;allowances&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;from&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;validity&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;from&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;provider-specific metadata&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Once those concepts are separate, airtime and complex bundles can live in the same catalogue without pretending they are the same product.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;AI disclosure:&lt;/strong&gt; This article was prepared with AI assistance. The publishing editor should review the schema examples and factual accuracy before publication.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>database</category>
      <category>backend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>MobileTopUP: Designing a Safer Mobile Recharge Flow</title>
      <dc:creator>MobileTopUP</dc:creator>
      <pubDate>Tue, 08 Sep 2026 16:29:03 +0000</pubDate>
      <link>https://dev.to/mobilerings/mobiletopup-designing-a-safer-mobile-recharge-flow-40bp</link>
      <guid>https://dev.to/mobilerings/mobiletopup-designing-a-safer-mobile-recharge-flow-40bp</guid>
      <description>&lt;p&gt;A mobile recharge form looks deceptively simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;choose a country;&lt;/li&gt;
&lt;li&gt;enter a phone number;&lt;/li&gt;
&lt;li&gt;choose an amount;&lt;/li&gt;
&lt;li&gt;pay.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From an engineering perspective, however, the interesting part is everything that has to happen between steps two and four.&lt;/p&gt;

&lt;p&gt;A reliable recharge flow needs to answer several different questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the phone number structurally valid?&lt;/li&gt;
&lt;li&gt;Which country context applies?&lt;/li&gt;
&lt;li&gt;Is the number eligible for recharge?&lt;/li&gt;
&lt;li&gt;Which products are available right now?&lt;/li&gt;
&lt;li&gt;Is the price still valid?&lt;/li&gt;
&lt;li&gt;What happens if the user clicks Pay twice?&lt;/li&gt;
&lt;li&gt;What happens if a provider times out after accepting the request?&lt;/li&gt;
&lt;li&gt;How do we show a useful status without pretending that an asynchronous operation is instantaneous?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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. For additional product context, this is how the user-facing &lt;a href="https://mobilerings.net/how-to-recharge/" rel="noopener noreferrer"&gt;mobile recharge flow&lt;/a&gt; is structured.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Treat phone-number input as data normalization, not a text field
&lt;/h2&gt;

&lt;p&gt;A common first implementation stores whatever the user typed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;07700 900123
+44 7700 900123
0044 7700 900123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those may represent the same logical number.&lt;/p&gt;

&lt;p&gt;The application should therefore distinguish between:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the user's display input;&lt;/li&gt;
&lt;li&gt;normalized number data used internally.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A reasonable internal representation might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"calling_code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"44"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"national_number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"7700900123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"normalized"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+447700900123"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normalization should happen before downstream eligibility checks.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;A phone-number library can help with parsing and structural validation, but even a valid-looking number is not necessarily rechargeable.&lt;/p&gt;

&lt;p&gt;That is a separate question.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Separate syntax validation from recharge eligibility
&lt;/h2&gt;

&lt;p&gt;These are different checks.&lt;/p&gt;

&lt;p&gt;A number can be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;syntactically valid;&lt;/li&gt;
&lt;li&gt;plausible for a country;&lt;/li&gt;
&lt;li&gt;assigned to a real subscriber;&lt;/li&gt;
&lt;li&gt;connected to a supported network;&lt;/li&gt;
&lt;li&gt;eligible for a specific recharge product.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A frontend validator can answer only some of those questions.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Does this look like a valid UK mobile number?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is not equivalent to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Can our current recharge provider deliver this £10 product to this number?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eligibility belongs closer to the product/provider layer.&lt;/p&gt;

&lt;p&gt;A useful flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User input
   ↓
Normalize number
   ↓
Structural validation
   ↓
Eligibility lookup
   ↓
Available products
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each stage should have its own failure message.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Invalid phone number&lt;/code&gt; is useful when parsing fails.&lt;/p&gt;

&lt;p&gt;It is misleading when the number is perfectly valid but the operator is unsupported.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Do not rely on prefixes as your source of truth for the current operator
&lt;/h2&gt;

&lt;p&gt;It is tempting to map number prefixes directly to carriers.&lt;/p&gt;

&lt;p&gt;That can work as a hint.&lt;/p&gt;

&lt;p&gt;It should not be treated as authoritative.&lt;/p&gt;

&lt;p&gt;Mobile number portability means a number may move between networks while keeping the same number.&lt;/p&gt;

&lt;p&gt;This matters because recharge products are normally operator-specific.&lt;/p&gt;

&lt;p&gt;If your provider offers a lookup or eligibility endpoint, prefer that over hard-coded prefix assumptions.&lt;/p&gt;

&lt;p&gt;A better model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"number"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+447700900123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"provider-operator-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Mobile"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"lookup_source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"checked_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-08T16:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The timestamp matters because catalogue information is not permanent.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Generate a quote, not just a product selection
&lt;/h2&gt;

&lt;p&gt;Once the user chooses a recharge product, create a quote snapshot.&lt;/p&gt;

&lt;p&gt;Do not assume that the product record currently stored in your database will still describe the transaction later.&lt;/p&gt;

&lt;p&gt;A quote should capture the commercial state the user is about to accept.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"quote_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"q_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recipient"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"+447700900123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"product_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prod_456"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"product_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10 GBP Airtime"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recipient_value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"recipient_currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GBP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fee"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.49"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"charge_total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.49"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"charge_currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GBP"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expires_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-09-08T16:05:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The confirmation screen should render from the quote.&lt;/p&gt;

&lt;p&gt;Not from several unrelated API responses.&lt;/p&gt;

&lt;p&gt;Not from frontend state assembled during the previous five minutes.&lt;/p&gt;

&lt;p&gt;The quote is the contract between product selection and payment.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Put a real confirmation boundary before execution
&lt;/h2&gt;

&lt;p&gt;A good confirmation screen answers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which phone number will receive the recharge?&lt;/li&gt;
&lt;li&gt;Which operator was identified?&lt;/li&gt;
&lt;li&gt;What exactly will the recipient receive?&lt;/li&gt;
&lt;li&gt;How much will the sender pay?&lt;/li&gt;
&lt;li&gt;Which currency is used?&lt;/li&gt;
&lt;li&gt;What fee is included?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is particularly important for irreversible or difficult-to-reverse operations.&lt;/p&gt;

&lt;p&gt;The final button should mean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Execute this exact quoted transaction.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It should not mean:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Recalculate everything and then do whatever the newest API response suggests.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If a quote has expired, request a new quote and ask the user to confirm again.&lt;/p&gt;

&lt;p&gt;That is slightly less convenient.&lt;/p&gt;

&lt;p&gt;It is much safer.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Protect the execution endpoint with idempotency
&lt;/h2&gt;

&lt;p&gt;Double-clicks happen.&lt;/p&gt;

&lt;p&gt;Mobile browsers retry requests.&lt;/p&gt;

&lt;p&gt;Reverse proxies retry requests.&lt;/p&gt;

&lt;p&gt;Users refresh pages when they think something is stuck.&lt;/p&gt;

&lt;p&gt;A transactional endpoint should assume that the same logical request can arrive more than once.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;POST /recharges
Idempotency-Key: 7fc14a8c-...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if idempotency_key already completed:
    return previous result

if idempotency_key currently processing:
    return current transaction state

otherwise:
    create transaction
    begin processing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The database should enforce the uniqueness rule, not just application code.&lt;/p&gt;

&lt;p&gt;Without that protection, an impatient double-click can become two real top-ups.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Model processing as a state machine
&lt;/h2&gt;

&lt;p&gt;Avoid representing the whole transaction with a single boolean such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;success = true / false
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;External transaction systems have intermediate and ambiguous states.&lt;/p&gt;

&lt;p&gt;A more useful model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;created
quoted
payment_authorized
submitted
processing
succeeded
failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You may need additional states depending on the payment and provider architecture.&lt;/p&gt;

&lt;p&gt;The important property is that transitions are deliberate.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;submitted → processing
processing → succeeded
processing → failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;but perhaps not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;succeeded → processing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;without an explicit reconciliation operation.&lt;/p&gt;

&lt;p&gt;State transitions should be logged.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Treat timeout as “unknown,” not automatically “failed”
&lt;/h2&gt;

&lt;p&gt;This is one of the most important external-API lessons.&lt;/p&gt;

&lt;p&gt;Suppose your application submits a recharge.&lt;/p&gt;

&lt;p&gt;The provider processes it successfully.&lt;/p&gt;

&lt;p&gt;The response is lost because the request times out.&lt;/p&gt;

&lt;p&gt;Your server sees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TimeoutException
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happened?&lt;/p&gt;

&lt;p&gt;You do not know.&lt;/p&gt;

&lt;p&gt;Retrying immediately may submit the same recharge twice unless the provider also supports idempotency.&lt;/p&gt;

&lt;p&gt;The correct flow is usually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request timed out
      ↓
mark transaction as uncertain/processing
      ↓
query provider status or reconcile
      ↓
retry only when safe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A network failure describes the communication channel.&lt;/p&gt;

&lt;p&gt;It does not necessarily describe the business transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Design the UI around honest states
&lt;/h2&gt;

&lt;p&gt;Users do not need every internal state.&lt;/p&gt;

&lt;p&gt;They do need truthful ones.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Processing&lt;/strong&gt;&lt;br&gt;
We have submitted the recharge and are waiting for final confirmation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Completed&lt;/strong&gt;&lt;br&gt;
The provider confirmed successful delivery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failed&lt;/strong&gt;&lt;br&gt;
The recharge was not completed.&lt;/p&gt;

&lt;p&gt;Avoid showing “Failed” simply because one HTTP request timed out.&lt;/p&gt;

&lt;p&gt;Likewise, avoid showing “Completed” because payment succeeded if the recharge itself is still processing.&lt;/p&gt;

&lt;p&gt;Payment status and fulfilment status are separate dimensions.&lt;/p&gt;
&lt;h2&gt;
  
  
  10. Keep an audit trail
&lt;/h2&gt;

&lt;p&gt;For a transaction system, debugging from the current database row is not enough.&lt;/p&gt;

&lt;p&gt;Useful events include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;quote_created
payment_authorized
recharge_submitted
provider_response_received
status_reconciled
recharge_completed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store timestamps and relevant external identifiers.&lt;/p&gt;

&lt;p&gt;This helps with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;customer support;&lt;/li&gt;
&lt;li&gt;reconciliation;&lt;/li&gt;
&lt;li&gt;debugging;&lt;/li&gt;
&lt;li&gt;duplicate detection;&lt;/li&gt;
&lt;li&gt;provider disputes;&lt;/li&gt;
&lt;li&gt;operational monitoring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sensitive payment data should obviously not be dumped into logs.&lt;/p&gt;

&lt;p&gt;Log identifiers and state changes, not secrets.&lt;/p&gt;

&lt;h2&gt;
  
  
  A safer recharge flow in one diagram
&lt;/h2&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of these ideas is unique to mobile recharge.&lt;/p&gt;

&lt;p&gt;The same pattern works for many transactional applications.&lt;/p&gt;

&lt;p&gt;The broader lesson is that a safe flow separates:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;input validation, eligibility, quoting, confirmation, execution, and final settlement.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When those responsibilities get compressed into one “Submit” handler, edge cases become expensive very quickly.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;AI disclosure:&lt;/strong&gt; This article was prepared with AI assistance. The publishing editor should review the technical examples and factual accuracy before publication.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>architecture</category>
      <category>backend</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
