java.time (Date and Time API)

Master Java's modern date-time API: LocalDate, LocalDateTime, Instant, Duration, Period, and ZonedDateTime for robust time handling.

published: reading time: 16 min read author: Geek Workbench

Introduction

Java 8 introduced the java.time package to address the well-known deficiencies of java.util.Date and java.util.Calendar. The new API is immutable, thread-safe, and follows a clear domain-driven design with separate types for different time concepts.

When to Use

Use CaseRecommended Type
Birthdays, holidays, birthdaysLocalDate
Timestamp with no timezone contextInstant
Precise elapsed durationsDuration
Age calculation, calendar periodsPeriod
Time with timezone awarenessZonedDateTime / OffsetDateTime
Human-readable date and timeLocalDateTime

When NOT to Use

  • Legacy interop: Use java.util.Date only when forced by legacy APIs.
  • Database columns: Use JDBC adapters rather than manual conversion; prefer LocalDateTime for TIMESTAMP WITH TIME ZONE.
  • Serializing to JSON: Configure ObjectMapper with JavaTimeModule instead of manual parsing.

Core Types Diagram

classDiagram
    class Temporal~T~ {
        <<interface>>
    }
    class TemporalAmount {
        <<interface>>
    }
    class LocalDate {
        +now() LocalDate
        +of(int year, int month, int day) LocalDate
        +plusDays(long) LocalDate
        +minusDays(long) LocalDate
        +isAfter(LocalDate) boolean
    }
    class LocalDateTime {
        +now() LocalDateTime
        +of(int year, int month, int day, int hour, int min) LocalDateTime
        +toLocalDate() LocalDate
        +toLocalTime() LocalTime
    }
    class Instant {
        +now() Instant
        +ofEpochSecond(long) Instant
        +plus(Duration) Instant
        +minus(Duration) Instant
        +toEpochMilli() long
    }
    class ZonedDateTime {
        +now(ZoneId) ZonedDateTime
        +of(LocalDateTime, ZoneId) ZonedDateTime
        +withZoneSameInstant(ZoneId) ZonedDateTime
    }
    class Duration {
        +ofDays(long) Duration
        +ofHours(long) Duration
        +between(Temporal, Temporal) Duration
        +toMillis() long
    }
    class Period {
        +ofDays(int) Period
        +ofMonths(int) Period
        +between(LocalDate, LocalDate) Period
        +getDays() int
    }

    Temporal~T~ <|-- LocalDate
    Temporal~T~ <|-- LocalDateTime
    Temporal~T~ <|-- Instant
    Temporal~T~ <|-- ZonedDateTime
    TemporalAmount <|-- Duration
    TemporalAmount <|-- Period
    LocalDateTime --> LocalDate : toLocalDate()
    ZonedDateTime --> LocalDateTime : composes

Code Examples

LocalDate — Birthdays and Calendar Dates

import java.time.LocalDate;
import java.time.Month;

LocalDate today = LocalDate.now();
LocalDate javaBirthday = LocalDate.of(1995, Month.MAY, 23);
LocalDate endOfYear = LocalDate.of(2026, 12, 31);

// Difference in days
long daysBetween = javaBirthday.until(today).getDays();
System.out.println("Java is " + daysBetween + " days old");

// Adding/subtracting
LocalDate nextWeek = today.plusWeeks(1);
LocalDate lastMonth = today.minusMonths(1);

// Comparison
if (today.isAfter(javaBirthday)) {
    System.out.println("Java has been around for a while");
}

Instant — Machine Timestamps

import java.time.Instant;
import java.time.Duration;

Instant startup = Instant.now();
// ... app runs ...
Instant now = Instant.now();

Duration elapsed = Duration.between(startup, now);
System.out.println("Elapsed: " + elapsed.toMillis() + "ms");

// Convert to/from epoch millis
long epochMillis = Instant.now().toEpochMilli();
Instant restored = Instant.ofEpochMilli(epochMillis);

ZonedDateTime — Timezone-Aware Timestamps

import java.time.ZoneId;
import java.time.ZonedDateTime;

ZonedDateTime utcNow = ZonedDateTime.now(ZoneId.of("UTC"));
ZonedDateTime tokyoTime = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));

