Time, Money & Localization
Three data-handling rules that are cheap to follow from the start and corrupt data when they are not.
Time — store UTC, format at the edge
- Every timestamp is stored in UTC, and APIs return ISO-8601 with an offset (
2026-06-27T17:33:34Z). - Only the presentation layer converts to local time, with
Intl.DateTimeFormatordate-fns. - Never store a local time without its zone, and never infer the zone from the server's clock.
- A date with no time (a birthday, an invoice date) is a date, not a midnight timestamp. Storing it as one moves it across the date line for someone.
- For a future appointment, store the IANA zone (
Europe/London) alongside the instant — offsets change when governments change the rules.
Money — never floating point
- Store the smallest unit as an integer:
$10.50is1050.0.1 + 0.2 = 0.30000000000000004, and that error compounds through every calculation. - Kotlin:
BigDecimalor aMoneyvalue object. NeverDoubleorFloat. - Currency travels with the amount. A bare number is not a price.
- Format at the edge with
Intl.NumberFormat. Round once, at presentation, and define the rounding rule where money is split.
Language
- Do not hard-code user-facing strings where multi-language support is required. Extract to
en.jsonwithreact-i18next. - Never build a sentence by concatenating fragments — word order differs between languages. Use interpolation with named placeholders.
- Leave room in layouts: German runs roughly 30% longer than English.
Smells
A TIMESTAMP column with no zone, Double for a price, an amount with no currency, "Hello " + name + ", you have " + n + " items", a date shifting by a day for one user, toLocaleString() called in a domain function.