Scheduled Tasks

Scheduled processing lets a Spring Boot application run a method automatically at fixed times or intervals, without any user request. You enable it with @EnableScheduling and mark a method with @Scheduled, and Spring runs it on a background thread. It is used for daily reports, data cleanup, sending reminders, syncing with other systems, and cache refresh. No extra dependency is needed, because it is part of spring-context.

Features

  • Enabled with one annotation: @EnableScheduling
  • Three trigger styles: cron, fixedRate, and fixedDelay
  • initialDelay to wait before the first run
  • Timings can come from application.properties, so they change without code edits
  • Optional time zone for cron jobs: zone = "Asia/Kolkata"
  • Scheduler thread pool size set with spring.task.scheduling.pool.size
  • The scheduled method can use any service or repository
  • Same logic can be reused for a manual “run now” API

Hands-on Experiment: Creating a spring boot application to understand how Spring Boot can automatically execute tasks at predefined intervals.

The following steps are involved in developing this hands-on experiment.

Step 1: Generate the Project

Step 2: Project Structure

Step 3: pom.xml

Step 4: application.properties

Step 5: Create Entity Class

Step 6: Create Repository Interface

Step 7: Create Service Class

Step 8: Create Controller Class

Step 9: Main Application Class

Step 10: Run the Main Application Class

Step 1: Configure the Project on Spring Initializr

Go to start.spring.io and set:

  • Project: Maven
  • Language: Java
  • Spring Boot version: 3.2.x
  • Group: com.example
  • Artifact: scheduler
  • Name: scheduler
  • Package name: com.example.scheduler
  • Packaging: Jar
  • Java: 17
  • Dependencies: Spring Web, Spring Data JPA, MySQL Driver.

Click Generate to download the ZIP, then extract and import it into your IDE as a Maven project.

Step 2: Generated Project Structure

Step 3: Explore pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>student-scheduler-demo</artifactId>
    <version>1.0.0</version>
    <name>student-scheduler-demo</name>
    <description>Scheduled Processing - daily Student report job</description>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <!-- @Scheduled is part of spring-context, so no extra dependency is needed -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Step 4: application.properties

spring.application.name=student-scheduler-demo

# ---------- MySQL ----------
# Create the database first:  CREATE DATABASE student_scheduler_db;
spring.datasource.url=jdbc:mysql://localhost:3306/student_scheduler_db
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.hibernate.ddl-auto=update

# ---------- Scheduler thread pool ----------
# Default is 1 thread, so jobs wait for each other. 2 threads let jobs run side by side.
spring.task.scheduling.pool.size=2
spring.task.scheduling.thread-name-prefix=student-job-

# ---------- Logging (to watch the jobs in the console) ----------
logging.level.root=INFO
logging.level.com.example.scheduler=DEBUG
logging.pattern.console=%d{HH:mm:ss} | %-5level | %thread | %msg%n

# ---------- Job timings ----------
# CRON: second minute hour day-of-month month day-of-week
# DEMO: every minute (second 0). For a real daily report at 8:00 AM use:  0 0 8 * * *
app.report.cron=0 * * * * *

# FIXED RATE: log student statistics every 30 seconds, first run 10 seconds after startup
app.schedule.stats-rate-ms=30000
app.schedule.initial-delay-ms=10000

# FIXED DELAY: purge old reports 60 seconds AFTER the previous purge finished
app.schedule.cleanup-delay-ms=60000
# Reports older than this many days are deleted by the cleanup job
app.report.keep-days=30Code language: PHP (php)

Step 5: Create Entity Class

package com.example.scheduler.entity;

import jakarta.persistence.*;
import java.time.LocalDate;
import java.time.LocalDateTime;

@Entity
@Table(name = "daily_reports")
public class DailyReport {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // One report per day
    @Column(nullable = false, unique = true)
    private LocalDate reportDate;

    private long totalStudents;

    private long newStudentsToday;

    private Double averageMarks;

    @Column(length = 1000)
    private String courseSummary;

    private LocalDateTime generatedAt;

    // SCHEDULER or MANUAL
    private String generatedBy;

