The java.time API
Choosing between Instant, LocalDateTime and ZonedDateTime, Period versus Duration, what happens at a daylight-saving gap, and how to store timestamps so they survive a zone change.
On this page
java.time replaced an API that was mutable, zero-indexed in one place and one-indexed in another,
and shipped with a formatter that corrupted data when shared between threads. The replacement is
excellent and has one genuine difficulty: choosing the right type.
Key Takeaways
Instantis a point on the UTC timeline.LocalDateTimeis a wall-clock reading with no zone. They are not interchangeable.ZonedDateTimeis a local date-time plus a zone — the only type that knows about daylight saving.Periodis calendar time (months, days);Durationis elapsed time (seconds, nanos).- Every type is immutable, so every operation returns a new instance and
DateTimeFormatteris safe to share. - Store timestamps as
Instant; store wall-clock values asLocalDate/LocalTime.
Choosing the type
| Type | Holds | Example use |
|---|---|---|
Instant | Nanoseconds since the epoch, UTC | created_at, event timestamps, logs |
LocalDate | Year, month, day | Date of birth, invoice date |
LocalTime | Hour, minute, second | Shop opens at 09:00 |
LocalDateTime | Both, no zone | An appointment slot before a zone is chosen |
ZonedDateTime | Local date-time + ZoneId | A meeting in Europe/London, DST-aware |
OffsetDateTime | Local date-time + fixed offset | Wire formats, ISO-8601 with +01:00 |
Duration | Elapsed seconds and nanos | Timeouts, measured latency |
Period | Years, months, days | Age, subscription length |
The distinction that trips people up: LocalDateTime does not identify a moment. "2026-03-29
01:30" is not a point in time until you say where — and in Europe/London that particular reading does
not exist at all, because the clocks jump from 01:00 to 02:00.
The essential operations
Instant now = Instant.now();
LocalDate today = LocalDate.now(); // uses the default zone
LocalDate explicit = LocalDate.now(ZoneId.of("Asia/Kolkata")); // better: be explicit
// Instant -> ZonedDateTime -> LocalDateTime
ZonedDateTime london = now.atZone(ZoneId.of("Europe/London"));
LocalDateTime wall = london.toLocalDateTime();
// LocalDateTime -> Instant requires a zone. There is no default that is correct.
Instant back = wall.atZone(ZoneId.of("Europe/London")).toInstant();
// Arithmetic — every method returns a NEW instance
LocalDate nextMonth = today.plusMonths(1);
LocalDate endOfMonth = today.with(TemporalAdjusters.lastDayOfMonth());
LocalDate nextFriday = today.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));
// Comparison
boolean overdue = dueDate.isBefore(LocalDate.now());
// Difference
Duration latency = Duration.between(start, end); // elapsed
Period age = Period.between(birthDate, today); // calendarPeriod versus Duration
LocalDate birth = LocalDate.of(1990, 5, 20);
Period age = Period.between(birth, LocalDate.of(2026, 8, 8));
age.getYears(); // 36
age.getMonths(); // 2
age.getDays(); // 19
Duration d = Duration.ofHours(36);
d.toDays(); // 1
d.toHoursPart(); // 12Period counts calendar units, which are not fixed lengths — a month is 28 to 31 days, a year is 365
or 366. Duration counts seconds, which are always the same length. Adding Period.ofMonths(1) to
31 January gives 28 February (clamped); adding Duration.ofDays(30) gives 2 March.
Use Period for anything a human would describe in months or years, and Duration for timeouts,
measured elapsed time and anything under a day.
Time zones and daylight saving
// In Europe/London, 2026-03-29 01:30 does not exist — the clocks go 01:00 -> 02:00.
LocalDateTime gap = LocalDateTime.of(2026, 3, 29, 1, 30);
ZonedDateTime resolved = gap.atZone(ZoneId.of("Europe/London"));
// -> 2026-03-29T02:30+01:00 (shifted forward by the gap length, silently)// In autumn, 01:30 happens twice. atZone picks the EARLIER offset by default.
LocalDateTime overlap = LocalDateTime.of(2026, 10, 25, 1, 30);
ZonedDateTime first = overlap.atZone(ZoneId.of("Europe/London")); // +01:00
ZonedDateTime second = first.withLaterOffsetAtOverlap(); // +00:00
Duration.between(first.toInstant(), second.toInstant()); // PT1H — an hour apartNeither case throws. The API resolves them by documented rules, which means a scheduling bug here is silent. If your system schedules recurring jobs in local time, these two hours a year are exactly where it will misfire.
ZonedDateTime before = ZonedDateTime.of(
LocalDateTime.of(2026, 3, 28, 12, 0), ZoneId.of("Europe/London"));
before.plusDays(1); // 2026-03-29T12:00+01:00 — 23 real hours later
before.plus(Duration.ofDays(1)); // 2026-03-29T13:00+01:00 — exactly 24 hours later"Same time tomorrow" and "24 hours from now" are different requirements, and the API makes you choose.
Calendar arithmetic (plusDays, plusMonths) keeps the wall-clock reading; Duration arithmetic
keeps the elapsed time.
Formatting and parsing
private static final DateTimeFormatter ISO = DateTimeFormatter.ISO_INSTANT;
private static final DateTimeFormatter UK =
DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm").withZone(ZoneId.of("Europe/London"));
String s = UK.format(Instant.now());
LocalDate d = LocalDate.parse("2026-08-08"); // ISO by default
LocalDate uk = LocalDate.parse("08/08/2026", DateTimeFormatter.ofPattern("dd/MM/yyyy"));A static final DateTimeFormatter is correct and is the direct fix for the SimpleDateFormat
problem. Two pattern-letter gotchas worth remembering: yyyy is the calendar year while YYYY is the
week-based year (which differs in the last days of December, and has caused real incidents), and mm
is minutes while MM is months.
Interop with the old API
Date legacy = Date.from(instant);
Instant modern = legacy.toInstant();
java.sql.Timestamp ts = java.sql.Timestamp.from(instant);
LocalDate sqlDate = java.sql.Date.valueOf(localDate).toLocalDate();
Calendar cal = GregorianCalendar.from(zonedDateTime);
ZonedDateTime fromCal = ((GregorianCalendar) cal).toZonedDateTime();Modern JDBC drivers and Hibernate 6 map Instant, LocalDate and LocalDateTime directly, so in a
new codebase these conversions should only appear at the edge of a third-party API.
What gets asked
Reliably: the difference between Instant and LocalDateTime; Period versus Duration; why
SimpleDateFormat was dangerous; and what you would store in a database column. That last question
is the practical one — answer "an Instant in a TIMESTAMP WITH TIME ZONE column for events, and a
LocalDate for anything that is genuinely a calendar date", and explain the daylight-saving reason,
and you have covered the whole topic.
Frequently Asked Questions
Should I store an Instant or a LocalDateTime in the database?
Why was SimpleDateFormat a problem?
What happens when I add a day across a daylight-saving boundary?
Related tutorials
- Default & Static Methods in InterfacesWhy default methods were added, the three resolution rules when a class inherits conflicting defaults, calling a specific supertype with X.super.method(), and private interface methods.
- Modern Java 9-21: Records, Sealed Types, Pattern MatchingWhat actually changed after Java 8 and why it matters in an interview: var, records, sealed interfaces, pattern matching for switch, text blocks, the module system and virtual threads.
- Optional: Correct Use and Common AbuseWhat Optional was designed for and what it was not, the orElse versus orElseGet trap that evaluates the fallback every time, chaining with map and flatMap, and why Optional fields are a mistake.
- Parallel Streams and the Common ForkJoinPoolWhy every parallel stream in your JVM shares one pool, which sources split well, the N times Q rule for deciding, and why a blocking call inside a parallel stream can stall the whole application.