skip to content

Daylight Saving Time Handling

The spring-forward gap and the fall-back overlap mean some local times do not exist and others happen twice, which is why the withEarlierOffsetAtOverlap family exists. It is also why adding 24 hours and adding one day can give different answers.

part ofJavaoverview, primer and where to startread it →
on this pageshow

questions

4

What is the spring-forward gap in DST, and what happens in java.time when you create a ZonedDateTime at a local time that does not exist?

level: juniorimportance: must knowfreq 62%

answer

  1. Spring-forward = clock jumps, an hour vanishes
  2. Gap local times do not exist as an instant
  3. java.time shifts forward by the gap, never throws
  4. Detect via ZoneRules.getTransition(...).isGap()
  5. 02:30 Berlin -> 03:30 +02:00

basics

~20 s

In spring, clocks jump forward (e.g. 02:00 to 03:00), so an hour of local time never happens. If you ask java.time for a ZonedDateTime in that missing hour, it pushes the time forward by the gap (the missing hour) instead of failing.

solid answer

~40 s

Daylight Saving Time means clocks shift twice a year. In spring-forward, the clock skips an hour: at 02:00 it jumps to 03:00, so local times like 02:30 do not exist on that day. When you build a ZonedDateTime for a non-existent local time, java.time does not throw. By default it resolves the gap by shifting the instant forward by the length of the gap (usually one hour) and applying the later offset, so 02:30 becomes 03:30 in the new offset. This is documented behavior of ZonedDateTime.of and ofLocal: in a gap the local time is adjusted forward; in an overlap it picks the earlier offset. Knowing this prevents off-by-one-hour bugs when scheduling near the spring transition.

code

java · 7 lines
java
ZoneId berlin = ZoneId.of("Europe/Berlin");
LocalDateTime missing = LocalDateTime.parse("2024-03-31T02:30");
ZonedDateTime z = missing.atZone(berlin);
System.out.println(z); // 2024-03-31T03:30+02:00[Europe/Berlin]

var t = berlin.getRules().getTransition(missing);
System.out.println(t != null && t.isGap()); // true

go deeper

for a junior

Knows DST exists and that spring loses an hour; can state java.time shifts the time forward rather than throwing.

for a middle

Can show the exact transformation (02:30 -> 03:30 with the later offset) and detect a gap with ZoneRules.getTransition.

for a senior

Reasons about the scheduling consequences (a local-time job firing late on transition day) and chooses to reject/log vs accept the shift.

for a principal

Sets org-wide policy: store instants/UTC, schedule in UTC where possible, and define how gap inputs are validated across services to avoid silent shifts.

**Background terms.** - **Local time / `LocalDateTime`**: a date-and-time with no time zone, e.g. `2024-03-31T02:30`. It is just "wall-clock" numbers; it does not by itself name a moment on the global timeline. - **Time zone / `ZoneId`**: a named region (e.g. `Europe/Berlin`) whose rules say which UTC **offset** applies at any moment. - **Offset / `ZoneOffset`**: the fixed difference from UTC, e.g. `+01:00` (winter) or `+02:00` (summer). - **Instant**: an exact point on the global timeline (UTC seconds since 1970). A `ZonedDateTime` = local time + zone, which together resolve to one instant. - **DST (Daylight Saving Time)**: many zones move their clocks forward in spring and back in autumn to shift daylight. The forward move is **spring-forward**; the backward move is **fall-back**. **The spring-forward gap.** When a zone springs forward, say at `02:00` the clock instantly becomes `03:00`, the wall-clock times `02:00:00`–`02:59:59` **never occur** that day. This missing interval is the **gap** (typically one hour). A `LocalDateTime` inside the gap is a perfectly valid date-time object, but it corresponds to **no real instant** in that zone. **What java.time does.** `ZonedDateTime.of(localDateTime, zone)` (and `LocalDateTime.atZone(zone)`, which calls the same logic) must turn the local time into an instant. The contract for a gap is: **shift the local time forward by the size of the gap and use the offset after the transition**. So `2024-03-31T02:30` in `Europe/Berlin` becomes `2024-03-31T03:30+02:00`, not an exception, not a null. The instant produced is the moment one hour later than the naive reading would suggest. The rules come from `ZoneRules.getTransition(localDateTime)`, which returns a `ZoneOffsetTransition`; for a gap, `isGap()` is true and the resolution adds `transition.getDuration()`. **Why it never throws.** java.time deliberately favors a total, predictable function over exceptions: every `LocalDateTime` maps to exactly one `ZonedDateTime`. The cost is that a silently-shifted value can surprise you, hence the bugs around scheduling. **How to detect a gap yourself.** Use `zone.getRules().getTransition(local)`; if it returns a non-null transition with `isGap() == true`, your local time was invalid and got moved. You can then decide to reject the input, log it, or accept the shift. **Practical impact.** A daily 02:30 job scheduled by local time will, on the spring-forward day, actually fire at 03:30 (the gap pushed it). Alarm/cron-style code that assumes "02:30 exists every day" is wrong twice a year for affected zones.

  • How can you tell, in code, that a given LocalDateTime fell into a spring-forward gap?
    Call zone.getRules().getTransition(localDateTime). If it returns a non-null ZoneOffsetTransition whose isGap() is true, the local time does not exist and resolution shifts it forward by getDuration().
  • Does this gap behavior affect Instant or epoch-millis arithmetic?
    No. Instant and epoch time are pure UTC points with no DST. The gap only matters when converting a wall-clock LocalDateTime to/from a zoned time.

saying these in an interview costs you the question

  • Claiming java.time throws an exception for a gap time (it does not by default)
  • Thinking a gap LocalDateTime is invalid to construct (only its zone resolution is special)
  • Assuming the gap is always exactly one hour (most are, but the duration comes from the rules)
  • Believing UTC or Instant are affected by DST (they are not; only wall-clock zones)

context

open as a page

What is the fall-back overlap in DST, how does java.time resolve an ambiguous local time by default, and how do you choose the other offset?

level: middleimportance: must knowfreq 58%

basics

~20 s

In autumn, clocks go back so one hour of local time happens twice. A time like 02:30 is ambiguous. By default java.time picks the earlier of the two offsets. You can force the other one with withLaterOffsetAtOverlap() (or back with withEarlierOffsetAtOverlap()).

open as a page

Across a DST boundary, why can adding a Duration of 24 hours give a different result than adding a Period of 1 day, and which should you use?

level: seniorimportance: must knowfreq 54%

basics

~20 s

Duration counts exact seconds, so 24 hours is always 24 hours of real time. Period counts calendar units, so 1 day means the same wall-clock time tomorrow. On a DST-change day the real day is 23 or 25 hours, so the two answers differ by an hour. Use Period for calendar dates, Duration for elapsed time.

open as a page

For a global app that schedules events and stores timestamps, how should you model time to stay correct across DST, and what are the trade-offs of storing Instant/UTC versus zoned/local time?

level: principalimportance: should knowfreq 40%

basics

~20 s

Store an exact instant (UTC) for things that already happened, so they are unambiguous. For future events tied to a place's wall clock, store the local time plus the ZoneId (not a fixed offset), so the right offset is applied when DST rules change. Never store bare local time alone.

open as a page