Time Zones and Dates: The Bugs That Keep Coming Back
Key takeaway
Store instants in UTC, but store future appointments as a local time plus a time zone identifier — because if a government changes the daylight saving rules, a meeting at 9am must still be at 9am. Getting that distinction right prevents most recurring date bugs.
Date bugs have a characteristic rhythm: everything works until the clocks change, or until a customer in another country complains, or until the last day of the month. They're avoidable, but only with rules applied consistently.
The core distinction
- An instant — something that happened — is a point on the timeline. Store it in UTC, convert for display.
- A future local commitment — an appointment, a recurring meeting, a delivery window — is a wall-clock time in a place. Store the local time and the IANA time zone identifier, and compute the instant when you need it.
- Storing a future appointment as UTC is the classic bug: when the daylight saving rules for that region change, every future booking shifts by an hour.
- A date without a time — a birthday, an invoice date — is a calendar date, not midnight in some time zone. Store it as a date.
Daylight saving will break your assumptions
- Some local times don't exist (the hour skipped in spring) and some occur twice (the hour repeated in autumn). Your booking system must handle both.
- A day is not always 24 hours, and adding 86,400 seconds is not the same as adding one day.
- Time zone rules change by legislation, sometimes with weeks of notice. Keep your time zone database updated as part of routine maintenance.
- Never store a fixed UTC offset in place of a zone identifier; "+05:30" loses the rule that produced it.
Recurring events
"Every Tuesday at 9am" is a rule, not a list of timestamps. Store the rule with its time zone and generate occurrences on demand, keeping explicit exceptions for instances that were moved or cancelled. Expanding a recurrence into thousands of stored rows means every rule change becomes a data migration.
Business time is its own problem
- Business days, working hours, and holidays are per country, per region, and sometimes per customer. Make the calendar data, not code.
- Service level agreements measured in business hours need that calendar to be correct historically, not just today.
- Month-end and year-end arithmetic has edge cases — adding one month to 31 January has no obvious answer, so define yours.
- Test with a fixed, injectable clock so date-dependent logic is testable at all, including across a daylight saving boundary.