Stage 3: Core APIs, lesson 8 of 12

java.time: dates done right

Intermediate3 min read@since 8Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

Java 8 introduced java.time to replace the mutable, confusing Date and Calendar. Every class is immutable and thread-safe.

  • LocalDate: a date with no time, like a birthday.
  • LocalDateTime: date and time with no zone.
  • ZonedDateTime: date and time in a zone such as Asia/Kolkata.
  • Instant: a point on the UTC timeline. Store this for timestamps.
  • Duration (hours, seconds) and Period (days, months, years).
  • DateTimeFormatter: thread-safe formatting, unlike the old SimpleDateFormat.

Rule of thumb for backends: store an Instant (or timestamptz in PostgreSQL) and convert to the user's zone only for display.

LocalDate, LocalTime and LocalDateTime

"Local" types have no time zone: a birthday, an office opening time, a timetable. They're immutable; methods such as plusDays return a new value.

Java
LocalDate today = LocalDate.now();
LocalDate due = today.plusDays(7);
LocalDate diwali = LocalDate.of(2026, 11, 8);
LocalTime opens = LocalTime.of(9, 30);
LocalDateTime meeting = LocalDateTime.of(diwali, opens);
boolean late = today.isAfter(due);

Instant, ZonedDateTime and time zones

Instant is a point on the global timeline (UTC), ideal for storing when something happened. ZonedDateTime adds a zone such as Asia/Kolkata for display and calendar logic. Store instants; convert to the user's zone when showing them.

Java
Instant paidAt = Instant.now();
ZonedDateTime inIndia = paidAt.atZone(ZoneId.of("Asia/Kolkata"));
ZonedDateTime inLondon = inIndia.withZoneSameInstant(ZoneId.of("Europe/London"));

Duration vs Period

Duration measures time-based amounts (hours, minutes, seconds); Period measures date-based amounts (years, months, days). ChronoUnit.X.between counts units between two values.

Java
Duration call = Duration.ofMinutes(95);           // PT1H35M
Period age = Period.between(LocalDate.of(1995, 5, 10), LocalDate.now());
long days = ChronoUnit.DAYS.between(start, end);

Formatting and parsing

DateTimeFormatter converts between dates and text. It's immutable and thread-safe, unlike the old SimpleDateFormat. Pattern letters are case-sensitive: MM is month, mm is minutes.

Java
DateTimeFormatter f = DateTimeFormatter.ofPattern("dd-MM-yyyy");
String text = LocalDate.of(2026, 9, 26).format(f);    // "26-09-2026"
LocalDate parsed = LocalDate.parse("01-10-2026", f);
String iso = LocalDate.now().toString();              // ISO: 2026-09-26

Daylight saving and zone rules

India doesn't use daylight saving, but many countries do, which creates missing and repeated local times. Always convert through a ZoneId and let java.time apply the rules, instead of adding fixed hour offsets.

The old Date and Calendar classes

java.util.Date, Calendar and SimpleDateFormat are mutable, confusing (months start at 0) and not thread-safe. Use java.time in new code, and convert at the edges when an old API needs them.

Java
Date legacy = Date.from(Instant.now());
Instant back = legacy.toInstant();

Testing time with Clock

Code that calls LocalDate.now() directly is hard to test. Inject a Clock and use LocalDate.now(clock); tests pass a fixed clock.

Java
class Billing {
    private final Clock clock;
    Billing(Clock clock) { this.clock = clock; }
    boolean isOverdue(LocalDate due) { return LocalDate.now(clock).isAfter(due); }
}
// test: new Billing(Clock.fixed(Instant.parse("2026-10-01T00:00:00Z"), ZoneOffset.UTC))

Example

Java
LocalDate today = LocalDate.now();
LocalDate due = today.plusDays(30);
long days = ChronoUnit.DAYS.between(today, due);            // 30

Instant createdAt = Instant.now();                           // store this in the DB
ZonedDateTime ist = createdAt.atZone(ZoneId.of("Asia/Kolkata"));

DateTimeFormatter fmt = DateTimeFormatter.ofPattern("dd MMM yyyy, hh:mm a");
System.out.println(ist.format(fmt));                         // e.g. 24 Sep 2026, 10:15 AM

Period age = Period.between(LocalDate.of(1995, 5, 20), today);
Duration timeout = Duration.ofSeconds(30);

Common mistake

Using LocalDateTime for event timestamps. It has no zone, so the same value means different moments on servers in different regions.

Under the hood

JPA 2.2 and Hibernate map LocalDate, LocalDateTime and Instant natively, with no converters needed. Inject a Clock into services instead of calling now() directly, so tests can freeze time with Clock.fixed(...).

Check yourself

Which type should store "when was this order created"?

How this connects

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.