// Convert between zones
ZonedDateTime parisScheduled = ZonedDateTime.of(2026, 6, 15, 14, 0, 0, 0, ZoneId.of("Europe/Paris"));
ZonedDateTime tokyoConverted = parisScheduled.withZoneSameInstant(ZoneId.of("Asia/Tokyo"));
System.out.println("Paris 14:00 = Tokyo " + tokyoConverted.getHour() + ":00");

Duration and Period — Elapsed Time

import java.time.Duration;
import java.time.Period;
import java.time.LocalDate;

Duration oneHour = Duration.ofHours(1);
Duration halfHour = Duration.ofMinutes(30);

// Combine durations
Duration total = oneHour.plus(halfHour); // 90 minutes

// Period for calendar math
LocalDate start = LocalDate.of(2026, 1, 1);
LocalDate end = LocalDate.of(2026, 5, 23);
Period age = Period.between(start, end);
System.out.println("Months: " + age.getMonths() + ", Days: " + age.getDays());

Date Formatting and Parsing

import java.time.format.DateTimeFormatter;
import java.time.LocalDateTime;

DateTimeFormatter iso = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
DateTimeFormatter custom = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm");

LocalDateTime now = LocalDateTime.now();
String formatted = now.format(custom);
System.out.println(formatted); // e.g., 23/05/2026 14:30

LocalDateTime parsed = LocalDateTime.parse("23/05/2026 14:30", custom);

Failure Scenarios

ScenarioProblemSolution
LocalDate.of(2026, 2, 30)DateTimeException — invalid day for FebruaryValidate day range: day > 0 && day <= month.length(year % 4 == 0)
Instant.parse("2026-02-30T10:00")DateTimeParseExceptionAlways parse through a formatter with lenient mode if input is untrusted
Duration.between(date1, date2)java.time.temporal.UnsupportedTemporalTypeExceptionDuration works with Instant, LocalDateTime, OffsetTime; use Period for LocalDate
ZonedDateTime.now(ZoneId.of("Mars"))ZoneRulesExceptionValidate zone ID against ZoneId.getAvailableZoneIds()
Race condition on cached ZoneIdStale zone dataDo not cache ZoneId instances across JVM restarts

Trade-off Table

Aspectjava.timejava.util.Datejava.util.Calendar
Thread safetyImmutable, thread-safeMutable, not thread-safeMutable, not thread-safe
API clarityDomain-specific typesSingle ambiguous typeComplex, inconsistent
Timezone handlingFirst-class ZoneIdManual offset managementPartial support
PerformanceSlight overhead for immutabilityFast but unsafeSlow and unsafe
New featuresStreams, parsing, formattingLegacy onlyLegacy only

Observability Checklist

// Instrumenting time operations for observability
import java.time.Instant;
import java.time.Duration;

public class TimedOperation {
    private final Instant start;

    public TimedOperation() {
        this.start = Instant.now();
    }

    public long elapsedMillis() {
        return Duration.between(start, Instant.now()).toMillis();
    }

    // Log with structured fields for observability platforms
    public void logDuration(String operation) {
        System.out.println("operation=" + operation +
            " duration_ms=" + elapsedMillis() +
            " timestamp=" + Instant.now());
    }
}
  • Measure elapsed time with Duration.between(start, end) and expose as a metric.
  • Use Instant for log timestamps rather than System.currentTimeMillis().
  • Validate timezone IDs at startup to fail fast on misconfiguration.
  • Store Instant in persistence layers, not LocalDateTime, to preserve the moment in time.
  • Log parsing failures with the raw input string for debugging.

Security Notes

  • Time-of-check to time-of-use (TOCTOU): Immutable types eliminate some TOCTOU races but calendar operations still require atomic validation.
  • Timezone spoofing: If user input sets the timezone, validate it against an allowlist of permitted zones.
  • Deserialization attacks: Malformed date strings in untrusted input can cause DateTimeParseException; catch and log securely without leaking stack traces.
  • Leap second handling: Instant treats leap seconds as a 1-second increment, not as a special case — do not rely on Instant for financial time calculations.

Pitfalls

  1. Confusing Duration with Period: Duration.ofDays(1) creates a 24-hour duration, while Period.ofDays(1) represents a calendar day which may have different length near DST transitions.
  2. Forgetting ZonedDateTime for scheduled tasks: LocalDateTime.now() returns a time without timezone — two users in different zones see different “now”.
  3. Immutability surprises: Methods like plusDays() return a new instance — the original is unchanged. Assign the result.
  4. Epoch overflow: Instant.MAX is year +1,000,000,000; Instant.MIN is year -1,000,000,000. Unlikely to hit, but be aware.
  5. LocalDateTime to ZonedDateTime is ambiguous: LocalDateTime.of(2026, 3, 29, 2, 30) during DST spring-forward has no valid UTC equivalent — this throws an exception.

