Learning goals
By the end of this chapter you should be able to:
- Use
java.timetypes instead of legacyDateandCalendar - Model instant, local, and zoned time correctly for logs, APIs, and user-facing displays
- Parse and format with
DateTimeFormatterwithout surprise locale bugs - Perform safe arithmetic with
PeriodandDuration
The java.time mental model
Legacy java.util.Date mixed instant and calendar concerns and was mutable. java.time (Java 8+) splits responsibilities:
| Type | Meaning | Example use |
|---|---|---|
Instant |
Point on UTC timeline | Event timestamps in logs/DB |
LocalDate |
Calendar date, no zone | Birthdays, billing cycles |
LocalTime |
Clock time, no zone | Store opening hours |
LocalDateTime |
Date + time, no zone | Meeting without zone agreed |
ZonedDateTime |
Full instant in a zone | User-facing “3pm in Tokyo” |
OffsetDateTime |
Instant with fixed offset | ISO-8601 API payloads |
Rule: store Instant or OffsetDateTime in databases; convert to ZonedDateTime at the UI boundary.
Instants and clocks
Instant now = Instant.now();
Instant later = now.plus(Duration.ofHours(2));
boolean before = now.isBefore(later);
long epochMillis = now.toEpochMilli();
Instant fromMillis = Instant.ofEpochMilli(epochMillis);
For tests, inject a Clock instead of calling Instant.now() directly:
Clock fixed = Clock.fixed(Instant.parse("2026-01-15T10:00:00Z"), ZoneOffset.UTC);
Instant frozen = Instant.now(fixed); // always the same
Local dates and times
LocalDate today = LocalDate.now();
LocalDate deadline = LocalDate.of(2026, 12, 31);
LocalDate nextWeek = today.plusWeeks(1);
LocalTime start = LocalTime.of(9, 30);
LocalDateTime meeting = LocalDateTime.of(today, start);
LocalDate uses ISO-8601 calendar (2026-08-02). Parsing rejects invalid dates like February 30.
LocalDate parsed = LocalDate.parse("2026-08-02"); // ISO_LOCAL_DATE formatter
Zones and offsets
ZoneId tokyo = ZoneId.of("Asia/Tokyo");
ZonedDateTime zdt = ZonedDateTime.now(tokyo);
Instant sameInstant = zdt.toInstant(); // canonical storage form
ZonedDateTime nyView = sameInstant.atZone(ZoneId.of("America/New_York"));
ZoneId handles daylight saving rules. ZoneOffset is a fixed +09:00 style offset — use when the protocol specifies offset, not a named zone.
Period vs Duration
Period— calendar-based: days, months, years (LocalDate.plus)Duration— time-based: seconds, nanos (Instant.plus,LocalTime.plus)
Period threeMonths = Period.ofMonths(3);
LocalDate quarterEnd = LocalDate.now().plus(threeMonths);
Duration timeout = Duration.ofSeconds(30);
Instant expires = Instant.now().plus(timeout);
Do not use Duration.ofDays(30) when you mean “one calendar month” — month lengths differ.
Formatting and parsing
Prefer built-in ISO formatters:
String iso = Instant.now().toString(); // 2026-08-02T17:30:00.123Z
LocalDate d = LocalDate.parse("2026-08-02");
Custom patterns — always set locale explicitly in user-facing code:
DateTimeFormatter fmt = DateTimeFormatter
.ofPattern("dd MMM yyyy", Locale.US);
String text = LocalDate.now().format(fmt);
LocalDate back = LocalDate.parse("02 Aug 2026", fmt);
DateTimeFormatter is immutable and thread-safe — create once, reuse.
Converting legacy types
Bridge old APIs at the edges:
Date legacy = Date.from(Instant.now());
Instant fromLegacy = legacy.toInstant();
Avoid storing new Date/Calendar in domain models. Convert at integration boundaries only.
Common mistakes
- Using
LocalDateTimefor global events — without zone it is ambiguous across DST boundaries. - Assuming
Duration.betweenon dates — usePeriod.betweenfor calendar dates. - Hard-coding
ZoneId.systemDefault()— server zone may differ from user zone; pass zone as parameter. - Mutable
SimpleDateFormat— not thread-safe; replaced byDateTimeFormatter. - Ignoring nanoseconds —
Instanthas ns precision; JSON serializers may truncate to ms.
Practice checkpoint
-
Store “user clicked button” in a DB column — which type? Answer:
Instant(UTC timeline); convert to user zone when displaying. -
Add 1 month to January 31 — what is the result? Answer:
LocalDateadjusts to last valid day of month (Feb 28/29), perPeriodrules. -
Parse
"2026-08-02T15:30:00+05:30"— which type? Answer:OffsetDateTime.parse(...)— includes offset; orZonedDateTimeif you need named zone rules later. -
Why inject
Clockin tests? Answer: deterministic time; no flakiness fromnow()moving between assertions.
Interview angles
- Instant vs ZonedDateTime: instant is universal; zoned is for human rules (DST, local midnight).
- Legacy migration:
Date→Instant,Calendar→ZonedDateTime/LocalDate. - API design: accept ISO-8601 strings; document whether offset or zone is required.
- Thread safety:
java.timetypes are immutable; formatters are thread-safe when shared.