Why Time is a Pain (and How Kotlin Treats It)
\n\nIf you've ever written an application that serves users from different time zones, you know this pain. A request comes with a timestamp, the database stores UTC, a user in New York complains that a notification arrived at 3 AM, while in the database it's 8 AM. Sound familiar?
\n\nWorking with time zones is that area of programming where apparent simplicity turns into dozens of bugs. But Kotlin, having inherited the best practices from Java, gives us a powerful arsenal to fight the chaos. In this article, we'll break down how to properly work with time zones in Kotlin, avoid common mistakes, and write code that won't break during daylight saving time transitions.
\n\nA Brief Overview: What is a Time Zone?
\n\nBefore diving into code, let's agree on terms. A time zone is not just an offset from UTC. It also includes a set of rules for daylight saving time (DST) transitions. For example, Europe/Moscow is not just +3 hours, it also includes historical changes (in 2014, Russia stopped changing clocks).
In the Java/Kotlin world, two key concepts handle this:
\n\n- \n
- ZoneId — a time zone identifier (e.g., "America/New_York"). Contains the full DST history. \n
- ZoneOffset — just an offset (e.g., +05:00). No transition rules. \n
The difference is critical: if you use ZoneOffset instead of ZoneId, your code will break on the day clocks are changed. Remember this.
Basic java.time Classes in Kotlin
\n\nKotlin fully supports the java.time package (Java 8+). If you're writing for Android, don't forget to add desugaring or use ThreeTenABP. For server-side Kotlin, there are no issues.
Instant — A Moment in Time
\n\nInstant is an absolute point on the timeline. It is not tied to any time zone. This is what you should store in the database.
import java.time.Instant\n\nfun main() {\n val now = Instant.now()\n println(now) // 2025-03-15T10:30:00.123Z\n}\n\nNotice the letter Z at the end — this is Zulu time, i.e., UTC. Store everything in UTC — this is the golden rule.
ZonedDateTime — Time with Zone Awareness
\n\nWhen you need to display time to a user, use ZonedDateTime. It stores date, time, offset, and zone identifier.
import java.time.ZonedDateTime\nimport java.time.ZoneId\n\nfun main() {\n val moscowTime = ZonedDateTime.now(ZoneId.of("Europe/Moscow"))\n println(moscowTime) // 2025-03-15T13:30:00.123+03:00[Europe/Moscow]\n}\n\nSee the square brackets? That's the ZoneId, which allows the system to correctly handle DST.
OffsetDateTime — Time with Offset
\n\nIf you don't need zone information, only the offset, use OffsetDateTime. It is lighter and faster.
import java.time.OffsetDateTime\nimport java.time.ZoneOffset\n\nfun main() {\n val offsetTime = OffsetDateTime.now(ZoneOffset.ofHours(5))\n println(offsetTime) // 2025-03-15T15:30:00.123+05:00\n}\n\nBut remember: if you save OffsetDateTime for New York, and then the US switches to daylight saving time, your offset will become outdated.
Converting Between Time Zones in Kotlin
\n\nThe most common task is to take time from the database (UTC) and display it in the user's time zone. Let's break down how to do this correctly.
\n\nFrom UTC to Local Zone
\n\nimport java.time.Instant\nimport java.time.ZoneId\nimport java.time.ZonedDateTime\n\nfun convertUtcToLocal(utcInstant: Instant, targetZone: String): ZonedDateTime {\n return utcInstant.atZone(ZoneId.of(targetZone))\n}\n\nfun main() {\n val utcNow =