Quick Recap

  • java.time is immutable, thread-safe, and domain-driven.
  • LocalDate for calendar dates, LocalDateTime for naive datetime, Instant for UTC timestamps.
  • ZonedDateTime for timezone-aware scheduling; use ZoneId to define the zone.
  • Duration for precise elapsed time, Period for calendar-based elapsed time.
  • Format with DateTimeFormatter; parse with LocalDateTime.parse(str, formatter).
  • Always use Instant for logging and metric timestamps.
  • Prefer Duration.between() over manual subtraction for elapsed time.

Interview Questions

1. What is the difference between `Instant` and `LocalDateTime`?

Model Answer: "`Instant` represents a point in time on the UTC timeline (nanosecond precision, epoch seconds since `1970-01-01T00:00:00Z`). `LocalDateTime` represents a local date and time with no timezone information — it is the same moment viewed differently in different zones. Use `Instant` for machine timestamps and logging; use `LocalDateTime` for human-readable calendar events that have no inherent timezone."

2. How does `Duration` differ from `Period`?

Model Answer: "`Duration` models a precise amount of time in seconds or nanoseconds — it is suited for measuring elapsed time between two `Instant` values. `Period` models a calendar-based amount in days, months, and years — it is suited for date arithmetic where you want "1 month later" to roll over to the next month calendar-wise. A `Duration` of one day is always exactly 86,400 seconds; a `Period` of one day may represent a different amount of wall-clock time near DST transitions."

3. What happens when you parse an invalid date like `2026-02-30`?

Model Answer: "A `DateTimeParseException` is thrown at runtime. The `java.time` types perform strict validation — there is no silent rollover or clamping like the legacy `Calendar`. You must validate input before parsing or catch the exception explicitly."

4. How do you convert between time zones in `java.time`?

Model Answer: "Use `ZonedDateTime.withZoneSameInstant(ZoneId.of("Asia/Tokyo"))` to convert a `ZonedDateTime` to a different zone while preserving the same instant in time. Use `ZonedDateTime.withZoneSameLocal()` if you want to keep the local time but change the zone (this can produce different instants near DST transitions). For `Instant`, use `Instant.atZone(ZoneId)` to attach a zone."

5. Why is `java.util.Date` considered legacy and when might you still need it?

Model Answer: "`java.util.Date` is mutable and not thread-safe, its `toString()` returns a misleading local timezone string, and month numbering is 0-based. You still need it only for interoperability with legacy APIs that require `java.util.Date` — JDBC `setTimestamp()`, some enterprise integrations, and `java.util.concurrent.ScheduledExecutorService`. In all new code, use `java.time`."

6. What is the difference between `ZonedDateTime` and `OffsetDateTime`?

Model Answer: "`ZonedDateTime` includes a time zone ID (e.g., "Europe/Paris") which represents the full timezone rules including DST transitions. `OffsetDateTime` includes only a fixed UTC offset (e.g., "+02:00") which does not account for DST. A `ZonedDateTime` knows the exact wall-clock time in a region; an `OffsetDateTime` only knows the offset from UTC. Use `ZonedDateTime` for scheduled events in a region; use `OffsetDateTime` for timestamps where the offset is known but zone rules are not needed."

7. How does `Instant.now()` behave across different JVMs and how should you correlate instants?

Model Answer: "`Instant.now()` uses the system clock in UTC — it returns the same instant value across all JVMs on the same machine if they share the same clock source. However, different machines may have slightly different clock readings. For distributed system correlation, use a centralized time source (NTP) or log both wall-clock time and epoch millis. Never assume two JVMs have synchronized clocks — always use epoch-based timestamps (`Instant.toEpochMilli()`) for cross-system correlation."

8. What is the purpose of `TemporalAdjusters` and when should you use them?

Model Answer: "`TemporalAdjusters` provides common date adjustments: `nextOrSame(DayOfWeek)`, `lastDayOfMonth()`, `firstDayOfNextMonth()`, etc. They implement the `TemporalAdjuster` interface and can be passed to `with()` methods on date-time types. For example: `date.with(TemporalAdjusters.lastDayOfMonth())` returns the last day of the month. Use these instead of manual day-of-month calculations for readability and correctness near month boundaries."

