Index

Scheduling and Recurrence

Java Date and Time

13.1 Modeling Recurring Events (e.g., "Every Monday")

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.

Scheduling Weekly Recurrence: "Every Monday"

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.

Monthly Recurrence: "First Friday of Each Month"

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.

Custom Intervals: "Every 15 Days"

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.

Edge Cases: Holidays and Irregular Months

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.

Summary

Modeling recurring events in Java is straightforward with the java.time API:

The clarity and composability of these tools make them ideal for implementing business-grade scheduling features.

Index

13.2 Using ChronoUnit for Custom Intervals

The 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.

Adding and Subtracting Time with 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.

Example 1: Adding Custom Units

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);
    }
}

Example 2: Subtracting Time

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().

Calculating Time Between Dates

You can use ChronoUnit.between(start, end) to calculate the difference between two dates or times in a specific unit.

Example 3: Time Difference in Days and Months

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.

Iterating with Custom Steps

ChronoUnit also shines when building loops with custom intervals—like generating a sequence of dates every 3 days or every 2 months.

Example 4: Every 3 Days

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);
}

Example 5: Every 2 Months

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.

Click to view full runnable Code

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);
        }
    }
}

Summary

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.

Index

13.3 Integration with Scheduling Frameworks

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.

Using 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.

Example: Running a Task Every 5 Seconds

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.

Using Quartz with 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.

Example: Scheduling a Job Every Monday at 10 AM

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.

Time Zone Awareness and Recurring Tasks

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:

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.

Summary and Best Practices

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.

Index