skip to content

A meeting is booked for 09:00 local time in Berlin, eleven months from now. Using Temporal, why is persisting the computed UTC instant the wrong choice, and what would you store instead?

level: seniorimportance: should knowfreq 40%

answer

  1. past and future are different facts
  2. zone rules are political data
  3. an offset is a cached result
  4. store the intent, derive late
  5. resolution can legitimately fail

basics

~20 s

Time-zone rules change, so a UTC instant computed today can stop meaning 09:00 in Berlin. Store the wall-clock value and the IANA zone ID — a Temporal.PlainDateTime plus 'Europe/Berlin' — and resolve to an instant when you need it.

solid answer

~50 s

An instant is the right representation for something that already happened, because the exact time is settled forever. A future local appointment is a different kind of fact: the user asserted a wall-clock time in a place, and the mapping from that to an instant is decided by tzdata rules that governments change — DST windows move, and countries abolish DST outright with months of notice. If you compute the instant today and store only that, a later rule change leaves your record pointing at 08:00 or 10:00 Berlin time, and nothing in the data records that it was ever meant to be 09:00. So store the intent: a `Temporal.PlainDateTime` (or `PlainDate` plus `PlainTime`) alongside the IANA zone ID `'Europe/Berlin'`, and call `toZonedDateTime('Europe/Berlin')` when you need to fire, display or compare it. Equivalently, persist the `ZonedDateTime.toString()` form, which carries both, and re-parse with `{ offset: 'prefer' }` so a stale offset is recomputed rather than trusted.

code

javascript · 12 lines
javascript
import { Temporal } from '@js-temporal/polyfill';

// Persist the intent, not the derived instant
const booking = { localDateTime: '2027-03-28T09:00', timeZone: 'Europe/Berlin' };

// Derive under whatever tzdata is current when you actually need it
const zdt = Temporal.PlainDateTime
  .from(booking.localDateTime)
  .toZonedDateTime(booking.timeZone, { disambiguation: 'reject' });

console.log(zdt.toString());             // 2027-03-28T09:00:00+02:00[Europe/Berlin]
console.log(zdt.toInstant().toString()); // 2027-03-28T07:00:00Z

go deeper

for a junior

Know that time-zone rules are set by governments and do change, so a UTC time computed today for a date next year may not still be 09:00 there. Store the local time together with the IANA zone name.

for a middle

Explain that an instant is a derived value for a future local appointment, name the Temporal call that derives it, and say why an IANA zone ID carries the rules while an offset only caches one result.

for a senior

Separate the two classes of temporal fact and defend the storage choice for each, including which zone owns a multi-participant event, how re-parse handles a stale offset, and why a resolution failure should be surfaced rather than silently absorbed.

for a principal

Own the data-model policy across services: which columns are instants and which are wall clock plus zone, how tzdata updates are rolled out and what reconciliation runs after one, and how recurrence rules are stored so a rule change does not require rewriting history.