9. What happens when you add months to a date near the end of the month?

Model Answer: "`LocalDate.of(2026, 1, 31).plusMonths(1)` throws `DateTimeException` because February has no 31st day. The design intentionally prevents silent rollover. Options: use `plusDays()` for a fixed day count, use `withDayOfMonth()` after adding months to clamp to the valid day, or check the day range beforehand and decide whether to throw or clamp. The same rule applies to `minusMonths()`, `plusYears()`, etc."

10. What is the difference between `Duration` and `Period` when measuring elapsed time across DST transitions?

Model Answer: "A `Duration` of one day is always exactly 86,400 seconds (24 hours). A `Period` of one day represents a calendar day — near DST transitions, a day may have 23 or 25 hours. For example, adding a `Period` of one day across a spring-forward DST transition produces a different wall-clock result than adding a `Duration` of one day. Use `Period` for human-calendar arithmetic; use `Duration` for precise elapsed time measurement."

11. What does `ZoneId.of("UTC")` return and how does it differ from `ZoneOffset.UTC`?

Model Answer: "`ZoneId.of("UTC")` returns the "UTC" zone ID (a `ZoneId` with fixed offset of 0). `ZoneOffset.UTC` is a `ZoneOffset` representing UTC with zero offset. Both represent UTC but have different types — `ZoneOffset` is a simple fixed offset, while `ZoneId` is a full timezone with rules. When you need UTC specifically, `ZoneOffset.UTC` is cleaner; when you need a full `ZoneId` for methods requiring a zone, use `ZoneId.of("UTC")`."

12. How does `LocalDateTime.parse()` handle invalid dates and what should you use instead?

Model Answer: "`LocalDateTime.parse(CharSequence)` uses `DateTimeFormatter.ISO_LOCAL_DATE_TIME` by default. If the input string is malformed, it throws `DateTimeParseException`. For untrusted input, wrap parsing in a try-catch or use `DateTimeFormatter` with a custom pattern and `setResolverStyle(ResolverStyle.STRICT)` for stricter validation. For user-facing input that may be flexible, use `DateTimeFormatter` with `ResolverStyle.SMART` or `LENIENT` as appropriate."

13. What is the purpose of `Instant` in logging and metric systems?

Model Answer: "`Instant` is the recommended type for log timestamps and metric events because it represents a point in time globally (UTC-based) without timezone ambiguity. `Instant.now()` is monotonic and well-suited for elapsed time calculation. Unlike `LocalDateTime` which is timezone-specific and can be ambiguous near DST transitions, `Instant` always refers to the same moment. Use `Instant` when the exact moment matters; use `ZonedDateTime` when the wall-clock display matters."

14. What does `Period.between()` return and how does it differ from `Duration.between()`?

Model Answer: "`Period.between(LocalDate start, LocalDate end)` returns a `Period` with years, months, and days — appropriate for calendar arithmetic. `Duration.between(Instant start, Instant end)` returns a `Duration` with seconds and nanoseconds — appropriate for precise elapsed time. If you call `Duration.between()` on two `LocalDate` values, it throws an exception because `LocalDate` is not a `Temporal` type that `Duration.between()` accepts."

15. What is the behavior of `ZonedDateTime` during the hour before or after DST transition?

Model Answer: "During the hour "skipped" by spring-forward (e.g., 2:00 AM becomes 3:00 AM in US), times in that gap are invalid and throw `DateTimeException`. During the hour "repeated" by fall-back (e.g., 1:00 AM happens twice), the later offset is used by default. `ZonedDateTime.ofLocal()` handles these by using the offset after the transition for ambiguous times and throwing for gaps. `ZonedDateTime.ofInstant()` can recreate any ambiguous time by specifying the offset explicitly."

16. What is the difference between `DateTimeFormatter.ofLocalizedDateTime()` and the ISO formatters?

Model Answer: "`DateTimeFormatter.ofLocalizedDateTime()` uses locale-specific formatting — for example, `FormatStyle.MEDIUM` produces "Jan 23, 2026" in US locale but "23 Jan 2026" in UK locale. The ISO formatters (`DateTimeFormatter.ISO_LOCAL_DATE_TIME`, etc.) always use the ISO format regardless of locale. Use localized formatters for displaying dates to users in their locale; use ISO formatters for machine-to-machine communication and storage."

