Recurring events are a fundamental part of scheduling in many domains—whether it’s a team meeting every Monday, a payroll run on the last day of each month, or reminders that recur every 15 days. Java’s modern date-time API provides powerful tools to model such recurrence patterns in a readable, maintainable way.
In this section, we’ll explore how to model recurring events using the java.time
package. Specifically, we'll look at using TemporalAdjusters
, DayOfWeek
, and simple iteration patterns with LocalDate
to generate recurring event dates.
Let’s begin with a common case—finding every Monday starting from a given date.
import java.time.*;
import java.time.temporal.TemporalAdjusters;
import java.util.ArrayList;
import java.util.List;
public class WeeklyRecurringEvent {
public static void main(String[] args) {
LocalDate start = LocalDate.of(2025, 6, 1);
List<LocalDate> mondays = new ArrayList<>();
LocalDate nextMonday = start.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY));
for (int i = 0; i < 5; i++) {
mondays.add(nextMonday);
nextMonday = nextMonday.plusWeeks(1);
}
mondays.forEach(System.out::println);
}
}
Output:
2025-06-02
2025-06-09
2025-06-16
2025-06-23
2025-06-30
This approach uses TemporalAdjusters.nextOrSame()
to align the start date to the next Monday and a loop with plusWeeks(1)
to generate a fixed number of recurrences.
Some events recur monthly but not on a fixed date. For example, the "first Friday of every month" can be modeled as follows:
import java.time.*;
import java.time.temporal.TemporalAdjusters;
public class MonthlyFirstFriday {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2025, 1, 1);
for (int i = 0; i < 6; i++) {
LocalDate firstFriday = date.with(TemporalAdjusters.firstInMonth(DayOfWeek.FRIDAY));
System.out.println(firstFriday);
date = date.plusMonths(1);
}
}
}
Output:
2025-01-03
2025-02-07
2025-03-07
2025-04-04
2025-05-02
2025-06-06
This demonstrates how firstInMonth()
helps you directly capture human-centric recurrence rules without error-prone logic.
For simple date intervals (like "every 15 days"), a loop using plusDays()
works well:
LocalDate start = LocalDate.of(2025, 6, 1);
for (int i = 0; i < 5; i++) {
System.out.println(start.plusDays(i * 15));
}
This works best for evenly spaced patterns where weekday or month alignment isn’t needed.
Recurring schedules are rarely perfect. Consider holidays or variable month lengths:
You can layer in additional checks:
// Example: Skip weekends
if (!date.getDayOfWeek().equals(DayOfWeek.SATURDAY) &&
!date.getDayOfWeek().equals(DayOfWeek.SUNDAY)) {
// valid business day
}
Handling holidays may require integrating with a holiday calendar API or service.
Modeling recurring events in Java is straightforward with the java.time
API:
TemporalAdjusters
for human-friendly recurrences like “first Monday”.plusDays
, plusWeeks
, plusMonths
) for interval-based recurrences.The clarity and composability of these tools make them ideal for implementing business-grade scheduling features.
ChronoUnit
for Custom IntervalsThe ChronoUnit
enum in the java.time.temporal
package provides a powerful and expressive way to work with temporal units such as days, weeks, months, and years. It enables developers to perform operations like date/time arithmetic and calculating intervals in a clean and concise way.
In this section, we’ll demonstrate how to use ChronoUnit
for adding, subtracting, and iterating with custom intervals between LocalDate
and LocalDateTime
values.
ChronoUnit
The plus(long amountToAdd, TemporalUnit unit)
and minus(long amountToSubtract, TemporalUnit unit)
methods can be used on any temporal object (like LocalDate
, LocalTime
, or LocalDateTime
) to shift values by a specific unit.
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
public class ChronoUnitAddExample {
public static void main(String[] args) {
LocalDate today = LocalDate.now();
LocalDate threeDaysLater = today.plus(3, ChronoUnit.DAYS);
LocalDate twoMonthsLater = today.plus(2, ChronoUnit.MONTHS);
System.out.println("Today: " + today);
System.out.println("3 Days Later: " + threeDaysLater);
System.out.println("2 Months Later: " + twoMonthsLater);
}
}
LocalDate oneYearAgo = LocalDate.now().minus(1, ChronoUnit.YEARS);
System.out.println("One Year Ago: " + oneYearAgo);
Using ChronoUnit
this way provides better readability and flexibility than hardcoding operations like plusDays()
or minusMonths()
.
You can use ChronoUnit.between(start, end)
to calculate the difference between two dates or times in a specific unit.
LocalDate start = LocalDate.of(2025, 1, 1);
LocalDate end = LocalDate.of(2025, 6, 1);
long days = ChronoUnit.DAYS.between(start, end);
long months = ChronoUnit.MONTHS.between(start, end);
System.out.println("Days between: " + days);
System.out.println("Months between: " + months);
Note that results depend on how the unit maps to calendar time. For example, months can have different lengths, so be cautious when using ChronoUnit.MONTHS
for financial or scheduling logic.
ChronoUnit
also shines when building loops with custom intervals—like generating a sequence of dates every 3 days or every 2 months.
LocalDate start = LocalDate.of(2025, 6, 1);
LocalDate end = LocalDate.of(2025, 6, 15);
for (LocalDate date = start; date.isBefore(end); date = date.plus(3, ChronoUnit.DAYS)) {
System.out.println(date);
}
LocalDate start = LocalDate.of(2025, 1, 1);
LocalDate end = LocalDate.of(2025, 12, 31);
for (LocalDate date = start; date.isBefore(end); date = date.plus(2, ChronoUnit.MONTHS)) {
System.out.println(date);
}
These loops are efficient for generating recurring schedules, periodic reports, billing cycles, or reminders.
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
public class ChronoUnitExamples {
public static void main(String[] args) {
System.out.println("=== Example 2: Subtracting Time ===");
LocalDate oneYearAgo = LocalDate.now().minus(1, ChronoUnit.YEARS);
System.out.println("One Year Ago: " + oneYearAgo);
System.out.println("\n=== Example 3: Time Difference in Days and Months ===");
LocalDate start = LocalDate.of(2025, 1, 1);
LocalDate end = LocalDate.of(2025, 6, 1);
long daysBetween = ChronoUnit.DAYS.between(start, end);
long monthsBetween = ChronoUnit.MONTHS.between(start, end);
System.out.println("Days between: " + daysBetween);
System.out.println("Months between: " + monthsBetween);
System.out.println("\n=== Example 4: Iterating Every 3 Days ===");
LocalDate start3Day = LocalDate.of(2025, 6, 1);
LocalDate end3Day = LocalDate.of(2025, 6, 15);
for (LocalDate date = start3Day; date.isBefore(end3Day); date = date.plus(3, ChronoUnit.DAYS)) {
System.out.println(date);
}
System.out.println("\n=== Example 5: Iterating Every 2 Months ===");
LocalDate start2Months = LocalDate.of(2025, 1, 1);
LocalDate end2Months = LocalDate.of(2025, 12, 31);
for (LocalDate date = start2Months; date.isBefore(end2Months); date = date.plus(2, ChronoUnit.MONTHS)) {
System.out.println(date);
}
}
}
ChronoUnit
enables clean, type-safe date/time arithmetic across a wide range of temporal units.plus()
and minus()
with ChronoUnit
for expressive time manipulation.ChronoUnit.between(start, end)
.plus(unit)
logic.By using ChronoUnit
, you gain fine-grained control and improved code clarity—making it an essential tool for building scheduling and recurrence systems in Java.
In modern applications, scheduling tasks is a common requirement—whether for executing background jobs, sending periodic notifications, or automating maintenance tasks. Java provides both built-in and third-party solutions to schedule tasks, and with the advent of the java.time
API, integrating robust date and time handling into these schedulers has become more reliable and expressive.
This section introduces two widely used scheduling frameworks—ScheduledExecutorService
(built into Java) and Quartz Scheduler (a powerful third-party library)—and shows how to use them with java.time
types while handling time zones and recurring calendar-aware schedules.
ScheduledExecutorService
with java.time
The ScheduledExecutorService
is part of the java.util.concurrent
package and is suitable for lightweight periodic or delayed tasks. While it doesn’t natively support complex recurrence patterns or calendar-aware scheduling, it integrates well with java.time
.
import java.time.LocalTime;
import java.util.concurrent.*;
public class ScheduledTaskExample {
public static void main(String[] args) {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
Runnable task = () -> {
System.out.println("Task executed at: " + LocalTime.now());
};
scheduler.scheduleAtFixedRate(task, 0, 5, TimeUnit.SECONDS);
}
}
This code prints the current time every 5 seconds. You can include java.time
values in the task to calculate next occurrences or durations.
Note:
ScheduledExecutorService
uses fixed-rate or fixed-delay execution without awareness of calendar irregularities like weekends or daylight saving time.
java.time
Quartz Scheduler is a full-featured, enterprise-grade scheduling library that supports cron expressions, calendar-based triggers, time zone management, and misfire handling.
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import java.time.DayOfWeek;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.TimeZone;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;
import static org.quartz.CronScheduleBuilder.cronSchedule;
public class QuartzSchedulerExample {
public static class MyJob implements Job {
public void execute(JobExecutionContext context) {
System.out.println("Executing at: " + LocalTime.now());
}
}
public static void main(String[] args) throws Exception {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
JobDetail job = newJob(MyJob.class)
.withIdentity("weeklyJob")
.build();
Trigger trigger = newTrigger()
.withIdentity("monday10amTrigger")
.withSchedule(cronSchedule("0 0 10 ? * MON") // every Monday at 10:00
.inTimeZone(TimeZone.getTimeZone("Europe/Paris")))
.build();
scheduler.scheduleJob(job, trigger);
scheduler.start();
}
}
Quartz uses cron expressions and supports time zone-aware scheduling, making it ideal for business-critical and international applications.
When scheduling across time zones or observing local calendar rules (like weekends, holidays, daylight saving changes), it’s crucial to use APIs and frameworks that:
ZoneId
or TimeZone
explicitly.For example, scheduling a task at 2 AM every day in a DST-sensitive region may skip or duplicate executions on the transition days. Quartz's calendar support helps avoid those issues.
ScheduledExecutorService
for simple, clock-based periodic tasks.ZoneId.of(...)
) to avoid relying on ZoneId.systemDefault()
, which may differ across environments.java.time
types like LocalDateTime
, ZonedDateTime
, and Duration
in your scheduling logic for better clarity and correctness.By combining java.time
with robust schedulers, you ensure that your time-based logic is accurate, maintainable, and resilient across time zones and calendar complexities.