Instants and time zones
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
Instantis and when to use it. - Create a
ZoneIdfrom a region name such asAsia/Riyadh, and say why region names are preferred over fixed offsets. - Build a
ZonedDateTimeand convert it to another zone withwithZoneSameInstant. - Tell
withZoneSameInstantfromwithZoneSameLocal. - Describe what happens in a daylight saving gap and overlap.
- Choose between
LocalDate,LocalDateTime,InstantandZonedDateTimefor 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.
Instant i = Instant.parse("2026-05-04T07:00:00Z");
System.out.println(i + " " + i.getEpochSecond() + " " + i.plusSeconds(3600));2026-05-04T07:00:00Z 1777878000 2026-05-04T08:00:00ZInstant.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/LondonandAsia/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).
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.
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:00ZZonedDateTime.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:
| From | To | How |
|---|---|---|
Instant | ZonedDateTime | instant.atZone(zone) |
ZonedDateTime | Instant | zdt.toInstant() |
LocalDateTime | ZonedDateTime | ldt.atZone(zone) (you are saying which zone it was written in) |
ZonedDateTime | LocalDateTime | zdt.toLocalDateTime() (drops the zone) |
Converting to another zone: same instant or same local?
Two methods look alike and mean different things.
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 momentwithZoneSameInstantkeeps the moment and changes the clock reading. This is what you want when showing one meeting in another city.withZoneSameLocalkeeps 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).
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 occurrenceYou 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
| Requirement | Type |
|---|---|
| A birthday, a holiday, a due date | LocalDate |
| "Opens at 09:00" | LocalTime |
| A log line, a created-at, a payment time | Instant |
| A meeting or flight shown with its own zone | ZonedDateTime |
| A date and time on a form, zone not known yet | LocalDateTime (temporarily) |
Examples
Example 1: converting a meeting to London (simple)
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());2026-05-04T10:00+03:00[Asia/Riyadh]
2026-05-04T08:00+01:00[Europe/London]
2026-05-04T07:00:00ZThree views of one moment. The offset in brackets tells you which rules applied.
Example 2: the same city, different offsets (practical)
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));
}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.
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")));Asia/Riyadh 2026-11-01T08:30+03:00[Asia/Riyadh]
America/New_York 2026-11-01T01:30-04:00[America/New_York]
trueThe 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:
- A request arrives with a time (an ISO string with
Zor an offset, or a local time plus the user's zone). - The service converts it to an
Instantand stores it. - When responding, it converts the
Instantto 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.
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:
- The five lines show different local times for one instant.
- You used region IDs, not fixed offsets.
- If you try an invalid ID on purpose, you can read the
DateTimeExceptionmessage.
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
Instantis one exact moment on the UTC timeline, best for timestamps.ZoneIdnames a region and its rules; preferArea/CityIDs to fixed offsets.ZonedDateTimeis a date, time and zone.withZoneSameInstantkeeps the moment;withZoneSameLocalkeeps 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
- Which type is a point on the global timeline with no zone?
- Why should you prefer
Europe/Londonover+01:00? - Which method keeps the moment when changing zone?
- What does Java do with
02:30on the day clocks jump from 02:00 to 03:00? - Give two reasons to store timestamps in UTC.
Show answers
Instant.- The region ID knows when its offset changes during the year; the fixed offset does not.
withZoneSameInstant.- The time does not exist, so Java shifts it forward by the length of the gap (to 03:30).
- 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
Ztext form, andatZone. - 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
withZoneSameInstantandwithZoneSameLocal. - 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.
Marking a lesson complete is optional and you can undo it any time. Your progress stays in this browser.