17. What is the purpose of `Period.ofDays()`, `Period.ofMonths()`, and `Period.ofYears()` factory methods?

Model Answer: "These create a `Period` with only the specified unit set — `Period.ofDays(5)` creates a period of 5 days with 0 months and 0 years. They are more explicit than `Period.of(5, 0, 0)` because they document intent immediately. Use them when you need a simple period of a single unit. `Period.parse("P5D")` parses ISO-8601 duration format and produces the same result."

18. How does `TemporalUnit.between()` differ from `Duration.between()` and `Period.between()`?

Model Answer: "`TemporalUnit.between(Temporal start, Temporal end)` is the general-purpose method on the `TemporalUnit` interface (e.g., `ChronoUnit.DAYS.between(start, end)`) and can compute the difference between any two temporal types, returning the appropriate unit as a `long`. It handles the type conversion — for example, `ChronoUnit.DAYS.between(LocalDate.of(2026, 1, 1), LocalDate.of(2026, 1, 31))` returns 30. `Duration.between()` and `Period.between()` are specialized methods for specific temporal types."

19. What is the difference between `LocalDate.now(ZoneId)` and `ZonedDateTime.now(ZoneId).toLocalDate()`?

Model Answer: "Both return the current date in the specified zone. LocalDate.now(ZoneId) is the cleaner API — it directly returns the date portion of the current instant in that zone. ZonedDateTime.now(ZoneId).toLocalDate() creates an intermediate ZonedDateTime object and then extracts the date from it. The result is identical, but LocalDate.now(ZoneId) is more efficient and readable."

20. What happens when you call `plusDays()` versus `plus(Duration.ofDays(1))` on an `Instant`?

Model Answer: "instant.plusDays(1) does not exist on Instant — Instant only supports plus(long, TemporalUnit) or plus(Duration). instant.plus(Duration.ofDays(1)) works because Duration implements TemporalAmount. For Instant, use instant.plus(1, ChronoUnit.DAYS) or instant.plus(Duration.ofDays(1)). For LocalDate, plusDays(int) is the standard method. Mixing them can cause confusion — check the type before calling methods."

Further Reading

Conclusion

The java.time package addresses everything wrong with java.util.Date and java.util.Calendar — mutability, thread-safety issues, confusing API design, and zero-based month indexing. The new API is immutable by design, which makes it safe to share across threads and predictable across time-zone boundaries. Once you internalize the domain model (LocalDate for calendar days, Instant for machine timestamps, ZonedDateTime for scheduled events), you will find date-time code becomes significantly shorter and less bug-prone.

The most important decision in java.time is choosing the right type for the job. LocalDate stores a calendar date with no time component — use it for birthdays, expiration dates, and any date that is inherently calendar-aligned. Instant stores a point in UTC time — use it for log timestamps, metric events, and any moment that must be preserved across time zones. ZonedDateTime adds a time zone to a local date-time — use it for scheduled events that must fire at a specific wall-clock time in a specific region.

Duration vs Period is another critical distinction. Duration measures elapsed time in seconds and nanoseconds between two TemporalAccessor points — it is appropriate for measuring program execution time, latency metrics, and any time interval that is precisely defined. Period measures elapsed time in calendar units (days, months, years) between two LocalDate values — use it for age calculations, subscription periods, and any interval where calendar semantics matter more than precise milliseconds.

For formatting and parsing, use java.time.format.DateTimeFormatter with the patterns from java.time rather than the legacy java.text formatting classes. DateTimeFormatter is immutable and thread-safe, whereas SimpleDateFormat is neither. The pattern syntax is similar but the new API is more consistent and supports localization properly.

  • Use LocalDate for birthdays, holidays, and any date without a time component
  • Use Instant for all machine timestamps — log timestamps, metric events, and event sourcing
  • Use ZonedDateTime for scheduled events that must fire at a specific wall-clock time
  • Use Duration for precise elapsed time between instants; use Period for calendar-based intervals
  • Always use DateTimeFormatter from java.time.format — never SimpleDateFormat for new code
  • java.time types are immutable — methods like plusDays() return new instances, they do not mutate

Category

Related Posts

Abstract Classes in Java

Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.

#java-abstract-classes #java #java-fundamentals

Arithmetic Operators in Java

Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.

#java-arithmetic-operators #java #java-fundamentals

Array Basics in Java

Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.

#java-array-basics #java #java-fundamentals