Skip to content

Build1 publisher3 min readPublished

Recharge platform schema keeps purchase price and recipient value in separate columns

A dev.to post on prepaid catalogues moves bundle allowances into their own rows, so a combo of data, minutes and SMS needs no new column. The nullable value field is gated on an explicit product type.

The Engineer · Build desk

Illustration accompanying Recharge platform schema keeps purchase price and recipient value in separate columns

What happened

  • The catalogue that breaks a single amount field includes 10 pounds of general airtime, 5 GB valid for 7 days, 500 SMS, 10 GB plus 100 minutes, and unlimited social data for 3 days.
  • Two products priced at 10 EUR deliver different value: 10 EUR of airtime in one case, 8 GB of data valid for 14 days in the other.
  • Product behaviour comes from an explicit product_type of airtime, data_bundle, voice_bundle, sms_bundle or combo_bundle, with the display name treated as presentation only.
  • A product_allowances table holds one row per allowance, so product 123 carries data 10 GB, voice 200 minutes and sms 100 messages as rows rather than columns.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • constraint With recipient_value null for bundles, any code answering what the recipient receives has to branch on product_type and read a second table, so the discriminator column is a correctness dependency.
  • capability Once validity is two typed fields, sorting bundles by duration and flagging short expiry are queries, not string parses over operator marketing copy.
  • cost Rendering a bundle now costs a join, and someone has to maintain the type taxonomy as operators invent new combinations.
  • decision Because providers rename SKUs and get replaced, the choice of internal identity has to be made before the second integration; afterwards it is a key migration instead of a mapping update.

Asking what the recipient gets has two shapes in this model, and one column decides which. For general airtime, the post's example sets purchase_amount to 10.49 EUR and recipient_value to 10.00 EUR [7]. For the data bundle at the same purchase price, recipient_value and recipient_currency are both null [8], and the value sits in product_allowances rows instead: 10 GB of data, 200 voice minutes and 100 SMS, all pointing at product 123 [13]. product_type is the column that tells the application which read is legal [10].

The two amounts on the airtime row differ by 0.49 EUR, which is 4.9 per cent of the 10.00 credited to the phone [19]. Separate columns are what make that figure reportable; a single amount field collapses it.

The post opens by rejecting both an enormous table full of nullable columns and an unstructured JSON blob that becomes impossible to query [3], then lands on a products table with two columns that are null for an entire product class [8]. The difference is the count and the gate. The sprawl it rejects grows data_amount, data_unit, voice_minutes, sms_count, social_data_amount and onward [12], while the normalized allowance table absorbs a new bundle shape without a new column [13].

The failure being fixed is inference from strings. The post's example of the wrong check is a test for whether the product name contains "GB" [9], and its rule is that the name stays presentation data while the type becomes application data [11]. Validity gets the same treatment: validity_value 30 and validity_unit day, instead of the number living inside description text such as "Awesome 10GB package valid for 30 days!" [14].

Identity is the part I would argue hardest for. Provider SKUs in the post look like ABC-UK-10, prod_847261 and sku_2291, and it says to keep those in external_product_id while products.id stays an internal UUID [16]. The listed reasons are all provider behaviour: renamed SKUs, migrated APIs, reused external conventions, duplicates returned across environments, and providers being replaced outright [17]. Operators get the same split, with external_operator_id beside an internal id under a countries-to-operators-to-products hierarchy [18].

Three conditions decide whether the extra table pays. Your catalogue contains combo bundles, or soon will; a catalogue of single-allowance products can keep data_amount and data_unit on the product row and skip the join. You integrate more than one provider, or expect to swap one, since internal identity matters most when the provider changes [17]. And you run queries against the catalogue: filtering, sorting bundles by duration and comparison logic are the operations the post names as the payoff [14]. The post presents schema shapes and example payloads, with no query timings or migration steps [20].

What to watch

  • Whether a follow-up spells out the multiple provider mappings per operator that the post defers to later.
  • Whether allowance unit gets a constrained list the way product_type does; comparing 500 MB against 5 GB depends on it.
  • Whether the post's argument about JSON columns, which its text breaks off mid-example, ends on a hybrid of typed columns plus JSON for provider extras.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories