Interviewers ask this in a few forms: “What is the difference between LocalDateTime and Instant?”, “How would you store a meeting time for users in different countries?”, or “Why did this scheduled job run twice last Sunday?” The question tests whether you can model time correctly, which is surprisingly rare. Many production bugs come from storing a wall-clock time without a zone, or adding 24 hours when the business meant “the same time tomorrow”.
A senior answer chooses the right type for each fact, explains daylight saving gaps and overlaps, and shows how to make time-dependent code testable.
Before you start
You should know the java.time API basics introduced in Java 8, and why the legacy java.util.Date and Calendar classes are discouraged: they are mutable, poorly designed and confusingly named. Understand that a time zone such as Europe/Paris is a set of rules (offset changes over history) while an offset such as +02:00 is a single fixed number.
The short answer
Use Instant for a point on the global timeline, such as when an event happened; it is what you store and compare. Use LocalDate and LocalDateTime for wall-clock values without a zone, such as a birthday or “9:00 on the calendar”. Use ZonedDateTime when local rules matter, for example scheduling “9:00 every day in Paris”, because it applies daylight saving transitions; OffsetDateTime carries a fixed offset and suits APIs and databases. Convert to a zone only at the edges, and inject a Clock so tests control “now”.
How it works
The main types differ in what they know:
| Type | Example | Knows the zone rules | Represents an exact moment |
|---|---|---|---|
Instant |
2026-03-29T01:30:00Z |
no (always UTC) | yes |
LocalDateTime |
2026-03-29T02:30 |
no | no |
OffsetDateTime |
2026-03-29T03:30+02:00 |
no, fixed offset | yes |
ZonedDateTime |
2026-03-29T03:30+02:00[Europe/Paris] |
yes | yes |
A LocalDateTime is ambiguous until you attach a zone. “2026-11-01T01:30” is a different instant in New York and London, and in New York it happens twice that day.
Daylight saving transitions create two kinds of trouble. In a gap, clocks jump forward and some local times do not exist. In an overlap, clocks fall back and some local times occur twice. ZonedDateTime.of resolves them by documented rules: a time in a gap is shifted forward by the gap length, and a time in an overlap uses the earlier offset by default.
ZoneId paris = ZoneId.of("Europe/Paris");
// 2026-03-29: clocks jump from 02:00 to 03:00 in Paris
ZonedDateTime gap = ZonedDateTime.of(LocalDateTime.of(2026, 3, 29, 2, 30), paris);
System.out.println(gap); // 2026-03-29T03:30+02:00[Europe/Paris]
// 2026-10-25: clocks fall back from 03:00 to 02:00
ZonedDateTime overlap = ZonedDateTime.of(LocalDateTime.of(2026, 10, 25, 2, 30), paris);
System.out.println(overlap); // 2026-10-25T02:30+02:00[Europe/Paris]
System.out.println(overlap.withLaterOffsetAtOverlap()); // 2026-10-25T02:30+01:00[Europe/Paris]Arithmetic also depends on intent. plus(Duration.ofHours(24)) adds exact elapsed time, while plusDays(1) keeps the same local time on the next calendar day. Across a DST change they give different instants.
Step-by-step walkthrough
Step 1: Classify each time value in your domain
Ask what each value means. “The payment was captured” is an instant. “The shop opens at 09:00” is a local time plus the shop’s zone. “The contract starts on 2026-01-01” is a LocalDate. “A user’s birthday” is a LocalDate with no zone at all. Writing this down first prevents most bugs.
Step 2: Store instants and zone ids, not formatted strings
record Event(UUID id, Instant occurredAt) {}
record Appointment(UUID id, LocalDateTime localStart, ZoneId zone) {
Instant startInstant() {
return localStart.atZone(zone).toInstant();
}
}For a future appointment, store the local time and the zone id rather than only an instant. If a government changes its zone rules before the appointment, the instant must be recomputed, while the user’s intended “10:00 local” stays the same. In PostgreSQL, map Instant or OffsetDateTime to timestamptz.
Step 3: Convert at the edges
Instant now = Instant.now(clock);
ZonedDateTime forUser = now.atZone(ZoneId.of(user.timeZone()));
String label = forUser.format(DateTimeFormatter.ofPattern("EEE d MMM, HH:mm", user.locale()));Server code works in instants. The presentation layer converts to the viewer’s zone and locale.
Step 4: Choose the right arithmetic
ZonedDateTime start = ZonedDateTime.of(2026, 3, 28, 9, 0, 0, 0, paris);
start.plusDays(1); // 2026-03-29T09:00+02:00 (same wall-clock time)
start.plus(Duration.ofHours(24)); // 2026-03-29T10:00+02:00 (24 elapsed hours)Daily reminders want plusDays. Session timeouts and rate limits want Duration.
Step 5: Inject a Clock
class InvoiceService {
private final Clock clock;
InvoiceService(Clock clock) { this.clock = clock; }
boolean isOverdue(Invoice inv) {
return LocalDate.now(clock.withZone(inv.zone())).isAfter(inv.dueDate());
}
}Production passes Clock.systemUTC(); tests pass Clock.fixed(...).
Worked scenario
A reporting job is scheduled to run “every day at 02:30 Europe/Paris”. The original scheduler computes the next run like this:
LocalDateTime next = lastRun.plusHours(24);
Instant fireAt = next.atZone(paris).toInstant();On the spring-forward night, 02:30 does not exist, so atZone shifts it to 03:30, and the next calculation starts from 03:30, permanently drifting the schedule by an hour. On the autumn night, 02:30 happens twice; a different scheduler built with fixed 24-hour durations fires at both 02:30 occurrences because it tracks elapsed time. Finance received two reports and then a late one.
The fix keeps the local target time constant and lets the zone rules decide each instant, with an explicit policy for gaps and overlaps:
static Instant nextRun(LocalDate day, ZoneId zone) {
LocalDateTime target = day.atTime(2, 30);
ZonedDateTime zdt = ZonedDateTime.of(target, zone); // gap: moves to 03:30
return zdt.withEarlierOffsetAtOverlap().toInstant(); // overlap: run once, at the first 02:30
}Each run computes from the calendar date, not the previous instant, so drift cannot accumulate. Many teams also schedule batch jobs in UTC to avoid the question entirely; that is a valid answer if the business accepts that local run times shift with DST.
Common mistake
- Storing LocalDateTime for events. Without a zone, nobody can tell which moment it was.
- Using a fixed offset for a region.
+01:00is wrong for Paris half the year. UseZoneId.of("Europe/Paris"). - Adding 24 hours for “tomorrow”. Across DST, that is not the same wall-clock time.
- Calling LocalDate.now() without a zone or clock. It silently uses the server’s default zone, which differs between laptops and containers.
- Using three-letter zone ids.
ESTandISTare ambiguous;ISTcan mean India, Ireland or Israel.
Verify the behavior
These assertions pin down the gap, overlap and arithmetic rules for Paris in 2026:
import static org.junit.jupiter.api.Assertions.*;
import java.time.*;
import org.junit.jupiter.api.Test;
class ZoneRulesTest {
final ZoneId paris = ZoneId.of("Europe/Paris");
@Test
void gapShiftsForward() {
var zdt = ZonedDateTime.of(LocalDateTime.of(2026, 3, 29, 2, 30), paris);
assertEquals(LocalTime.of(3, 30), zdt.toLocalTime());
}
@Test
void overlapDefaultsToEarlierOffset() {
var zdt = ZonedDateTime.of(LocalDateTime.of(2026, 10, 25, 2, 30), paris);
assertEquals(ZoneOffset.ofHours(2), zdt.getOffset());
assertEquals(ZoneOffset.ofHours(1), zdt.withLaterOffsetAtOverlap().getOffset());
}
@Test
void plusDaysVersusDuration() {
var start = ZonedDateTime.of(2026, 3, 28, 9, 0, 0, 0, paris);
assertEquals(9, start.plusDays(1).getHour());
assertEquals(10, start.plus(Duration.ofHours(24)).getHour());
}
}Run java -version and check the JDK’s tzdata version if results differ; zone rules ship with the JDK and are updated in patch releases.
Follow-up questions
Is Instant the same as epoch milliseconds? It represents the same timeline with nanosecond precision. Instant.toEpochMilli() converts, truncating sub-millisecond detail.
What is the difference between Period and Duration? Period is date-based (years, months, days) and respects calendars; Duration is exact seconds and nanoseconds.
Are java.time types thread-safe? Yes. They are immutable, and DateTimeFormatter is too, unlike the old SimpleDateFormat.
How do you convert legacy Date? date.toInstant() and Date.from(instant). For java.sql.Timestamp, use toInstant() or toLocalDateTime(), knowing which you mean.
Interview exercise
A user in New York books a call for “10:00 on 2026-11-01” with a colleague in London. What do you store, and what time does the London colleague see?
Answer and reasoning
Store the organiser’s intent: LocalDateTime 2026-11-01T10:00 and ZoneId America/New_York, plus the computed Instant for indexing. New York falls back to standard time at 02:00 on 2026-11-01, so 10:00 that day is unambiguous at offset -05:00, which is 15:00 UTC. London has already returned to GMT on 2026-10-25, so the London colleague sees 15:00. A week earlier, the gap between the cities would have been only four hours, because the two regions change clocks on different dates. Storing the zone id rather than a fixed offset is what lets the system get both weeks right, and keeping the local intent lets you recompute if zone rules change before the meeting.
Continue learning
Practise more with the Java interview questions and the Java MCQs. Related notes: immutable objects and defensive copies and records and shallow immutability. The type overview lives in the java.time package documentation.