Project: An Event Scheduler
Throughout this tutorial you have learned every major piece of java.time: local and zoned date-times, instants, durations, periods, formatting, parsing, and legacy interop. In this final lesson you assemble all of those pieces into a small but realistic Event Scheduler application that can store events, display them correctly in different time zones, find upcoming events, and format output for end users.
This is a deliberately focused project — roughly 200 lines of idiomatic Java — so you can see how every concept connects without getting lost in framework noise.
What the scheduler must do
- Accept an event name, a start date-time, and a
ZoneId (the time zone in which the event was created or will take place).
- Store events internally as
Instant values so they are time-zone-neutral on disk or in a database.
- Display any event in any requested time zone, converting on the fly.
- List all events that occur in the next N days from the caller's perspective.
- Format output in both machine-readable ISO-8601 and human-readable locale-sensitive strings.
The Event record
Start with a clean data model. A Java 17 record is perfect here — immutable, compact, and auto-generates equals, hashCode, and toString.
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public record Event(String name, Instant start, ZoneId originZone) {
/** Display this event in the given target zone and locale. */
public String displayIn(ZoneId targetZone, Locale locale) {
ZonedDateTime zdt = start.atZone(targetZone);
DateTimeFormatter fmt = DateTimeFormatter
.ofPattern("EEEE, dd MMMM yyyy 'at' HH:mm z", locale);
return name + " — " + zdt.format(fmt);
}
/** Machine-readable ISO-8601 string (always UTC). */
public String toIso() {
return start.toString(); // e.g. 2025-11-15T09:00:00Z
}
}
Why store as Instant? An Instant is a single point on the universal timeline — it has no time zone. Storing events this way means you never accidentally compare a New York time against a Tokyo time without an explicit conversion. The time zone is recorded separately as originZone only for auditing ("where was this event entered?"), not for arithmetic.
The EventScheduler service
The scheduler holds a list of events and provides domain operations. Notice how each method reaches for exactly the right java.time type.
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
public class EventScheduler {
private final List<Event> events = new ArrayList<>();
/**
* Add an event. The caller supplies the local start time and the
* zone it is expressed in; we convert to Instant immediately.
*/
public void addEvent(String name,
LocalDateTime localStart,
ZoneId zone) {
Instant start = localStart.atZone(zone).toInstant();
events.add(new Event(name, start, zone));
}
/**
* Return all events that start within the next [days] days,
* evaluated from [now] in the given [viewerZone].
*/
public List<Event> upcomingEvents(ZonedDateTime now, int days) {
Instant from = now.toInstant();
Instant to = now.plusDays(days).toInstant();
return events.stream()
.filter(e -> !e.start().isBefore(from)
&& e.start().isBefore(to))
.sorted(Comparator.comparing(Event::start))
.collect(Collectors.toList());
}
/**
* Print all events, displaying each in the viewer's zone.
*/
public void printAll(ZoneId viewerZone, Locale locale) {
if (events.isEmpty()) {
System.out.println("No events scheduled.");
return;
}
events.stream()
.sorted(Comparator.comparing(Event::start))
.forEach(e -> System.out.println(e.displayIn(viewerZone, locale)));
}
/** How long until (or since) a specific event, as a human string. */
public String timeUntil(Event event, ZonedDateTime now) {
Duration duration = Duration.between(now.toInstant(), event.start());
long hours = duration.toHours();
long minutes = duration.toMinutesPart();
if (duration.isNegative()) {
return event.name() + " already happened "
+ Math.abs(hours) + "h " + Math.abs(minutes) + "m ago";
}
return event.name() + " starts in " + hours + "h " + minutes + "m";
}
}
Use streams for filtering on Instant. Instant.isBefore() and Instant.isAfter() compare absolute points in time — they are always unambiguous regardless of daylight-saving changes. Never compare ZonedDateTime objects with < or >; use compareTo or convert to Instant first.
Parsing user input
Real applications receive date-time strings from forms or APIs. Add a factory method that parses a user-supplied string like "2025-11-15 09:00" and a zone string like "America/New_York":
import java.time.format.DateTimeParseException;
public static Optional<Event> parse(String name,
String dateTimeStr,
String zoneStr) {
DateTimeFormatter parser =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
try {
ZoneId zone = ZoneId.of(zoneStr);
LocalDateTime ldt = LocalDateTime.parse(dateTimeStr, parser);
Instant start = ldt.atZone(zone).toInstant();
return Optional.of(new Event(name, start, zone));
} catch (DateTimeParseException | java.time.zone.ZoneRulesException ex) {
System.err.println("Bad input: " + ex.getMessage());
return Optional.empty();
}
}
Always validate zone strings at parse time. ZoneId.of("America/NewYork") (missing underscore) throws ZoneRulesException at runtime. Wrap zone construction in a try-catch and return an Optional or throw a domain exception with a clear message. Never let a bad zone string silently default to UTC — that hides bugs for months.
Putting it all together — a main demo
public class Main {
public static void main(String[] args) {
EventScheduler scheduler = new EventScheduler();
// A conference in New York
scheduler.addEvent(
"Java Summit",
LocalDateTime.of(2025, 11, 15, 9, 0),
ZoneId.of("America/New_York")
);
// A webinar hosted in London
scheduler.addEvent(
"Cloud Architecture Webinar",
LocalDateTime.of(2025, 11, 15, 14, 0),
ZoneId.of("Europe/London")
);
// A team call in Tokyo
scheduler.addEvent(
"Asia-Pacific Team Sync",
LocalDateTime.of(2025, 11, 16, 10, 0),
ZoneId.of("Asia/Tokyo")
);
ZoneId viewerZone = ZoneId.of("Europe/Berlin");
Locale viewerLocale = Locale.ENGLISH;
System.out.println("=== All Events (Berlin time) ===");
scheduler.printAll(viewerZone, viewerLocale);
System.out.println();
System.out.println("=== Events in the next 2 days ===");
ZonedDateTime now = ZonedDateTime.of(
LocalDateTime.of(2025, 11, 15, 8, 0),
viewerZone
);
List<Event> upcoming = scheduler.upcomingEvents(now, 2);
upcoming.forEach(e -> System.out.println(e.displayIn(viewerZone, viewerLocale)));
System.out.println();
System.out.println("=== ISO-8601 machine output ===");
upcoming.forEach(e -> System.out.println(e.toIso()));
System.out.println();
System.out.println("=== Countdown ===");
upcoming.forEach(e -> System.out.println(scheduler.timeUntil(e, now)));
}
}
Sample output (times shown in Europe/Berlin which is UTC+1 in November):
=== All Events (Berlin time) ===
Saturday, 15 November 2025 at 15:00 CET — Java Summit
Saturday, 15 November 2025 at 15:00 CET — Cloud Architecture Webinar
Sunday, 16 November 2025 at 02:00 CET — Asia-Pacific Team Sync
=== Events in the next 2 days ===
Java Summit — Saturday, 15 November 2025 at 15:00 CET
Cloud Architecture Webinar — Saturday, 15 November 2025 at 15:00 CET
Asia-Pacific Team Sync — Sunday, 16 November 2025 at 02:00 CET
=== ISO-8601 machine output ===
2025-11-15T14:00:00Z
2025-11-15T14:00:00Z
2025-11-16T01:00:00Z
=== Countdown ===
Java Summit starts in 7h 0m
Cloud Architecture Webinar starts in 7h 0m
Asia-Pacific Team Sync starts in 18h 0m
Notice the coincidence. The New York 09:00 summit and the London 14:00 webinar both convert to 14:00 UTC — they start at exactly the same absolute moment. That is not a bug; UTC arithmetic revealed a real scheduling conflict that would have been invisible if you had compared local times naively.
Key design principles this project demonstrates
- Store as Instant, display as ZonedDateTime. Keep your persistence layer time-zone-agnostic; convert only at the presentation layer.
- Carry the origin zone for auditability, not for calculation. All arithmetic happens on
Instant or after an explicit conversion to a specific zone.
- Parse defensively. Wrap
ZoneId.of() and LocalDateTime.parse() in try-catch and surface errors early.
- Use
Duration for "time until" calculations. Duration.between() on two Instants returns a precise, DST-immune span.
- Format late, format explicitly. Pass
Locale to every formatter so day/month names appear in the viewer's language.
Summary
You have built a complete, timezone-aware event scheduler using nothing but the standard java.time API. The pattern — accept local input → convert to Instant immediately → store Instant → convert to the viewer's zone at display time — is the same pattern used in every serious calendar, booking, and scheduling system, from Google Calendar to airline reservation platforms. With this foundation you are equipped to handle time correctly in any Java application.