Skip to content
JavaAgentic

Type at least two characters. Try “RAG”, “pgvector” or “tool calling”.

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.

Beginner5 min readUpdated
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

  • Instant is a point on the UTC timeline. LocalDateTime is a wall-clock reading with no zone. They are not interchangeable.
  • ZonedDateTime is a local date-time plus a zone — the only type that knows about daylight saving.
  • Period is calendar time (months, days); Duration is elapsed time (seconds, nanos).
  • Every type is immutable, so every operation returns a new instance and DateTimeFormatter is safe to share.
  • Store timestamps as Instant; store wall-clock values as LocalDate/LocalTime.

Choosing the type

The first question is whether the value is a moment on the timeline or a reading off a wall clock.
TypeHoldsExample use
InstantNanoseconds since the epoch, UTCcreated_at, event timestamps, logs
LocalDateYear, month, dayDate of birth, invoice date
LocalTimeHour, minute, secondShop opens at 09:00
LocalDateTimeBoth, no zoneAn appointment slot before a zone is chosen
ZonedDateTimeLocal date-time + ZoneIdA meeting in Europe/London, DST-aware
OffsetDateTimeLocal date-time + fixed offsetWire formats, ISO-8601 with +01:00
DurationElapsed seconds and nanosTimeouts, measured latency
PeriodYears, months, daysAge, 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

creating and converting
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);       // calendar

Period versus Duration

two different questions
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();   // 12

Period 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

the gap
// 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)
the overlap
// 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 apart

Neither 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.

the plusDays question
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

DateTimeFormatter is immutable and thread-safe
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

crossing the boundary
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?
Store an Instant — a point on the UTC timeline — for anything that records when something happened: created_at, an audit entry, an event. Store a LocalDateTime or LocalDate only for a wall-clock value whose meaning is independent of zone, such as a birthday or a shop opening time of 09:00. Mapping an Instant to a TIMESTAMP WITH TIME ZONE column is the safe default.
Why was SimpleDateFormat a problem?
It is mutable and not thread-safe, and it keeps parsing state in instance fields. A static SimpleDateFormat shared across request threads produces silently wrong dates, not an exception — years off by a decade, months swapped. DateTimeFormatter is immutable and safe to share, which removes the whole class of bug.
What happens when I add a day across a daylight-saving boundary?
plusDays(1) on a ZonedDateTime adds a calendar day and keeps the local time, so it may be 23 or 25 real hours. plus(Duration.ofDays(1)) adds exactly 24 hours of elapsed time, so the local time shifts. Both are correct for different questions: "same time tomorrow" versus "24 hours from now". Choosing the wrong one is a classic scheduling bug.

Related tutorials