## Two different kinds of temporal fact The decision turns on what the record actually asserts. "Payment captured" is an assertion about the physical timeline. It is finished; no legislature can move it. Its natural representation is a `Temporal.Instant`, stored as an epoch value or a `Z`-suffixed ISO string. Local rendering is a presentation concern applied at read time. "Meeting at 09:00 in Berlin next March" is an assertion about a clock in a place. The instant it corresponds to is a *derived* value, computed by applying the current tzdata rules to the stated wall-clock time. Deriving it early and throwing away the inputs is the mistake. ## Why the derived instant decays Time-zone rules are political data, not physics. They change several times a year somewhere in the world: DST start and end dates move, jurisdictions abolish DST, and standard offsets change. Announcements often come weeks or months before the change takes effect — well inside the booking horizon of a calendar, a payroll run, or an annual review. When the rules change, every stored instant that was derived under the old rules silently becomes a different wall-clock time. Nothing throws. Nothing logs. The meeting simply happens an hour early, and the only evidence is angry users. Recomputing is impossible after the fact, because the original wall-clock intent was discarded at write time. The same reasoning demotes a bare UTC offset. Storing `+01:00` records what the rules said at the moment you wrote the row; storing `Europe/Berlin` records *which rules apply*, so the value re-derives correctly under whatever tzdata the host has later. The zone ID is the durable fact; the offset is a cached result. ## What to store The workable pattern is to persist the intent and derive the instant on demand: ```js // stored const row = { localDateTime: '2027-03-28T09:00', timeZone: 'Europe/Berlin' }; // derived at read time, under whatever tzdata is current const zdt = Temporal.PlainDateTime .from(row.localDateTime) .toZonedDateTime(row.timeZone, { disambiguation: 'reject' }); const fireAt = zdt.toInstant(); // hand this to the scheduler, close to the event ``` Two details are load-bearing. First, the zone must be an IANA ID; a display label like "CET" is ambiguous and not a rule set. Second, resolution can fail: if a rule change lands the stored wall-clock time inside a DST gap it does not exist, and `{ disambiguation: 'reject' }` turns that into a `RangeError` you can surface, instead of silently moving the appointment by an hour. A `ZonedDateTime` string is the compact alternative because it carries both parts: ```js const stored = zdt.toString(); // '2027-03-28T09:00:00+02:00[Europe/Berlin]' Temporal.ZonedDateTime.from(stored, { offset: 'prefer' }); ``` The `offset` option is the crux on re-parse. The default for strings is `'reject'`, which throws when the embedded offset disagrees with what the zone now says. `'prefer'` keeps the offset while it remains valid and otherwise recomputes from the zone ID, preserving the wall-clock time the user chose — which is what a booking wants. `'use'` does the opposite: it trusts the offset and preserves the exact time, letting the local clock reading drift. `'ignore'` discards the offset entirely. ## Whose zone? A meeting has participants in several places, so "the zone" needs a definition. The usual rule is that one authoritative zone owns the event — typically the organiser's, or the venue's — and every other participant's view is a rendering of that single value. Storing each attendee's local time separately guarantees they diverge on the next rule change. Recurring events raise the same issue one level up. "Every weekday at 09:00 Berlin time" is a rule, not a list of instants. Materialising a year of occurrences into stored instants recreates the original problem for every one of them; store the rule plus the zone and expand it near the time. ## When the instant genuinely is the right thing This is not an argument against instants. Prefer an `Instant` when the fact is exact-time-shaped: audit logs, created/updated timestamps, message ordering, cache expiry, deadlines expressed as "90 minutes from now", and anything where two records must be comparable across zones. Those never need re-derivation, and storing them as wall-clock plus zone would add ambiguity for nothing. A mixed system usually ends up with both columns and an explicit rule about which fields are which — and that explicitness is the actual deliverable. The interviewer is checking whether you can articulate why the two cases differ, not whether you can recite an API.

  • Which events should still be stored as a Temporal.Instant?
    Anything whose fact is exact-time-shaped and already settled: audit and log entries, created/updated timestamps, message ordering, cache and token expiry, and relative deadlines such as "90 minutes from now". These never need re-derivation, are globally comparable without a zone, and storing them as wall clock plus zone would add ambiguity for no benefit.
  • A stored future wall-clock time lands in a DST gap after a rule change. What happens and what should the system do?
    The wall-clock reading no longer exists, so resolving it must choose. Passing `{ disambiguation: 'reject' }` throws a RangeError, which is the right default for bookings: surface the affected records and let a human or a documented policy move them. The default `'compatible'` would silently shift the appointment forward, with no audit trail.
  • Why is storing 'CET' or a +01:00 offset not equivalent to storing 'Europe/Berlin'?
    An offset is the result of applying rules at one moment, and an abbreviation like CET is ambiguous and not a rule set at all. Only the IANA ID identifies the rules, so only it lets a value re-derive correctly when the rules change or when you do calendar arithmetic across a DST boundary.
  • How would you handle a recurring weekly meeting under this model?
    Store the recurrence rule plus the owning zone, not a materialised list of instants. Expanding a year of occurrences into stored instants recreates the decay problem once per occurrence. Expand the rule to ZonedDateTimes shortly before each firing, resolving disambiguation at that point.

saying these in an interview costs you the question

  • Convert everything to UTC at write time and you are safe
  • Time-zone rules are stable enough to ignore
  • Storing the offset is equivalent to storing the zone ID
  • Each attendee's local time should be stored separately
  • A future appointment and an audit timestamp are the same kind of value

context