Skip to content
Ameen Abozaid
PublishedBEG-033-L03

Instants and time zones

  • Lesson 3 of 5
  • 10 min read
  • Beginner
On This Lesson 11 topics

Lesson Overview

Lesson 2 gave you dates and times with no zone. This lesson adds the missing piece: how to name an exact moment and how to show it on the clock of any city. Think of the wall of world clocks again. An Instant is the moment itself; a ZoneId chooses which clock you read; a ZonedDateTime is the moment already read on one clock.

This is the heart of the module. It leads to the rule used by almost every backend system: store timestamps in UTC, and convert to the user's zone only for display. You will see why, and you will see what Java does when a local time falls into the hour that daylight saving skips or repeats.

You need Lessons 1 and 2, and exceptions from BEG-029.

Learning Objectives

By the end of this lesson you will be able to:

  • Explain what an Instant is and when to use it.
  • Create a ZoneId from a region name such as Asia/Riyadh, and say why region names are preferred over fixed offsets.
  • Build a ZonedDateTime and convert it to another zone with withZoneSameInstant.
  • Tell withZoneSameInstant from withZoneSameLocal.
  • Describe what happens in a daylight saving gap and overlap.
  • Choose between LocalDate, LocalDateTime, Instant and ZonedDateTime for a requirement.
  • Explain why timestamps should be stored in UTC and converted for display.

Written Explanation

Instant: a point on the timeline

An Instant is one point on the global timeline. The Javadoc describes it as a count of seconds (plus nanoseconds) from the epoch, 1970-01-01T00:00:00Z. It has no zone, because it needs none: a moment is a moment. Its text form is always in UTC and ends with Z.

java
Instant i = Instant.parse("2026-05-04T07:00:00Z");
System.out.println(i + " " + i.getEpochSecond() + " " + i.plusSeconds(3600));
text
2026-05-04T07:00:00Z 1777878000 2026-05-04T08:00:00Z

Instant.now() gives the current moment. Because it is independent of zone, two machines anywhere in the world get the same value for the same moment (provided their clocks are right). That is what makes it the best type for "when did this happen".

ZoneId: which clock to read

A ZoneId identifies a set of rules for finding the offset from UTC. There are two kinds, according to the Javadoc:

  • Region IDs such as Europe/London and Asia/Riyadh. Their rules can change the offset during the year.
  • Fixed offsets (ZoneOffset) such as +03:00, which never change.

Prefer region IDs for anything that has a real-world location. Europe/London knows that its offset is +00:00 in winter and +01:00 in summer; the fixed offset +01:00 does not. The default source of region data is the IANA Time Zone Database. Use exact spellings: a made-up ID like Riyadh throws a DateTimeException (Unknown time-zone ID: Riyadh).

java
ZoneId riyadh = ZoneId.of("Asia/Riyadh");

ZonedDateTime: date, time and zone

A ZonedDateTime combines a local date-time, a zone, and the resulting offset. It identifies one exact moment, in a way readers can understand.

java
ZonedDateTime z = ZonedDateTime.of(2026, 5, 4, 10, 0, 0, 0, ZoneId.of("Asia/Riyadh"));
System.out.println(z);               // 2026-05-04T10:00+03:00[Asia/Riyadh]
System.out.println(z.toInstant());   // 2026-05-04T07:00:00Z

ZonedDateTime.of takes year, month, day, hour, minute, second, nanosecond and the zone. The arguments are many; you can also build one from a LocalDateTime with ldt.atZone(zone), or from an Instant with instant.atZone(zone).

Converting between the types:

FromToHow
InstantZonedDateTimeinstant.atZone(zone)
ZonedDateTimeInstantzdt.toInstant()
LocalDateTimeZonedDateTimeldt.atZone(zone) (you are saying which zone it was written in)
ZonedDateTimeLocalDateTimezdt.toLocalDateTime() (drops the zone)

Converting to another zone: same instant or same local?

Two methods look alike and mean different things.

