All cases
Live SaaS · 4 repositories · September 2026

Taking a single-country product international

The product had no concept of country, so customers abroad could not even enter their own phone number.

What we did not have

The product had no concept of country.

It was built for one market, and that assumption was baked into the code rather than written down. Country was stored on every account, but nothing ever read it. Phone numbers were formatted by a hard-coded pattern for a single country.

The problem

The company was opening the product to customers in other countries. The single-country assumption broke in three ways.

  • Numbers were displayed wrong. A local mobile number abroad came out in a shape that exists in no country at all.
  • Numbers could not be entered. The input field rewrote what users typed into the one supported format as they typed it. Customers abroad literally could not enter their own phone number.
  • Text messaging was offered where it could not be delivered. Users pressed send and waited for a message that was never going to arrive.

This was not cosmetic. The wrong numbers appeared on screens, reports and spreadsheet exports — and on printed documents that customers sign and keep.

How I solved it

  • Replaced the hard-coded pattern with the account's real country, using Google's libphonenumber and its 245 regions instead of hand-written patterns. The library is maintained as countries change their numbering; my regular expression would not be.
  • Made the rule honest about uncertainty. Format the number only when it is genuinely valid for that country. Otherwise show the digits exactly as stored and never guess a shape. Output that looks correct but is wrong costs far more than output that looks unformatted.
  • Formatted on the server, not in the browser. That keeps a large library out of the front-end bundle, and guarantees the screen and the printed document can never disagree, because the same code produced both.
  • Mirrored one helper across all four repositories with identical rules. They share a database, so they cannot be allowed to disagree about what a phone number is.
  • Kept two separate country lists — one for display format, one for messaging — even though they hold identical values today. They answer different questions and will drift apart. Merging them is the obvious simplification and would be the wrong call.
  • Stored each number in parts: what the user typed, the local format, the country, the extension and the international format. A future international messaging provider now needs no interface changes.
  • Made every fallback land on the original behaviour, so existing customers saw no change whatsoever. This was the largest risk in the work and drove most of the design.

The results

Adding a new country went from a code project to a one-line list entry.

Scope
4 repositories, 171 files, ~2,800 lines, 61 commits
Delivery
4 pull requests, all reviewed and merged the same day
Coverage
245 countries supported, 241 verified end to end
Performance cost
~1.3 ms per page — then cut ~18% by moving a lookup out of a loop
Verification
17 unit tests, plus ~40 screens checked by hand in a browser
Impact on existing customers
Zero change, by design

Follow-on capability shipped on the same work: automatic generation of valid, country-correct sample data, so sales demonstrations look native to the prospect's market.