The java.time API offers several types to represent moments in time, and choosing the wrong one to compute durations can produce incorrect answers, especially around time zone differences and Daylight Saving Time (DST) transitions.

Why is this an issue?

The LocalDateTime class in the java.time API represents calendar and clock-face information without any time zone or offset from the UTC timeline. Arithmetic on this type is therefore a pure calendar and clock-face delta, not a measure of physical elapsed time.

When the two operands of Duration.between(time1, time2) or ChronoUnit.X.between(time1, time2) actually correspond to moments observed in different time zones, the result silently differs from the real elapsed duration by the offset between the two zones and by any Daylight Saving Time (DST) transition occurring within the window. To obtain the real physical elapsed time, both operands must first be anchored to their zone by being converted to Instant or ZonedDateTime.

What is the potential impact?

The computation does not throw any exception, so the wrong duration silently propagates into downstream logic, such as billing periods, service-level agreement (SLA) timers, scheduling windows, or travel itineraries. The defect is easy to miss in tests that run in a single time zone, and may only surface once the application is deployed across regions or runs through a DST transition.

How to fix it

When computing a duration between two LocalDateTime values observed in known time zones, convert each one with .atZone(ZoneId) first, then apply Duration.between or ChronoUnit.X.between to the resulting ZonedDateTime (or Instant) values.

Code examples

Noncompliant code example

The duration below is computed between two LocalDateTime values observed in different zones. Because LocalDateTime carries no offset, the result is a purely arithmetic delta that ignores the 6-hour difference between New York and Paris.

LocalDateTime departureNY = LocalDateTime.of(2026, 3, 28, 22, 0);  // America/New_York
LocalDateTime arrivalParis = LocalDateTime.of(2026, 3, 29, 11, 0); // Europe/Paris
long hours = ChronoUnit.HOURS.between(departureNY, arrivalParis);  // Noncompliant: returns 13

Compliant solution

Anchoring each LocalDateTime to its zone yields the real physical elapsed time.

ZonedDateTime departure = LocalDateTime.of(2026, 3, 28, 22, 0)
    .atZone(ZoneId.of("America/New_York"));
ZonedDateTime arrival = LocalDateTime.of(2026, 3, 29, 11, 0)
    .atZone(ZoneId.of("Europe/Paris"));
long hours = ChronoUnit.HOURS.between(departure, arrival); // Compliant: returns 7 hours

Resources

Documentation

Related rules