Scheduled Tasks

Running Jobs on a Timer

@SpringBootApplication
@EnableScheduling
public class ShopEaseApplication { ... }

@Component
@Slf4j
@RequiredArgsConstructor
public class ShopEaseJobs {
    private final CartRepository carts;
    private final ReportService reports;

    @Scheduled(fixedRate = 60_000)                 // every 60 s, measured from the start of each run
    public void releaseExpiredReservations() { ... }

    @Scheduled(fixedDelay = 30_000, initialDelay = 10_000)   // 30 s after the previous run ENDS
    public void syncStockFromWarehouse() { ... }

    @Scheduled(cron = "0 0 2 * * *", zone = "Asia/Kolkata")  // every day at 02:00 IST
    public void sendDailySalesReport() {
        reports.emailYesterdaySales();
    }

    @Scheduled(cron = "${shopease.jobs.abandoned-cart-cron:0 0 */3 * * *}")
    public void remindAbandonedCarts() {
        log.info("Reminded {} users", carts.remindAbandoned(Duration.ofHours(3)));
    }
}

Cron Format

ExpressionMeaning
0 0 2 * * *every day at 02:00:00
0 */15 * * * *every 15 minutes
0 0 9 * * MON-FRI09:00 on weekdays
0 0 0 1 * *midnight on the 1st of every month

Spring cron has six fields: second, minute, hour, day of month, month, day of week (Linux crontab has five, without seconds).

Scheduling in Plain Java

Example

@Scheduled(fixedDelay=...) sits on top of a ScheduledExecutorService. This job runs every 100 ms and the program stops it after three runs:

import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

public class Main {
    public static void main(String[] args) throws Exception {
        ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
        AtomicInteger runs = new AtomicInteger();
        CountDownLatch done = new CountDownLatch(3);

        scheduler.scheduleWithFixedDelay(() -> {
            int n = runs.incrementAndGet();
            if (n <= 3) {
                System.out.println("run " + n + ": released " + (n * 2) + " expired cart reservations");
                done.countDown();
            }
        }, 0, 100, TimeUnit.MILLISECONDS);

        done.await();
        scheduler.shutdownNow();
        System.out.println("scheduler stopped");
    }
}

Output

run 1: released 2 expired cart reservations
run 2: released 4 expired cart reservations
run 3: released 6 expired cart reservations
scheduler stopped

Several Instances and Pool Size

Note: If ShopEase runs on 3 servers, every @Scheduled job runs 3 times. Use ShedLock (a lock row in the database) or a single worker instance so the daily report is sent once.

@Scheduled(cron = "0 0 2 * * *", zone = "Asia/Kolkata")
@SchedulerLock(name = "dailySalesReport", lockAtMostFor = "PT30M")
public void sendDailySalesReport() { ... }

Example

Scheduled tasks share a single thread by default. Give them a pool:

application.yml

spring:
  task:
    scheduling:
      pool:
        size: 4
      thread-name-prefix: shopease-sched-

Practice

Exercise: Write a job that runs every Sunday at 23:30 IST and deletes cart items older than 30 days.

Show answer
@Scheduled(cron = "0 30 23 * * SUN", zone = "Asia/Kolkata")
@Transactional
public void purgeOldCartItems() {
    int deleted = cartItemRepository.deleteByAddedAtBefore(Instant.now().minus(Duration.ofDays(30)));
    log.info("Purged {} old cart items", deleted);
}