Date & Time API

Project: An Event Scheduler

15 min Lesson 10 of 13

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

  1. Store as Instant, display as ZonedDateTime. Keep your persistence layer time-zone-agnostic; convert only at the presentation layer.
  2. Carry the origin zone for auditability, not for calculation. All arithmetic happens on Instant or after an explicit conversion to a specific zone.
  3. Parse defensively. Wrap ZoneId.of() and LocalDateTime.parse() in try-catch and surface errors early.
  4. Use Duration for "time until" calculations. Duration.between() on two Instants returns a precise, DST-immune span.
  5. 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.