DEV Community

Cover image for Building a Birthstone Lookup Into a Gift-Shop Checkout: An Engineering Walkthrough
Tea-sip for Lizely

Posted on

Building a Birthstone Lookup Into a Gift-Shop Checkout: An Engineering Walkthrough

If you run an online gift shop, you eventually hit a question that looks nothing like an engineering problem until it becomes one: "Can the customer just type their birthday and get the right birthstone shown next to the right product?" On paper, that is a one-line if statement. In practice, it is a small piece of data engineering — and the way you wire it determines whether your checkout page stays fast, your support inbox stays quiet, and your merchandising team can run campaigns next month without a redeploy.

This walkthrough covers the constraints I hit while building that flow for a small jewelry storefront. It is not a tutorial on gemstone lore, and it is not a comparison of lookup methods (that angle already exists in another post). Instead, it is the production view: where the data comes from, how to keep it sane, how to validate it, and how to make it survivable for the next person who maintains the code.

What the lookup actually has to answer

A birthstone lookup looks simple: given a month, return a stone. Behind that one return value sit at least four questions the code has to answer correctly every time:

  1. Which tradition? The U.S. traditional list, the modern list maintained by the American Gem Trade Association, the mystical list, the Ayurvedic list, and several regional variants all disagree in non-trivial ways. Tanzanite is December in the modern list but is not on the older lists at all.
  2. Which calendar? If the storefront serves an international audience, someone, somewhere, will type a date in DD/MM/YYYY. Parsing has to be explicit, not locale-dependent.
  3. What about edge cases? February 29 leap-day customers exist. Tibetan and some lunar calendars will shift the month entirely.
  4. How does this change? Trade bodies revise the lists occasionally. The data file should be replaceable without a code change.

Skipping any of these shows up as a customer-service ticket, not a bug report.

Picking the source of truth and keeping it auditable

I treat the canonical month-to-stone mapping as a separate artifact from the code. The shape that has aged best is a JSON file in the repo, one entry per month, with the stone name, an alternate name (so search matches "zodiac garnet" as well as "garnet"), and the source list it came from. Something like:

{
  "month": 1,
  "stones": [
    { "name": "Garnet", "alternate": "Almandine", "source": "traditional-modern" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Storing the source string per row is the cheapest insurance you can buy. When a customer emails asking why their stone is different from their grandmother's pendant, you do not have to guess — the row tells you which list you quoted.

For authoritative external references on the modern list, the Gemological Institute of America page on birthstones is the cleanest landing page that does not rot, and the birthstone article on Wikipedia is the best place to cross-check the traditional and modern tables side by side. Both pages have been stable for years and are easy to cite in a pull request when you add a new variant.

Validation rules I now apply by default

Three validation rules have saved me the most time. They are easy to forget on the first build and very expensive to retrofit once customers have relied on the wrong output.

  1. Every month has exactly one "primary" stone. A month must never return an empty array, and the first stone in the array is the default one for product display. Order in the JSON file is meaningful, and that is documented in a README line.
  2. Every stone name maps to at least one SKU in the catalog. If you sell garnet jewelry under multiple SKUs, store them as a list and require at least one. The lookup is allowed to return a stone name that the shop does not stock, but the front-end code that renders the "buy this birthstone" call-to-action has to handle that case without crashing.
  3. Date input is parsed with an explicit format string, never with new Date(string) in JavaScript or strptime without a format in Python. A birthday typed as 02/03/1990 is unambiguous only if you decide whether it is MM/DD or DD/MM before parsing. I default to ISO 8601 (YYYY-MM-DD) in the form and parse it directly.

The third rule is the one developers most often skip, because JavaScript and Python both happily parse 02/03/1990 into a valid Date that happens to be March 2. The ECMAScript specification on date time string format is explicit that the behavior is implementation-defined for non-ISO formats, which is the polite way of saying "you cannot rely on this." Push the format into a <input type="date"> field, post the value, and your server-side parser never has to guess.

A small, durable deployment shape

The piece of code that does the lookup should be a single function with a single responsibility: take a month integer, return the canonical stone entry. That function has one input source — the JSON file. It has one output shape — the JSON entry above. Everything else (HTTP handlers, templating, error pages) wraps around it.

The reason I am insistent about this shape: when the merchandising team wants to add the Ayurvedic stones for a Diwali campaign, the diff should be one PR that changes only the JSON. The function does not move. The tests do not move. The deployment script does not move. The campaign goes live behind a feature flag, and if it underperforms, you delete one JSON entry and redeploy. No rollback, no apology email.

For the customer-facing lookup widget itself, the lazy path is fine: do the lookup server-side, cache the response per month for the lifetime of the deploy, and let the page make one cheap request when a customer finishes typing the date. The widget you ship does not have to be the source of truth — it is a convenience over the structured data behind it. That separation is what lets you later expose the same data through a CSV export, an internal admin tool, or a birthstone guide for date-of-birth lookup without rewriting the lookup logic.

The failure modes worth designing for

A few things will go wrong, and it is much cheaper to decide now how the page should behave than to decide during the 11 p.m. incident.

  • The customer enters an invalid month. Your parser should never throw on this; it should fall back to "no birthstone match, browse our collection" and the front-end should not flash an error toast. Birthstones are an enhancement, not a hard requirement of checkout.
  • The JSON file fails to load. The lookup function returns null. The product page renders without the birthstone badge. A health check logs the file-load failure. Do not block the page render on this data.
  • A stone is renamed by the trade body (this happened with tanzanite being promoted in the late 20th century and could happen again). Because the JSON stores an alternate field, the previous name still resolves in search. Keep the old name in the array, do not delete it.
  • A new list variant is requested mid-sprint. If your lookup function takes an optional tradition parameter, this is a 20-line change. If it does not, this is a refactor across templates, tests, and the database.

A short adoption checklist

If you are wiring this into an existing storefront, this is the order I would do it in:

  1. Put the canonical mapping in a versioned JSON file in the repo, not in a database table. Files diff cleanly, and you can review the merchandising team's edits in a pull request.
  2. Write one unit test per month, asserting that the lookup returns a non-empty array. One per month is twelve tests, takes ten minutes to write, and catches every accidental edit.
  3. Add a contract test that walks every stone name and asserts a product SKU exists in the catalog. Run it in CI, not only locally.
  4. Render the birthstone badge on the product page only after the server has resolved the lookup. Never trust the browser to do this — it will be wrong on the first customer with an ad blocker that strips your JSON.
  5. Keep a "traditional" and a "modern" toggle in the data file. Do not ship the toggle in the UI until marketing asks for it. The toggle in the data file is free; the toggle in the UI is a redesign.

Frequently asked questions

How often does the canonical birthstone list actually change?

Real changes are rare — the modern list has been essentially stable since the Jewelers of America and the American Gem Trade Association consolidated it in the mid-20th century. Plan for a change every 10–20 years, not every quarter. The reason to design for change anyway is that regional and cultural variants (mystical, Ayurvedic, Tibetan) are added much more often, and you want those to be additions, not schema migrations.

Should the lookup live in the browser or on the server?

Server-side, with the result cached for the life of the deploy. The dataset is small and changes rarely, so edge caching is essentially free, and you avoid shipping a JSON file the browser can edit. The front-end only needs the resolved stone name and an image URL — keep the full mapping behind your origin.

What about customers born on February 29?

Treat February 29 as February for birthstone purposes unless your merchandising team explicitly tells you otherwise. There is no widely accepted "leap day stone." Document the choice in the JSON file as a comment-style field, and your future self will not have to re-derive it.

Do I need a separate table for SKUs and stones?

Not initially. Store the SKU list inside the same JSON entry until the JSON file gets uncomfortable to edit by hand (somewhere past ~5 KB is a reasonable threshold). At that point, promote the SKU mapping to its own file or table, but keep the month-to-stone core in the JSON. The reason this is the right order: the JSON is human-readable in a PR review, and the SKU table is not.


This article was drafted with AI assistance and reviewed for technical accuracy before publishing.

Top comments (0)