java
ZonedDateTime london = z.withZoneSameInstant(ZoneId.of("Europe/London"));
// 2026-05-04T08:00+01:00[Europe/London]   same moment, London's clock
ZonedDateTime other = z.withZoneSameLocal(ZoneId.of("Europe/London"));
// 2026-05-04T10:00+01:00[Europe/London]   same clock reading, a different moment
  • withZoneSameInstant keeps the moment and changes the clock reading. This is what you want when showing one meeting in another city.
  • withZoneSameLocal keeps the clock reading and changes the moment. Use it only when you really mean "10:00 wherever it is".

Mixing them up gives a meeting that is an hour or more off.

Gaps and overlaps

Because zones change their offset, some local date-times are special. The ZonedDateTime Javadoc names three cases:

  • Normal: one valid offset.
  • Gap: clocks jump forward, so the local time does not exist. Java shifts the time forward by the length of the gap.
  • Overlap: clocks go back, so the local time occurs twice. Java keeps the earlier offset by default (or the previous offset, if it is valid).
java
ZoneId ny = ZoneId.of("America/New_York");
ZonedDateTime.of(LocalDateTime.of(2026, 3, 8, 2, 30), ny);
// 2026-03-08T03:30-04:00[America/New_York]    gap: 02:30 does not exist
ZonedDateTime.of(LocalDateTime.of(2026, 11, 1, 1, 30), ny);
// 2026-11-01T01:30-04:00[America/New_York]    overlap: earlier offset
ZonedDateTime.of(LocalDateTime.of(2026, 11, 1, 1, 30), ny).withLaterOffsetAtOverlap();
// 2026-11-01T01:30-05:00[America/New_York]    the second occurrence

You do not need to memorize the method names. Remember the idea: a local time plus a zone is usually one moment, but at two times a year it can be none or two, and Java makes a documented choice.

Why store in UTC (outcome 4)

Now the rule can be explained.

  • A UTC timestamp (an Instant) is one fixed reference. It has no daylight saving, so it never skips or repeats.
  • Sorting and comparing UTC timestamps is correct: later moments are always larger. Local times are not: in the repeated hour, two different moments have the same clock reading.
  • Data from users in many zones agrees, because everything is on one scale.
  • The user's zone is a display concern. A person in Riyadh sees +03:00, a person in New York sees their own offset, both derived from the same stored value.
  • Zone rules change (governments adjust them). If you store only the instant, a changed rule alters how it is displayed, never which moment it was.

