A cron expression is a short text pattern that tells Spring exactly when a task should run, such as “every day at 8:30 AM” or “every Monday at 9:00 AM”. Unlike fixed rate and fixed delay, which repeat after an interval, cron ties execution to clock and calendar times. You use it with @Scheduled(cron = "..."), optionally with a zone so that “8:30” means the same time on every server. It is used for daily reports, weekly summaries, month-end processing, and jobs that must run at specific business times.
Features
- Precise control over second, minute, hour, day, month, and weekday
- Lists (
1,15), ranges (9-17), steps (*/15), and names (MON-FRI) Lfor the last day of the month- Time zone support:
zone = "Asia/Kolkata" - Cron text read from
application.properties, so times change without editing code - A single dash (
-) switches a cron job off without deleting it - Macros such as
@daily,@hourly, and@weekly CronExpressionclass lets you check the next run times before using an expression
Hands-on Experiment: Creating a spring boot application to understand how cron expressions provide precise control over when scheduled tasks execute.
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: cron
- Name: cron
- Package name: com.example.cron
- 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-cron-demo</artifactId>
<version>1.0.0</version>
<name>student-cron-demo</name>
<description>Cron-based Scheduling - Student reports at specific times</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- @Scheduled and CronExpression are part of spring-context -->
<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-cron-demo
# ---------- MySQL ----------
# Create the database first: CREATE DATABASE student_cron_db;
spring.datasource.url=jdbc:mysql://localhost:3306/student_cron_db
spring.datasource.username=root
spring.datasource.password=password
spring.jpa.hibernate.ddl-auto=update
server.port=8082
# ---------- Scheduler ----------
spring.task.scheduling.pool.size=2
spring.task.scheduling.thread-name-prefix=cron-job-
# ---------- Logging ----------
logging.level.root=INFO
logging.level.com.example.cron=DEBUG
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} | %-5level | %thread | %msg%n
# ---------- Cron expressions ----------
# Format (6 fields): second minute hour day-of-month month day-of-week
#
# Time zone used by ALL cron jobs below (and to decide what "today" means in the reports)
app.cron.zone=Asia/Kolkata
# JOB 1 - THE EXPERIMENT: Student report every day at 08:30:00 AM
app.cron.daily=0 30 8 * * *
# JOB 2 - weekly summary: every Monday at 09:00:00 AM
app.cron.weekly=0 0 9 * * MON
# JOB 3 - student count snapshot: every hour on the hour, 9 AM to 5 PM, Monday to Friday
app.cron.office-hours=0 0 9-17 * * MON-FRI
# JOB 4 - monthly report: last day of every month at 6:00 PM (L = last day)
app.cron.monthly=0 0 18 L * *
# TIP 1: to test quickly, set the daily job to run every 20 seconds: app.cron.daily=*/20 * * * * *
# TIP 2: to switch a job OFF without deleting it, set its value to a single dash: app.cron.weekly=-Code language: PHP (php)
Step 5: Create Entity Class
package com.example.cron.entity;
import jakarta.persistence.*;
import java.time.LocalDateTime;
// One row for every report run (by cron or manually)
@Entity
@Table(name = "report_logs")
public class ReportLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// DAILY, WEEKLY, OFFICE_HOURS, MONTHLY
private String reportType;
// CRON or MANUAL
private String triggeredBy;
// The cron expression of the job that produced this row
private String cronExpression;
private String zone;
// Local time in the configured zone
private LocalDateTime generatedAt;
private long totalStudents;
private long newStudentsInPeriod;
private Double averageMarks;
@Column(length = 1000)
private String courseSummary;
public ReportLog() {
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getReportType() { return reportType; }
public void setReportType(String reportType) { this.reportType = reportType; }
public String getTriggeredBy() { return triggeredBy; }
public void setTriggeredBy(String triggeredBy) { this.triggeredBy = triggeredBy; }
public String getCronExpression() { return cronExpression; }
public void setCronExpression(String cronExpression) { this.cronExpression = cronExpression; }
public String getZone() { return zone; }
public void setZone(String zone) { this.zone = zone; }
public LocalDateTime getGeneratedAt() { return generatedAt; }
public void setGeneratedAt(LocalDateTime generatedAt) { this.generatedAt = generatedAt; }
public long getTotalStudents() { return totalStudents; }
public void setTotalStudents(long totalStudents) { this.totalStudents = totalStudents; }
public long getNewStudentsInPeriod() { return newStudentsInPeriod; }
public void setNewStudentsInPeriod(long newStudentsInPeriod) { this.newStudentsInPeriod = newStudentsInPeriod; }
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; }
}
package com.example.cron.entity;
import jakarta.persistence.*;
import java.time.Instant;
@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;
// Exact moment of registration (time-zone independent); used to count "new students" in a period
private Instant createdAt;
@PrePersist
void onCreate() {
this.createdAt = Instant.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 Instant getCreatedAt() { return createdAt; }
public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}Step 6: Create below Repository Interface
package com.example.cron.repository;
import com.example.cron.entity.ReportLog;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ReportLogRepository extends JpaRepository<ReportLog, Long> {
List<ReportLog> findTop20ByOrderByIdDesc();
List<ReportLog> findTop20ByReportTypeOrderByIdDesc(String reportType);
}
package com.example.cron.repository;
import com.example.cron.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.time.Instant;
import java.util.List;
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
boolean existsByEmail(String email);
long countByCreatedAtBetween(Instant start, Instant 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.cron.service;
import com.example.cron.entity.ReportLog;
import com.example.cron.repository.ReportLogRepository;
import com.example.cron.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 java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
@Service
public class ReportService {
private static final Logger log = LoggerFactory.getLogger(ReportService.class);
private final StudentRepository studentRepository;
private final ReportLogRepository reportLogRepository;
// The same properties that drive the @Scheduled annotations (kept here to store them in the report rows)
@Value("${app.cron.zone}")
private String zone;
@Value("${app.cron.daily}")
private String dailyCron;
@Value("${app.cron.weekly}")
private String weeklyCron;
@Value("${app.cron.office-hours}")
private String officeHoursCron;
@Value("${app.cron.monthly}")
private String monthlyCron;
public ReportService(StudentRepository studentRepository, ReportLogRepository reportLogRepository) {
this.studentRepository = studentRepository;
this.reportLogRepository = reportLogRepository;
}
// ==================================================================
// JOB 1 - THE EXPERIMENT: daily Student report at a specific time
// 0 30 8 * * * -> second 0, minute 30, hour 8, every day, every month, every weekday
// zone = the time zone in which "8:30" is measured
// ==================================================================
@Scheduled(cron = "${app.cron.daily}", zone = "${app.cron.zone}")
public void dailyReport() {
runJob("DAILY", dailyCron);
}
// JOB 2 - every Monday at 09:00
@Scheduled(cron = "${app.cron.weekly}", zone = "${app.cron.zone}")
public void weeklySummary() {
runJob("WEEKLY", weeklyCron);
}
// JOB 3 - on the hour, 9 to 17, Monday to Friday
@Scheduled(cron = "${app.cron.office-hours}", zone = "${app.cron.zone}")
public void officeHoursSnapshot() {
runJob("OFFICE_HOURS", officeHoursCron);
}
// JOB 4 - last day of the month at 18:00 (L = last day of the month)
@Scheduled(cron = "${app.cron.monthly}", zone = "${app.cron.zone}")
public void monthlyReport() {
runJob("MONTHLY", monthlyCron);
}
// Common wrapper: log, run, and never let an exception escape
private void runJob(String type, String cron) {
log.info("[CRON] {} job fired (cron='{}', zone={})", type, cron, zone);
try {
ReportLog report = generate(type, cron, "CRON");
log.info("[CRON] {} report saved: id={}, total={}, new in period={}, average marks={}",
type, report.getId(), report.getTotalStudents(),
report.getNewStudentsInPeriod(), report.getAverageMarks());
} catch (Exception ex) {
log.error("[CRON] {} job failed", type, ex);
}
}
// ==================================================================
// Report logic - used by the cron jobs AND by the manual API
// ==================================================================
public ReportLog generate(String type, String cron, String triggeredBy) {
ZoneId zoneId = ZoneId.of(zone);
ZonedDateTime now = ZonedDateTime.now(zoneId);
// Start of the period this report covers, measured in the configured time zone
Instant periodStart;
switch (type) {
case "WEEKLY" -> periodStart = now.minusDays(7).toInstant();
case "OFFICE_HOURS" -> periodStart = now.minusHours(1).toInstant();
case "MONTHLY" -> periodStart = LocalDate.now(zoneId).withDayOfMonth(1).atStartOfDay(zoneId).toInstant();
default -> periodStart = LocalDate.now(zoneId).atStartOfDay(zoneId).toInstant(); // DAILY
}
long total = studentRepository.count();
long newInPeriod = studentRepository.countByCreatedAtBetween(periodStart, now.toInstant());
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]);
}
ReportLog report = new ReportLog();
report.setReportType(type);
report.setTriggeredBy(triggeredBy);
report.setCronExpression(cron);
report.setZone(zone);
report.setGeneratedAt(LocalDateTime.now(zoneId));
report.setTotalStudents(total);
report.setNewStudentsInPeriod(newInPeriod);
report.setAverageMarks(average == null ? null : Math.round(average * 100.0) / 100.0);
report.setCourseSummary(String.join(", ", parts));
return reportLogRepository.save(report);
}
// Manual run: same logic, uses the cron text of the matching job
public ReportLog runManually(String type) {
String upper = type.toUpperCase();
String cron = switch (upper) {
case "DAILY" -> dailyCron;
case "WEEKLY" -> weeklyCron;
case "OFFICE_HOURS" -> officeHoursCron;
case "MONTHLY" -> monthlyCron;
default -> throw new IllegalArgumentException("Unknown type: " + type
+ ". Use DAILY, WEEKLY, OFFICE_HOURS or MONTHLY");
};
return generate(upper, cron, "MANUAL");
}
public List<ReportLog> getReports(String type) {
if (type == null || type.isBlank()) {
return reportLogRepository.findTop20ByOrderByIdDesc();
}
return reportLogRepository.findTop20ByReportTypeOrderByIdDesc(type.toUpperCase());
}
}
package com.example.cron.service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.support.CronExpression;
import org.springframework.stereotype.Service;
import java.time.DateTimeException;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
// Helps you SEE what a cron expression means: lists the configured jobs and previews run times
@Service
public class ScheduleService {
private static final DateTimeFormatter FORMAT = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss z");
@Value("${app.cron.zone}")
private String zone;
@Value("${app.cron.daily}")
private String dailyCron;
@Value("${app.cron.weekly}")
private String weeklyCron;
@Value("${app.cron.office-hours}")
private String officeHoursCron;
@Value("${app.cron.monthly}")
private String monthlyCron;
// The next run times of a cron expression
public List<String> preview(String cron, String zoneName, int count) {
if (cron == null || !CronExpression.isValidExpression(cron)) {
throw new IllegalArgumentException("Invalid cron expression: '" + cron
+ "'. Use 6 fields: second minute hour day-of-month month day-of-week");
}
ZoneId zoneId;
try {
zoneId = ZoneId.of(zoneName == null || zoneName.isBlank() ? zone : zoneName);
} catch (DateTimeException ex) {
throw new IllegalArgumentException("Invalid time zone: " + zoneName);
}
CronExpression expression = CronExpression.parse(cron);
List<String> times = new ArrayList<>();
ZonedDateTime next = ZonedDateTime.now(zoneId);
for (int i = 0; i < Math.min(Math.max(count, 1), 20); i++) {
next = expression.next(next);
if (next == null) {
break;
}
times.add(next.format(FORMAT));
}
return times;
}
// Every configured job with its cron expression and its next 3 run times
public List<Map<String, Object>> listJobs() {
List<Map<String, Object>> jobs = new ArrayList<>();
jobs.add(job("DAILY", "Student report every day", dailyCron));
jobs.add(job("WEEKLY", "Weekly summary", weeklyCron));
jobs.add(job("OFFICE_HOURS", "Hourly snapshot in office hours", officeHoursCron));
jobs.add(job("MONTHLY", "Month-end report", monthlyCron));
return jobs;
}
private Map<String, Object> job(String name, String description, String cron) {
Map<String, Object> info = new LinkedHashMap<>();
info.put("job", name);
info.put("description", description);
info.put("cron", cron);
info.put("zone", zone);
if ("-".equals(cron)) {
info.put("status", "DISABLED");
} else {
info.put("status", "ENABLED");
info.put("nextRuns", preview(cron, zone, 3));
}
return info;
}
}
package com.example.cron.service;
import com.example.cron.entity.Student;
import com.example.cron.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.cron.controller;
import com.example.cron.entity.ReportLog;
import com.example.cron.service.ReportService;
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;
}
// Latest 20 reports. Optional filter: ?type=DAILY
@GetMapping
public List<ReportLog> getReports(@RequestParam(required = false) String type) {
return reportService.getReports(type);
}
// Runs a report right now (for testing): DAILY, WEEKLY, OFFICE_HOURS or MONTHLY
@PostMapping("/run/{type}")
public ResponseEntity<?> runNow(@PathVariable String type) {
try {
return ResponseEntity.ok(reportService.runManually(type));
} catch (IllegalArgumentException ex) {
return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));
}
}
}
package com.example.cron.controller;
import com.example.cron.service.ScheduleService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/schedule")
public class ScheduleController {
private final ScheduleService scheduleService;
public ScheduleController(ScheduleService scheduleService) {
this.scheduleService = scheduleService;
}
// All jobs with their cron expression and next 3 run times
@GetMapping("/jobs")
public List<Map<String, Object>> jobs() {
return scheduleService.listJobs();
}
// Try any cron expression before putting it in application.properties
// Example: /schedule/preview?cron=0 30 8 * * *&count=5&zone=Asia/Kolkata
@GetMapping("/preview")
public ResponseEntity<?> preview(@RequestParam String cron,
@RequestParam(defaultValue = "5") int count,
@RequestParam(required = false) String zone) {
try {
return ResponseEntity.ok(Map.of("cron", cron, "nextRuns", scheduleService.preview(cron, zone, count)));
} catch (IllegalArgumentException ex) {
return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));
}
}
}
package com.example.cron.controller;
import com.example.cron.entity.Student;
import com.example.cron.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.cron;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class CronApplication {
public static void main(String[] args) {
SpringApplication.run(CronApplication.class, args);
}
}
Step 10: Run the CronApplication


