Java Appointment Times: Resolve DST Gaps and Overlaps Explicitly
Turn a local appointment into an instant without silently shifting nonexistent times or choosing an ambiguous offset.
On this page
Key takeaways
- A local date and time is not an instant until a zone and ambiguity policy are supplied.
- Inspect valid offsets before resolving a DST gap or overlap.
- Persist the chosen instant alongside the scheduling intent and zone.
The appointment that moved itself
A clinic accepts an appointment for 02:30 in Europe/Berlin on March 29, 2026. That wall-clock time does not exist: the spring transition jumps from 02:00 to 03:00. If the application immediately calls local.atZone(zone), Java's normal resolution rules move the local time forward by the gap. The resulting instant is valid, but the patient never agreed to that replacement appointment. This is a business decision disguised as a convenient conversion.
The autumn transition creates a different problem. On October 25, 2026, 02:30 occurs twice in Berlin. Both offsets are valid. A calendar field cannot express which occurrence the patient intended. Treat these two cases separately; a single generic "invalid date" error loses useful information.
Ask the zone rules first
This example uses the Java 8+ java.time API. It deliberately rejects nonexistent local times and asks for an explicit offset during overlaps. Production zone rules come from the runtime's timezone database; keep that database current when governments change future transitions.
import java.time.*;
import java.util.List;
public class AppointmentTime {
static Instant resolve(LocalDateTime local, ZoneId zone,
ZoneOffset chosenOffset) {
List<ZoneOffset> offsets = zone.getRules().getValidOffsets(local);
if (offsets.isEmpty()) {
throw new IllegalArgumentException("Nonexistent local time");
}
if (offsets.size() > 1 && chosenOffset == null) {
throw new IllegalArgumentException("Choose an overlap offset");
}
ZoneOffset offset = chosenOffset == null ? offsets.get(0) : chosenOffset;
if (!offsets.contains(offset)) {
throw new IllegalArgumentException("Offset does not match zone");
}
return ZonedDateTime.ofStrict(local, offset, zone).toInstant();
}
public static void main(String[] args) {
ZoneId zone = ZoneId.of("Europe/Berlin");
LocalDateTime local = LocalDateTime.of(2026, 10, 25, 2, 30);
System.out.println(resolve(local, zone, ZoneOffset.ofHours(2)));
System.out.println(resolve(local, zone, ZoneOffset.ofHours(1)));
}
}
The two autumn choices correspond to 00:30Z and 01:30Z. Display the choices as offsets and human-readable occurrences, not as unexplained boolean flags such as dst=true. Some transitions are not one hour, and a daylight-saving label alone is not a universal disambiguation contract.
Duration is not calendar arithmetic
Suppose a medication reminder repeats at the same local time tomorrow. A calendar-day operation and a fixed elapsed duration answer different questions. zoned.plusDays(1) advances the calendar date while resolving the next local time. Adding Duration.ofHours(24) to the instant advances exactly twenty-four elapsed hours and can change the displayed wall time across a transition.
Neither operation is intrinsically wrong. A maintenance timeout normally needs elapsed duration. A recurring office appointment normally needs local calendar intent. Specify the rule before choosing the method. If tomorrow lands inside a gap, your recurring scheduler also needs an explicit skip, reject, or shift policy; the first appointment's validation does not solve future recurrences.
Store evidence for later changes
For a booked, fixed occurrence, store an ISO instant, the IANA zone identifier, the submitted local datetime, and the chosen offset. The instant answers when execution occurs. The other fields explain what the user requested. A recurring series instead needs a local rule plus a zone and a documented resolution policy. Recomputing future occurrences after a timezone database upgrade may be appropriate, but silently changing already confirmed appointments is not.
Use the timestamp utility to inspect an instant and the date calculator to explore calendar boundaries. Those are diagnostic aids, not substitutes for server-side rules or the clinic's scheduling policy.
A focused regression matrix
Test an ordinary winter time, the spring gap, both autumn offsets, and an offset that is invalid for the supplied zone. Also test serialization through your database driver: accidentally converting an instant into a server-local datetime can undo correct resolution. Assertions should compare instants and intent fields, not a localized string whose formatting depends on the machine. Finally, pin the timezone database used by a reproducible test environment while making upgrades an explicit maintenance task.