    public DailyReport() {
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public LocalDate getReportDate() { return reportDate; }
    public void setReportDate(LocalDate reportDate) { this.reportDate = reportDate; }

    public long getTotalStudents() { return totalStudents; }
    public void setTotalStudents(long totalStudents) { this.totalStudents = totalStudents; }

    public long getNewStudentsToday() { return newStudentsToday; }
    public void setNewStudentsToday(long newStudentsToday) { this.newStudentsToday = newStudentsToday; }

    public Double getAverageMarks() { return averageMarks; }
    public void setAverageMarks(Double averageMarks) { this.averageMarks = averageMarks; }

    public String getCourseSummary() { return courseSummary; }
    public void setCourseSummary(String courseSummary) { this.courseSummary = courseSummary; }

    public LocalDateTime getGeneratedAt() { return generatedAt; }
    public void setGeneratedAt(LocalDateTime generatedAt) { this.generatedAt = generatedAt; }

    public String getGeneratedBy() { return generatedBy; }
    public void setGeneratedBy(String generatedBy) { this.generatedBy = generatedBy; }
}
package com.example.scheduler.entity;

import jakarta.persistence.*;
import java.time.LocalDateTime;

@Entity
@Table(name = "students")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false, unique = true)
    private String email;

    private String course;

    private Integer marks;

    // Set automatically when the student is saved - used to count "new students today"
    private LocalDateTime createdAt;

    @PrePersist
    void onCreate() {
        this.createdAt = LocalDateTime.now();
    }

    public Student() {
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    public String getCourse() { return course; }
    public void setCourse(String course) { this.course = course; }

    public Integer getMarks() { return marks; }
    public void setMarks(Integer marks) { this.marks = marks; }

    public LocalDateTime getCreatedAt() { return createdAt; }
    public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}

Step 6: Create below Repository Interface

package com.example.scheduler.repository;

import com.example.scheduler.entity.DailyReport;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.time.LocalDate;
import java.util.List;
import java.util.Optional;

@Repository
public interface DailyReportRepository extends JpaRepository<DailyReport, Long> {

    Optional<DailyReport> findByReportDate(LocalDate reportDate);

    Optional<DailyReport> findFirstByOrderByReportDateDesc();

    List<DailyReport> findAllByOrderByReportDateDesc();

    long deleteByReportDateBefore(LocalDate date);
}
package com.example.scheduler.repository;

import com.example.scheduler.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

import java.time.LocalDateTime;
import java.util.List;

@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {

    boolean existsByEmail(String email);

    long countByCreatedAtBetween(LocalDateTime start, LocalDateTime end);

    @Query("select avg(s.marks) from Student s")
    Double findAverageMarks();

    // Each row: [course, number of students]
    @Query("select s.course, count(s) from Student s group by s.course order by s.course")
    List<Object[]> countStudentsByCourse();
}

Step 7: Create below Service class

package com.example.scheduler.service;

import com.example.scheduler.entity.DailyReport;
import com.example.scheduler.repository.DailyReportRepository;
import com.example.scheduler.repository.StudentRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Service
public class ReportService {

    private static final Logger log = LoggerFactory.getLogger(ReportService.class);

    private final StudentRepository studentRepository;
    private final DailyReportRepository reportRepository;

    @Value("${app.report.keep-days}")
    private int keepDays;

    public ReportService(StudentRepository studentRepository, DailyReportRepository reportRepository) {
        this.studentRepository = studentRepository;
        this.reportRepository = reportRepository;
    }

    // ==================================================================
    // JOB 1 - CRON: generate the daily Student report
    // Cron = second minute hour day-of-month month day-of-week
    // The expression is read from application.properties (app.report.cron)
    // Add  zone = "Asia/Kolkata"  to run in a specific time zone.
    // ==================================================================
    @Scheduled(cron = "${app.report.cron}")
    public void scheduledDailyReport() {
        log.info("[CRON] Daily report job started");
        try {
            DailyReport report = generateDailyReport("SCHEDULER");
            log.info("[CRON] Daily report ready for {}: total={}, new today={}, average marks={}",
                    report.getReportDate(), report.getTotalStudents(),
                    report.getNewStudentsToday(), report.getAverageMarks());
        } catch (Exception ex) {
            // An exception must be caught here, otherwise it is only logged by Spring's error handler
            log.error("[CRON] Daily report job failed", ex);
        }
    }

    // ==================================================================
    // JOB 2 - FIXED RATE: runs every N ms, measured from the START of the previous run
    // initialDelay = wait before the very first run
    // ==================================================================
    @Scheduled(fixedRateString = "${app.schedule.stats-rate-ms}",
               initialDelayString = "${app.schedule.initial-delay-ms}")
    public void logStatistics() {
        log.debug("[FIXED RATE] Students in database right now: {}", studentRepository.count());
    }

    // ==================================================================
    // JOB 3 - FIXED DELAY: waits N ms AFTER the previous run FINISHED
    // Use it when runs must never overlap
    // ==================================================================
    @Scheduled(fixedDelayString = "${app.schedule.cleanup-delay-ms}",
               initialDelayString = "${app.schedule.initial-delay-ms}")
    @Transactional
    public void purgeOldReports() {
        LocalDate limit = LocalDate.now().minusDays(keepDays);
        long deleted = reportRepository.deleteByReportDateBefore(limit);
        log.debug("[FIXED DELAY] Cleanup finished - {} report(s) older than {} deleted", deleted, limit);
    }