So: store Instant (UTC), convert to ZonedDateTime at the edges. Persisting a zone ID alongside it is only needed when the zone itself is part of the data (for example, a flight's departure city).

Which type? A decision guide

RequirementType
A birthday, a holiday, a due dateLocalDate
"Opens at 09:00"LocalTime
A log line, a created-at, a payment timeInstant
A meeting or flight shown with its own zoneZonedDateTime
A date and time on a form, zone not known yetLocalDateTime (temporarily)

Examples

Example 1: converting a meeting to London (simple)

java
ZonedDateTime riyadh = ZonedDateTime.of(2026, 5, 4, 10, 0, 0, 0, ZoneId.of("Asia/Riyadh"));
System.out.println(riyadh);
System.out.println(riyadh.withZoneSameInstant(ZoneId.of("Europe/London")));
System.out.println(riyadh.toInstant());
text
2026-05-04T10:00+03:00[Asia/Riyadh]
2026-05-04T08:00+01:00[Europe/London]
2026-05-04T07:00:00Z

Three views of one moment. The offset in brackets tells you which rules applied.

Example 2: the same city, different offsets (practical)

java
ZoneId lon = ZoneId.of("Europe/London");
for (String s : new String[]{"2026-03-28T12:00:00Z", "2026-03-29T12:00:00Z"}) {
    System.out.println(Instant.parse(s).atZone(lon));
}
text
2026-03-28T12:00Z[Europe/London]
2026-03-29T13:00+01:00[Europe/London]

Both instants are 12:00 UTC, one day apart. The first day London is on winter time (Z, offset zero); the second day, clocks have gone forward, and 12:00 UTC is 13:00 in London. A fixed offset would show 12:00 both days. (Dates are for 2026 as computed by JDK 21's time zone data.)

Example 3: a "created at" column shown to two users (realistic)

A service stores created as 2026-11-01T05:30:00Z. Two users read it.

java
Instant created = Instant.parse("2026-11-01T05:30:00Z");
for (String id : new String[]{"Asia/Riyadh", "America/New_York"}) {
    System.out.println(id + " " + created.atZone(ZoneId.of(id)));
}
System.out.println(created.isBefore(Instant.parse("2026-11-01T06:00:00Z")));
text
Asia/Riyadh 2026-11-01T08:30+03:00[Asia/Riyadh]
America/New_York 2026-11-01T01:30-04:00[America/New_York]
true

The Riyadh user sees 08:30 and the New York user 01:30, on the day New York's clocks go back. One stored value, two correct displays, and the comparison with isBefore is done on the instant, not on clock readings.

Backend Insight

In a real service the pattern is almost always:

  1. A request arrives with a time (an ISO string with Z or an offset, or a local time plus the user's zone).
  2. The service converts it to an Instant and stores it.
  3. When responding, it converts the Instant to the zone the user wants, often sent in the request or stored in the user profile.

Keep the database and domain logic in UTC, and do zone conversion in one place at the edge. When a bug report says "this time is off by an hour", ask three questions: what is the stored value, what zone was used to convert, and was withZoneSameInstant or withZoneSameLocal used?

Careful: some databases and drivers have their own rules for time columns and for session time zones. That is covered when you reach databases (JNR-028); here, the Java side of the rule is enough.

Common Mistakes

Mistake 1: LocalDateTime for events in several zones

A LocalDateTime cannot be converted to another zone, because it has no zone. Use ZonedDateTime or Instant. Lesson 5 shows the compile error you get.

Mistake 2: fixed offsets instead of region IDs

ZoneOffset.ofHours(1) for London is right in summer and wrong in winter. Use Europe/London.

Mistake 3: withZoneSameLocal when you meant withZoneSameInstant

The first keeps the clock reading and changes the moment. Our Riyadh meeting would become 10:00 in London: two hours too late.

Mistake 4: misspelled or invented zone IDs

ZoneId.of("Riyadh") throws DateTimeException. Use the Area/City form from the IANA database.

Mistake 5: assuming a local time always exists exactly once

In a daylight saving gap it does not exist, and in an overlap it exists twice. Java picks a default; decide whether the default fits your business.

Exercises

Try each exercise yourself before opening the solutions. Running code depends on your IDE or terminal.

Level A: Foundation

A1. Choose a type for each: (a) a payment received, (b) a public holiday, (c) a flight leaving Jeddah with its own local time.

Show solution A1

A1. (a) Instant. (b) LocalDate. (c) ZonedDateTime, because the departure belongs to its zone.

A2. What does the Z at the end of 2026-05-04T07:00:00Z mean?

Show solution A2

A2. Z means UTC (offset zero).

A3. Why is Asia/Riyadh a better choice than +03:00 for a city's time, and when would a fixed offset also be fine?

Show solution A3

A3. Region names carry the rules for offset changes (such as daylight saving) for that place. A fixed offset never changes, so it is fine when you truly mean "always +03:00", for example when a protocol states an explicit offset or the place does not change its clocks. Prefer the region ID when the place matters.

Level B: Application

B1. For the instant 2026-01-15T12:00:00Z, write the clock readings in Asia/Riyadh, Europe/London and America/New_York. Use atZone.

Show solution B1

B1.

java
Instant i = Instant.parse("2026-01-15T12:00:00Z");
i.atZone(ZoneId.of("Asia/Riyadh"));        // 2026-01-15T15:00+03:00[Asia/Riyadh]
i.atZone(ZoneId.of("Europe/London"));      // 2026-01-15T12:00Z[Europe/London]
i.atZone(ZoneId.of("America/New_York"));   // 2026-01-15T07:00-05:00[America/New_York]

All three results were produced by running the code on JDK 21. In January neither London nor New York is on summer time.

B2. Given z = 2026-05-04T10:00+03:00[Asia/Riyadh], what are z.withZoneSameInstant(ZoneId.of("Europe/London")) and z.withZoneSameLocal(ZoneId.of("Europe/London"))?

Show solution B2

B2. withZoneSameInstant gives 2026-05-04T08:00+01:00[Europe/London] (same moment). withZoneSameLocal gives 2026-05-04T10:00+01:00[Europe/London] (same clock reading, one moment two hours later).

B3. A database column holds created_at. Say which Java type you would map it to, and how you would show it to a user in Riyadh.

Show solution B3

B3. Map it to Instant, which is a UTC moment. To show it to a user in Riyadh, call created.atZone(ZoneId.of("Asia/Riyadh")) and format it (Lesson 4). Mapping details depend on your database driver, which a later module covers.

Level C: Challenge

C1. What is ZonedDateTime.of(LocalDateTime.of(2026, 3, 8, 2, 30), ZoneId.of("America/New_York")) and why?

Show solution C1

C1. 2026-03-08T03:30-04:00[America/New_York]. On 8 March 2026 New York's clocks jump forward from 02:00 to 03:00, so 02:30 does not exist. Java shifts the time forward by the length of the gap, as the Javadoc describes.

C2. What does LocalDateTime.of(2026, 11, 1, 1, 30) at America/New_York mean, and how can you pick the second occurrence?

Show solution C2

C2. On 1 November 2026 the clocks go back and 01:30 happens twice. By default Java uses the earlier offset, -04:00. Calling .withLaterOffsetAtOverlap() on the result gives the second occurrence, 2026-11-01T01:30-05:00[America/New_York].

Mini Challenge

Write WorldClock. Take an Instant of your choice (use Instant.parse) and print it in five lines, one per region ID you choose from Asia/Riyadh, Europe/London, America/New_York, Asia/Tokyo, and one more region you look up. Use withZoneSameInstant at least once on a ZonedDateTime too.

Check your work:

  1. The five lines show different local times for one instant.
  2. You used region IDs, not fixed offsets.
  3. If you try an invalid ID on purpose, you can read the DateTimeException message.

Bonus: run the same program with an instant in January and one in July, and note which cities changed their offset. Explain why Riyadh did not.

Summary & Next Lesson

Key takeaways

  • Instant is one exact moment on the UTC timeline, best for timestamps.
  • ZoneId names a region and its rules; prefer Area/City IDs to fixed offsets.
  • ZonedDateTime is a date, time and zone. withZoneSameInstant keeps the moment; withZoneSameLocal keeps the clock reading.
  • Local times can fall in a daylight saving gap or overlap, and Java has documented defaults.
  • Store UTC Instants and convert to the user's zone only for display.

Next lesson

The next lesson, "Durations, periods, formatting", shows how to measure the gap between two values with Duration and Period, and how to print and read dates with DateTimeFormatter.

Self-check

  1. Which type is a point on the global timeline with no zone?
  2. Why should you prefer Europe/London over +01:00?
  3. Which method keeps the moment when changing zone?
  4. What does Java do with 02:30 on the day clocks jump from 02:00 to 03:00?
  5. Give two reasons to store timestamps in UTC.
Show answers
  1. Instant.
  2. The region ID knows when its offset changes during the year; the fixed offset does not.
  3. withZoneSameInstant.
  4. The time does not exist, so Java shifts it forward by the length of the gap (to 03:30).
  5. UTC has no daylight saving, so values never skip or repeat and sort correctly; and data from many zones agrees, with each user's zone applied only when displaying.

References

  • Oracle, java.time.Instant (Java SE 21). Supports the epoch model, the UTC Z text form, and atZone.
  • Oracle, java.time.ZoneId (Java SE 21). Supports the difference between region IDs and fixed offsets and the IANA Time Zone Database as default source.
  • Oracle, java.time.ZonedDateTime (Java SE 21). Supports gap and overlap handling and the behavior of withZoneSameInstant and withZoneSameLocal.
  • Printed outputs came from running the code on JDK 21. The statement that governments adjust zone rules is general background not checked on a specific page here, and database/driver behavior was not verified.

Practice

Apply this lesson with the module's lab, exercises and quiz.

Lab and exercises Quiz

Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.

Continue to next lesson

Type to search levels, phases, modules and lessons.