skip to content

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%

answer

  1. Duration = exact seconds; Period = calendar fields
  2. DST day is 23h (spring) or 25h (fall)
  3. plusHours(24) moves the instant; plusDays(1) keeps local time
  4. They differ by the transition size on change days
  5. Calendar -> Period/plusDays; elapsed -> Duration/plusHours

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.

solid answer

~40 s

Duration is machine time: a fixed number of seconds, DST-agnostic. Period is human/calendar time: years, months, days that mean field-based steps on the wall clock. When you add them to a ZonedDateTime, plus(Duration.ofHours(24)) advances the underlying instant by exactly 24 real hours, so on a spring-forward day (a 23-hour day) the wall-clock lands an hour later than the same time tomorrow. plusDays(1) (equivalent to a Period of one day) keeps the same local time on the next date and lets the offset adjust, so it advances only 23 (or 25) real hours. They diverge precisely on transition days. Rule of thumb: use Period/plusDays for calendar reasoning (a daily reminder at 09:00), use Duration/plusHours for elapsed physical time (a token expiring in 24 hours). Mixing them is a classic source of off-by-one-hour bugs.

code

java · 9 lines
java
ZoneId berlin = ZoneId.of("Europe/Berlin");
ZonedDateTime start = ZonedDateTime.of(
    LocalDateTime.parse("2024-03-31T01:30"), berlin); // spring-forward day

ZonedDateTime byDuration = start.plus(Duration.ofHours(24));
ZonedDateTime byCalendar = start.plusDays(1);
System.out.println(byDuration); // 2024-04-01T02:30+02:00 (24 real hours)
System.out.println(byCalendar); // 2024-04-01T01:30+02:00 (same local time)
System.out.println(Duration.between(start, byCalendar)); // PT23H

go deeper

for a junior

Knows Duration is exact time and Period is calendar days, and that a DST day is not 24 hours.

for a middle

Can predict that plusHours(24) and plusDays(1) differ by an hour on a transition day and pick the right one for a scenario.

for a senior

Explains the mechanism (instant vs wall-clock advance, 23/25-hour days) and audits scheduling/expiry code for the correct amount type.

for a principal

Sets conventions distinguishing elapsed vs calendar semantics across the system, and reviews APIs (token TTLs, billing, cron) so the right time model is used consistently.

**Two kinds of time.** java.time deliberately splits amounts into two types that implement different interfaces: - **`Duration`** (a `TemporalAmount` measured in seconds + nanos): an **exact, physical** length of time. `Duration.ofHours(24)` is precisely 86,400 seconds. It knows nothing about calendars or DST. - **`Period`** (a `TemporalAmount` measured in years/months/days): a **calendar/conceptual** length. `Period.ofDays(1)` means "advance the date field by one", *not* "add 86,400 seconds". Equivalent shortcuts are `plusDays(1)`, `plusMonths(1)`, etc. **Why a day is not always 24 hours.** On a normal day a zone's wall clock and its instants advance together, 24 wall-clock hours = 24 real hours. But on a DST transition day: - **Spring-forward**: an hour is skipped, so the civil day is only **23 hours** of real time. - **Fall-back**: an hour repeats, so the civil day is **25 hours** of real time. **Adding to a ZonedDateTime, the divergence.** Take `2024-03-31T01:30+01:00[Europe/Berlin]` (the morning of spring-forward, gap at 02:00->03:00): - `plus(Duration.ofHours(24))` advances the **instant** by exactly 24 hours. Because the civil day was 23 hours, the wall clock overshoots by one hour -> result `2024-04-01T02:30+02:00`. The *local time changed* (01:30 -> 02:30) even though you "added a day" of seconds. - `plusDays(1)` (Period semantics) keeps the **same local time** on the next date -> `2024-04-01T01:30+02:00`. The instant advanced only **23 hours**, but the wall clock reads the same 01:30. So the two results differ by exactly the size of the transition. On non-transition days they agree. **Which to use.** - Use **`Period` / `plusDays` / `plusMonths`** when the requirement is *calendar-based*: "same time tomorrow", "bill on the 1st of each month", "a reminder every day at 09:00". Users expect the wall clock to stay put; the real elapsed time flexes with DST. - Use **`Duration` / `plusHours` / `plusSeconds`** when the requirement is *elapsed physical time*: "this token is valid for exactly 24 hours", "retry after 30 seconds", "the meeting lasts 90 minutes". The instant must advance by an exact amount regardless of DST. **Common pitfalls.** - Modelling a daily job as `Duration.ofHours(24)`, it drifts an hour around DST. Use `plusDays(1)`. - Computing "how long until expiry" with `Period` between two zoned times, it gives calendar days, not the exact hours elapsed. Use `Duration.between` for exact, `Period.between` (on `LocalDate`) for calendar. - `Duration.between(start, end)` on two `ZonedDateTime`s always returns the true elapsed seconds (it converts to instants), so it can be 23h or 25h for what looks like "a day". **Instant note.** If everything is an `Instant`/UTC, there is no DST and `plus(Duration)` is the only meaningful add, the divergence appears only once you reason in a wall-clock zone.

  • What does Duration.between(start, end) return for two ZonedDateTimes spanning a spring-forward day at the same wall-clock time?
    23 hours, not 24. Duration.between converts both to instants and reports the true elapsed physical time, which is short by the skipped hour.
  • You need a daily 09:00 reminder that survives DST. Which API?
    Use plusDays(1) (Period semantics) on a ZonedDateTime so the local 09:00 is preserved; do not add Duration.ofHours(24), which would drift to 08:00 or 10:00 around transitions.

saying these in an interview costs you the question

  • Believing 24 hours always equals 1 day in a zoned calculation
  • Using Duration.ofHours(24) for daily scheduling (drifts at DST)
  • Thinking Duration.between two zoned days always yields 24h (it is the true elapsed time, 23/25h on transitions)
  • Assuming plusDays adds exactly 86,400 seconds (it adjusts the offset instead)

context