Kotlin
kotlinx-datetime, Instant vs LocalDateTime, and java.time interop.
Kotlin’s idiomatic, multiplatform answer to date and time is the kotlinx-datetime library. It deliberately offers a small set of distinct types instead of one overloaded class: Instant (an absolute point in time), LocalDateTime (a civil date-and-time with no zone), LocalDate (a calendar date), LocalTime (a time of day), and TimeZone. Durations come from the Kotlin standard library’s kotlin.time.Duration, while calendar-aware spans use kotlinx-datetime’s DateTimePeriod / DatePeriod.
The central distinction to hold onto is Instant versus LocalDateTime. An Instant names a unique moment on the global timeline — it is the equivalent of an “aware UTC” value and is what you should store when recording when something happened. A LocalDateTime is the naive value: 2026-03-29T01:30 carries no zone, so by itself it does not identify a moment. You cannot move between the two without supplying a TimeZone. See Instant vs civil time and Time zones vs offsets.
A second framing worth keeping: LocalDate names a period — a whole calendar day — while an Instant names a point. They are different kinds of value, not two precisions of one idea. See Points vs periods.
The safe default for “now” is Clock.System.now(), which returns an Instant. To get a human-readable civil reading you convert through a zone: now.toLocalDateTime(TimeZone.of("Europe/Paris")). The reverse, localDateTime.toInstant(timeZone), is the only correct way to pin a naive wall-clock value to the timeline.
Instant vs LocalDateTime
Because Instant and LocalDateTime are separate types, the naive/aware confusion that plagues many languages is caught by the compiler rather than at runtime: there is no implicit conversion, so you are forced to name a TimeZone at the boundary. The remaining hazard is choosing the wrong zone — converting an Instant through TimeZone.currentSystemDefault() when you meant a fixed business zone gives a value that silently changes with the host machine.
Pitfall: A LocalDateTime does not identify a moment in time. Treating one as if it were absolute — comparing two LocalDateTimes that were meant for different zones, or assuming it is UTC — produces results that are off by an offset and shift across DST transitions. Convert to Instant with an explicit TimeZone before comparing or subtracting.
Pitfall: Adding a kotlin.time.Duration (e.g. instant + 24.hours) advances exact elapsed time, not civil time. Across a spring-forward night, “24 hours later” is not “the same wall-clock time tomorrow”. For calendar arithmetic that respects DST, use instant.plus(1, DateTimeUnit.DAY, timeZone) instead.
See also: Instant vs civil time and Parsing user input.
Go deeper: conversions, DST-safe arithmetic, and java.time interop
The core types and conversions:
import kotlinx.datetime.*
val now: Instant = Clock.System.now() // an absolute instant
val tz = TimeZone.of("Europe/Paris")
val civil: LocalDateTime = now.toLocalDateTime(tz) // instant -> wall clock
val back: Instant = civil.toInstant(tz) // wall clock -> instant
val today: LocalDate = now.toLocalDateTime(tz).date // a calendar day (a period)
Parsing is type-directed, so the value you ask for is the value you get — no format guessing:
Instant.parse("2026-06-05T14:00:00Z") // absolute; offset (Z) required
LocalDate.parse("2026-06-05") // a date, never silently an instant
LocalDateTime.parse("2026-06-05T14:00") // civil; stays zone-less by design
Exact vs calendar arithmetic — the distinction behind pitfall #2:
import kotlin.time.Duration.Companion.hours
val tz = TimeZone.of("Europe/London")
val start = LocalDateTime(2026, 3, 29, 1, 30).toInstant(tz)
val exact = start + 24.hours // exactly 24h later
val nextDay = start.plus(1, DateTimeUnit.DAY, tz) // "same time tomorrow", DST-aware
// Across the spring-forward night these land on different instants.
(One caveat shared by every language: the “same wall-clock time tomorrow” can fall in a DST gap or fold, where the civil reading either does not exist or is ambiguous.)
Interop with java.time on Kotlin/JVM. kotlinx-datetime is multiplatform (JVM, JS, Native, Wasm); on the JVM it is implemented on top of java.time, and you can cross over explicitly when a Java API demands the standard types:
val javaInstant: java.time.Instant = Clock.System.now().toJavaInstant()
val ktInstant: Instant = javaInstant.toKotlinInstant()
If you are JVM-only and never need multiplatform code, using java.time directly is perfectly idiomatic — its Instant / LocalDateTime / ZonedDateTime draw the same boundaries. (Note that recent Kotlin releases are moving Instant and Clock into the standard library under kotlin.time; kotlinx-datetime is aligning with that, so check the versions in your build.)
Inherited java.util.Date / Calendar code is the JVM’s equivalent of JavaScript’s legacy Date: a java.util.Date is always an instant (no date-only or civil type), the deprecated constructors count years from 1900 and months from 0, and Calendar arithmetic silently clamps. Convert at the boundary rather than propagating it: legacy.toInstant().toKotlinInstant().
See also Time zones vs offsets and DST.