    // ==================================================================
    // The actual report logic - used by the cron job AND by the manual API
    // ==================================================================
    @Transactional
    public DailyReport generateDailyReport(String generatedBy) {
        LocalDate today = LocalDate.now();
        LocalDateTime startOfDay = today.atStartOfDay();
        LocalDateTime endOfDay = startOfDay.plusDays(1);

        long total = studentRepository.count();
        long newToday = studentRepository.countByCreatedAtBetween(startOfDay, endOfDay);
        Double average = studentRepository.findAverageMarks();

        List<String> parts = new ArrayList<>();
        for (Object[] row : studentRepository.countStudentsByCourse()) {
            parts.add((row[0] == null ? "Not set" : row[0]) + "=" + row[1]);
        }

        // One report per day: update today's report if it already exists
        Optional<DailyReport> existing = reportRepository.findByReportDate(today);
        DailyReport report = existing.orElseGet(DailyReport::new);
        report.setReportDate(today);
        report.setTotalStudents(total);
        report.setNewStudentsToday(newToday);
        report.setAverageMarks(average == null ? null : Math.round(average * 100.0) / 100.0);
        report.setCourseSummary(String.join(", ", parts));
        report.setGeneratedAt(LocalDateTime.now());
        report.setGeneratedBy(generatedBy);

        log.debug("{} report for {}", existing.isPresent() ? "Updating" : "Creating", today);
        return reportRepository.save(report);
    }

    public List<DailyReport> getAllReports() {
        return reportRepository.findAllByOrderByReportDateDesc();
    }

    public Optional<DailyReport> getLatestReport() {
        return reportRepository.findFirstByOrderByReportDateDesc();
    }
}
package com.example.scheduler.service;

import com.example.scheduler.entity.Student;
import com.example.scheduler.repository.StudentRepository;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class StudentService {

    private final StudentRepository studentRepository;

    public StudentService(StudentRepository studentRepository) {
        this.studentRepository = studentRepository;
    }

    public Student add(Student student) {
        if (studentRepository.existsByEmail(student.getEmail())) {
            throw new IllegalArgumentException("Email already registered: " + student.getEmail());
        }
        return studentRepository.save(student);
    }

    // Adds many students at once; duplicate emails are skipped
    public List<Student> addAll(List<Student> students) {
        List<Student> saved = new ArrayList<>();
        for (Student s : students) {
            if (!studentRepository.existsByEmail(s.getEmail())) {
                saved.add(studentRepository.save(s));
            }
        }
        return saved;
    }

    public List<Student> getAll() {
        return studentRepository.findAll();
    }
}

Step 8: Create Controller class

package com.example.scheduler.controller;

import com.example.scheduler.entity.DailyReport;
import com.example.scheduler.service.ReportService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/reports")
public class ReportController {

    private final ReportService reportService;

    public ReportController(ReportService reportService) {
        this.reportService = reportService;
    }

    // All reports saved by the scheduled job (newest first)
    @GetMapping
    public List<DailyReport> getAll() {
        return reportService.getAllReports();
    }

    @GetMapping("/latest")
    public ResponseEntity<?> latest() {
        return reportService.getLatestReport()
                .<ResponseEntity<?>>map(ResponseEntity::ok)
                .orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND)
                        .body(Map.of("error", "No report generated yet - wait for the scheduler or call POST /reports/run")));
    }

    // Runs the same logic as the scheduled job right now (for testing)
    @PostMapping("/run")
    public DailyReport runNow() {
        return reportService.generateDailyReport("MANUAL");
    }
}
package com.example.scheduler.controller;

import com.example.scheduler.entity.Student;
import com.example.scheduler.service.StudentService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/students")
public class StudentController {

    private final StudentService studentService;

    public StudentController(StudentService studentService) {
        this.studentService = studentService;
    }

    @PostMapping
    public ResponseEntity<?> add(@RequestBody Student student) {
        try {
            return ResponseEntity.status(HttpStatus.CREATED).body(studentService.add(student));
        } catch (IllegalArgumentException ex) {
            return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));
        }
    }

    @PostMapping("/bulk")
    public ResponseEntity<List<Student>> addAll(@RequestBody List<Student> students) {
        return ResponseEntity.status(HttpStatus.CREATED).body(studentService.addAll(students));
    }

    @GetMapping
    public List<Student> getAll() {
        return studentService.getAll();
    }
}

Step 9: Main Application

package com.example.scheduler;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling      // switches on @Scheduled processing for the whole application
public class SchedulerApplication {

    public static void main(String[] args) {
        SpringApplication.run(SchedulerApplication.class, args);
    }
}

Step 10: Run the SchedulerApplication

Scroll to Top