<?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 (mobilerings).</description>
    <link>https://dev.to/mobilerings</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%2Forganization%2Fprofile_image%2F14686%2Ff343153e-e3cf-4b4e-ba56-40bde9122b3c.png</url>
      <title>DEV Community: MobileTopUP</title>
      <link>https://dev.to/mobilerings</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mobilerings"/>
    <language>en</language>
    <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>
