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, andfixedDelay initialDelayto 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


