Quartz with MySQL and Spring Boot Example

In this tutorial, you will learn how to create a scheduler application using Spring Quartz and MySQL in Java Spring Boot.

Quartz is an open-source job scheduling library that can be integrated into Java applications of any size.

Quartz Scheduler can be used to execute different tasks at a pre-determined time or when the scheduled time arrives. For example: a task set to run every 2 hours, a task to run on weekdays except holidays at 12:30 PM, and a task to run every 2 days at 10:30 PM.

Spring Boot offers 'spring-boot-starter-quartz' Starter which makes working with the Quartz easier and faster. When Quartz is added to a Spring Boot application, the Scheduler is auto-configured via the SchedulerFactoryBean abstraction and also the beans of the following types are automatically picked up:

  • Calender: is used to define time and is associated with Trigger.
  • Trigger: is used to trigger a particular Job.
  • JobDetail: is used to define a Job detail.
  • JobBuilder: is used to build an instance of JobDetail.

Quartz uses JobStore which are responsible to keep track of all the work data that is given to the scheduler. By default Quartz uses in-memory JobStore but in this tutorial, we will use JDBCJobStore to keep all of its data in a MySQL database via JDBC.

Follow these steps to create a scheduler application in Spring Boot using Quartz with MySQL, allowing you to schedule and unschedule jobs via REST APIs:

  1. If you already have a Spring Boot project, you can skip to step 7. Otherwise, to create a project from scratch, you can go to the Spring Initializr website at https://start.spring.io.
  2. Create a Spring Boot application with details as follows:
    • Project: Choose the project type (Maven or Gradle).
    • Language: Set the language to Java.
    • Spring Boot: Specify the Spring Boot version. The default selection is the latest stable version of Spring Boot, so you can leave it unchanged.
    • Project Metadata: Enter a Group and Artifact name for your project. The group name is the id of the project. Artifact is the name of your project. Add any necessary project metadata (description, package name, etc.)
    • Choose between packaging as a JAR (Java Archive) or a WAR (Web Application Archive) depends on how you plan to deploy your Spring Boot application. Choose JAR packaging if you want a standalone executable JAR file and WAR packaging if you intend to deploy your application to a Java EE application server or servlet container. When you package your Spring Boot application as a JAR using JAR packaging, it includes an embedded web server, such as Tomcat, by default. This means that you don't need to separately deploy your application to an external Tomcat server. Instead, you can run the JAR file directly, and the embedded Tomcat server will start and serve your application.
    • Select the Java version based on the compatibility requirements of your project. Consider the specific needs of your project, any compatibility requirements, and the Java version supported by your target deployment environment when making these choices.
  3. Add project dependencies:
    • Click on the "Add Dependencies" button.
    • Choose the following dependencies: Spring Web, Quartz Scheduler, MySQL Driver, Spring Data JPA, Lombok, and Spring Boot DevTools.

    Here's an example:



  4. Generate the project:
    • Click on the "Generate" button.
    • Spring Initializr will generate a zip file containing your Spring Boot project.
  5. Download and extract the generated project:
    • Download the zip file generated by Spring Initializr.
    • Extract the contents of the zip file to a directory on your local machine.
  6. Import the project into your IDE:
    • Open your preferred IDE (IntelliJ IDEA, Eclipse, or Spring Tool Suite).
    • Import the extracted project as a Maven or Gradle project, depending on the build system you chose in Spring Initializr.
  7. Dependencies Required:
  8. When we use Quartz with relational databases like MySQL, we also need to add c3p0 library. c3p0 library is a mature, highly concurrent JDBC Connection pooling library. Find the latest version of c3p0 in the c3p0 Maven Repository.

    Here is a list of dependencies that the project requires: Quartz Scheduler, MySQL Driver, Spring Data JPA, and c3p0.

    For Maven:

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-quartz</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>
    
    <dependency>
        <groupId>com.mchange</groupId>
        <artifactId>c3p0</artifactId>
        <version>0.9.5.5</version>
    </dependency>

    For Gradle:

    implementation 'org.springframework.boot:spring-boot-starter-quartz'
    runtimeOnly 'com.mysql:mysql-connector-j'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation group: 'com.mchange', name: 'c3p0', version: '0.9.5.5'

  9. Add Configurations:
  10. Open the src/main/resources/application.properties file in your Eclipse editor and add the following configuration lines to the file:

    #port on which the application will run
    server.port= 8080
    
    #mysql database connection
    spring.datasource.url = jdbc:mysql://localhost:3306/test_buddy
    spring.datasource.username = root
    spring.datasource.password = Testing123$
    spring.datasource.timeBetweenEvictionRunsMillis = 60000
    spring.datasource.maxIdle = 1
    
    #below properties will automatically creates and updates database schema
    spring.jpa.generate-ddl=true
    spring.jpa.hibernate.ddl-auto=update
    
    #Quartz Log level
    logging.level.org.springframework.scheduling.quartz=DEBUG
    logging.level.org.quartz=DEBUG
  11. Configure Quartz:
  12. Create a "quartz.properties" file in the src/main/resources folder with the following configuration:

    #Quartz
    org.quartz.scheduler.instanceName = SampleJobScheduler
    org.quartz.scheduler.instanceId = AUTO
    org.quartz.scheduler.idleWaitTime = 10000
    org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
    org.quartz.threadPool.threadCount = 4
    org.quartz.threadPool.threadPriority = 5
    org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
    org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
    org.quartz.jobStore.tablePrefix = QRTZ_
    org.quartz.jobStore.misfireThreshold = 60000
    org.quartz.jobStore.isClustered = false
    org.quartz.jobStore.maxMisfiresToHandleAtATime = 10
    org.quartz.jobStore.useProperties = true
    
    #quartz mysql database connection
    org.quartz.jobStore.dataSource = mySql
    org.quartz.dataSource.mySql.driver = com.mysql.cj.jdbc.Driver
    org.quartz.dataSource.mySql.URL = jdbc:mysql://localhost:3306/database_name
    org.quartz.dataSource.mySql.user = root
    org.quartz.dataSource.mySql.password = MyPassword123$
    org.quartz.dataSource.mySql.maxConnections = 10
    org.quartz.dataSource.mySql.idleConnectionValidationSeconds = 50
    org.quartz.dataSource.mySql.validationQuery=select 0 from dual
    org.quartz.dataSource.mySql.maxIdleTime = 60

    Here's an explanation of the above configurations:

    • org.quartz.scheduler.instanceName: The value of this can be any string that will help you to distinguish schedulers when multiple instances are used within the same program. Incase of using the clustering features, same name must be used for every instance in the cluster which is logically the same Scheduler. In this example, we are naming our scheduler as SampleJobScheduler.
    • org.quartz.scheduler.instanceId: This value of this can be any string but must be unique for all schedulers within a cluster. You can use the value AUTO if you wish the Id to be auto-generated for you. Or you can also use the value SYS_PROP if you wish the Id to come from the system property.
    • org.quartz.scheduler.idleWaitTime: This value of this property are in milliseconds. It is the amount of time the scheduler must wait for re-queries for available triggers when the scheduler is otherwise idle. Avoid using values less than 5000 ms as it will cause excessive database querying and therefore are not recommended.
    • org.quartz.threadPool.class: Name of the ThreadPool implementation you wish to use. Quartz comes with "org.quartz.simpl.SimpleThreadPool" which provides a fixed-size pool of threads that live the lifetime of the scheduler.
    • org.quartz.threadPool.threadCount: There are 4 threads in the thread pool that can run 4 jobs simultaneously.
    • org.quartz.threadPool.threadPriority: This can be any number between Thread.MIN_PRIORITY(1) to Thread.MAX_PRIORITY(10). The default is Thread.NORM_PRIORITY (5).
    • org.quartz.jobStore.class: You need to tell Quartz which JobStore to use for storing scheduling information such as details of jobs, triggers, calendars. The "org.quartz.impl.jdbcjobstore.JobStoreTX" will tell Quartz to use database for helding Quartz's data.
    • org.quartz.jobStore.driverDelegateClass: Driver delegates understand the particular dialect of different database systemms. The value org.quartz.impl.jdbcjobstore.StdJDBCDelegate is known to work with many databases.
    • org.quartz.jobStore.tablePrefix: This is JDBCJobStore's table prefix string property that must be equal to the prefix given to Quartz's tables created in your database.
    • org.quartz.jobStore.misfireThreshold: The value of this property are in milliseconds. It is the amount of time the scheduler will tolerate a trigger to pass its next-fire-time by, before being considered misfired. The default value is 60000 ms which is equal to 60 seconds.
    • org.quartz.jobStore.isClustered: Set the value of this property to true if you wish to use clustering features otherwise false.
    • org.quartz.jobStore.maxMisfiresToHandleAtATime: The maximum number of misfired triggers to handle in a given time. Handling too many misfired at one time can cause the database tables to be locked long enough that the performance of other triggers may slow down.
    • org.quartz.jobStore.useProperties: Set the value of this property to true to instruct JDBCJobStore that all values in JobDataMaps will be Strings and can be stored as name-value pairs, rather than storing more complex objects in their serialized form.

  13. Enable spring auto wiring for Quartz Job and Service:
  14. Create a custom job factory class named "AutowiringSpringBeanJobFactory" that extends SpringBeanJobFactory and implements ApplicationContextAware:

    package com.example.config;
    
    import org.quartz.spi.TriggerFiredBundle;
    import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.ApplicationContextAware;
    import org.springframework.scheduling.quartz.SpringBeanJobFactory;
    
    public class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory
        implements ApplicationContextAware {
    
      AutowireCapableBeanFactory beanFactory;
    
      @Override
      public void setApplicationContext(final ApplicationContext context) {
        beanFactory = context.getAutowireCapableBeanFactory();
      }
    
      @Override
      protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
        final Object job = super.createJobInstance(bundle);
        beanFactory.autowireBean(job);
        return job;
      }
    
    }

    Here, the class utilizes Spring's autowiring capabilities to automatically inject dependencies into Quartz job instances, enabling seamless integration between Quartz scheduling and Spring dependency injection. This approach is useful when you want to leverage the benefits of Spring-managed beans and their dependencies within Quartz scheduled tasks.


  15. Configure Quartz Scheduler:
  16. Spring offers the SchedulerFactoryBean class, which serves as a FactoryBean responsible for creating and configuring a Quartz Scheduler. It also manages the scheduler's lifecycle within the Spring application context and exposes the scheduler as a bean reference for seamless dependency injection.

    Create a class named QuartzConfig and annotate it with the @Configuration annotation. The @Configuration annotation signifies that this class is a configuration class and will be utilized by the Spring application context to create beans for your application. The class includes the jobFactory method, which defines a Spring bean of type JobFactory. This method returns an instance of AutowiringSpringBeanJobFactory, a custom job factory that facilitates dependency injection into Quartz job instances. The method also sets the ApplicationContext of the job factory, enabling it to resolve and inject Spring beans into the Quartz jobs.

    Additionally, the class contains the schedulerFactoryBean method, which defines a Spring bean of type SchedulerFactoryBean. This method establishes the primary configuration for the Quartz Scheduler. Within the method, an instance of SchedulerFactoryBean is created, and various properties are configured:

    package com.example.config;
    
    import java.io.IOException;
    import java.util.Properties;
    import org.quartz.spi.JobFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.config.PropertiesFactoryBean;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.io.ClassPathResource;
    import org.springframework.scheduling.quartz.SchedulerFactoryBean;
    
    @Configuration
    public class QuartzConfig {
    
      @Autowired
      ApplicationContext applicationContext;
    
      @Bean
      public JobFactory jobFactory() {
        AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
        jobFactory.setApplicationContext(applicationContext);
        return jobFactory;
      }
    
      @Bean
      public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
    
        SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean();
        schedulerFactory.setQuartzProperties(quartzProperties());
        schedulerFactory.setWaitForJobsToCompleteOnShutdown(true);
        schedulerFactory.setAutoStartup(true);
        schedulerFactory.setJobFactory(jobFactory());
        return schedulerFactory;
      }
    
      public Properties quartzProperties() throws IOException {
        PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
        propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));
        propertiesFactoryBean.afterPropertiesSet();
        return propertiesFactoryBean.getObject();
      }
    
    }

  17. Create an Entity:
  18. Create a class named Payment that represents the payment entity:

    package com.example.entity;
    
    import java.util.Date;
    import org.springframework.data.annotation.CreatedDate;
    import org.springframework.data.annotation.LastModifiedDate;
    import org.springframework.data.jpa.domain.support.AuditingEntityListener;
    import jakarta.persistence.Column;
    import jakarta.persistence.Entity;
    import jakarta.persistence.EntityListeners;
    import jakarta.persistence.GeneratedValue;
    import jakarta.persistence.GenerationType;
    import jakarta.persistence.Id;
    import jakarta.persistence.Table;
    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @Builder
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @EntityListeners(AuditingEntityListener.class)
    @Entity
    @Table(name = "payment")
    public class Payment {
    
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;
    
      @Column(name = "sender_id")
      private String senderId;
    
      @Column(name = "receiver_id")
      private String receiverId;
    
      @Column(name = "amount")
      private double amount;
    
      @Column(name = "fee")
      private double fee;
    
      @Column(name = "payment_scheduled_at")
      private Date paymentScheduleAt;
    
      @Column(name = "status")
      private int status;
    
      @Column(name = "active")
      private Boolean active;
    
      @Column(name = "created_by")
      private String createdBy;
    
      @Column(name = "modified_by")
      private String modifiedBy;
    
      @CreatedDate
      @Column(name = "created")
      private Date created;
    
      @LastModifiedDate
      @Column(name = "modified")
      private Date modified;
    }

    Here, @Builder, @Data, @NoArgsConstructor, @AllArgsConstructor annotations are part of Lombok library that simplifies Java code by generating boilerplate code automatically. The @Builder annotation is used to generate a builder pattern for the class, which allows you to create instances of the class using a more readable and concise syntax. The @Data is used to generate getter and setter methods, toString(), equals(), and hashCode() methods for the class. This annotation reduces the amount of repetitive and boilerplate code needed for basic data classes. The @NoArgsConstructor annotation generates a no-argument constructor for the class. This is useful when you want to create instances of the class without having to pass any initial values. The @AllArgsConstructor annotation generates a constructor that includes all of the class's fields as parameters.

    The @EntityListeners(AuditingEntityListener.class) annotation indicates that the class uses an entity listener for auditing purposes. An entity listener is used to intercept lifecycle events of JPA (Java Persistence API) entities, such as creation and update events. This can be useful for maintaining timestamps or other auditing information.

    The @Entity annotation designates the class as a JPA entity. JPA entities are used to map Java classes to database tables, allowing you to perform database operations using Java objects.

    The @Table(name = "table_name") annotation specifies the name of the database table to which the entity is mapped.

    The @Id annotation indicates that the field following it is the primary key of the entity.

    The @GeneratedValue(strategy = GenerationType.IDENTITY) annotation, along with the @Id annotation, indicates that the primary key value is automatically generated upon insertion, using an identity column strategy. The exact behavior may depend on the database being used.


  19. Create a Repository:
  20. Create an interface named "PaymentRepository" that represents a repository responsible for handling data access operations related to payments:

    package com.example.repository;
    
    import org.springframework.data.repository.CrudRepository;
    import org.springframework.stereotype.Repository;
    import com.example.entity.Payment;
    
    @Repository
    public interface PaymentRepository extends CrudRepository<Payment, Long> {
    
    }

  21. Create Data Transfer Objects:
  22. Create a DTO (Data Transfer Object) class named PaymentRequestDto:

    package com.example.dto;
    
    import lombok.Data;
    
    @Data
    public class PaymentRequestDto {
      private String senderId;
      private String receiverId;
      private double amount;
      private double fee;
      private String paymentScheduleAt;
    }

    Create a DTO (Data Transfer Object) class named PaymentResponseDto:

    package com.example.dto;
    
    import lombok.Data;
    
    @Data
    public class PaymentResponseDto {
      private int status;
      private String statusMessage;
      private Long jobId;
    }

  23. Create Custom Exception:
  24. Create classes to handle custom exceptions. Custom exceptions allow you to create specific exception types for your application that can be thrown when certain exceptional situations occur.

    Let's start by creating a Java class named Error with three private fields: message, status, and timestamp. This class represents data container that holds information related to an error:

    package com.example.exception.model;
    
    import lombok.Data;
    
    @Data
    public class Error {
    	private String message;
    	private int status;
    	private Long timestamp;
    }

    Create a custom exception class named InvalidDataException, which extends the RuntimeException class:

    package com.example.exception;
    
    public class InvalidDataException extends RuntimeException {
      private static final long serialVersionUID = 1L;
    
      public InvalidDataException(String message) {
        super(message);
      }
    
    }

    Create a Global Exception Handler class named GlobalExceptionHandlerController. The purpose of this class is to handle specific exceptions globally, providing consistent and customized error responses to clients when certain exceptions occur during the application's execution:

    package com.example.exception.controller;
    
    import java.util.Date;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import com.example.exception.InvalidDataException;
    import com.example.exception.model.Error;
    import jakarta.servlet.http.HttpServletRequest;
    
    @ControllerAdvice
    public class GlobalExceptionHandlerController {
    
      @ExceptionHandler(InvalidDataException.class)
      public ResponseEntity<Object> invalidData(InvalidDataException ex, HttpServletRequest request) {
        Error error = new Error();
        error.setMessage(ex.getMessage());
        error.setTimestamp(new Date().getTime());
        error.setStatus(HttpStatus.BAD_REQUEST.value());
        return new ResponseEntity<>(error, null, HttpStatus.BAD_REQUEST);
      }
      
    }

    Here, the class is annotated with @ControllerAdvice, which indicates that this class will provide advice (global exception handling) across all controllers. The method within the class is annotated with @ExceptionHandler, which specifies that these methods will handle specific exceptions when they occur. In the method, InvalidDataException exception is caught. The method then creates an Error object, which is a custom model class that we earlier.

    The information from the caught exception is used to populate the Error object. In this example, the error message from the caught exception is set as the message in the Error object. The current timestamp is set using new Date().getTime(), and the appropriate HTTP status code is also set.

    Finally, a ResponseEntity is created, wrapping the Error object, and returned with the appropriate HTTP status code. This way, when the exception occurs, the framework will call the corresponding handler method to generate an error response with the necessary details.


  25. Create a Utility Class:
  26. Create a utility class named CommonUtils. This class contains methods for converting dates to strings and strings to dates:

    package com.example.util;
    
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import com.example.exception.InvalidDataException;
    
    public class CommonUtils {
    
      static final String PAYMENT_SCHEDULE_PATTERN = "yyyy-MM-dd HH:mm:ss";
    
      public static Date convertStringToDate(String date) {
        if (date == null || date.isEmpty()) {
          return null;
        }
    
        SimpleDateFormat sdf = new SimpleDateFormat(PAYMENT_SCHEDULE_PATTERN);
        sdf.setLenient(false); // Disallow lenient parsing to ensure strict matching
    
        try {
          // Attempt to parse the date with the specified pattern
          Date scheduleDateTime = sdf.parse(date);
          
          if (scheduleDateTime.before(new Date())) {
            throw new InvalidDataException("paymentScheduleAt must be set to a date and time later than the current datetime.");
          }
          
          return scheduleDateTime;
        } catch (ParseException e) {
          // If parsing fails, it means the date does not match the pattern,
          throw new InvalidDataException("paymentScheduleAt must be in yyyy-MM-dd HH:mm:ss format.");
        }
      }
    
    }

  27. Create a Quartz Job:
  28. Create a Quartz job named "PaymentJob" by implementing the org.quartz.Job interface. For example:

    package com.example.job;
    
    import java.util.Optional;
    import org.quartz.Job;
    import org.quartz.JobDataMap;
    import org.quartz.JobExecutionContext;
    import org.quartz.JobExecutionException;
    import org.quartz.JobKey;
    import org.quartz.SchedulerException;
    import org.quartz.TriggerKey;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import com.example.entity.Payment;
    import com.example.repository.PaymentRepository;
    
    public class PaymentJob implements Job {
      private static final Logger LOGGER = LoggerFactory.getLogger(PaymentJob.class);
    
      @Autowired
      private PaymentRepository paymentRepository;
    
      @Override
      public void execute(JobExecutionContext context) throws JobExecutionException {
        LOGGER.info("Job starting...");
        /* Get message id recorded by scheduler during scheduling */
        JobDataMap dataMap = context.getJobDetail().getJobDataMap();
    
        String paymentId = dataMap.getString("paymentId");
    
        LOGGER.info("Job PaymentId: {}", paymentId);
    
        /* Get message from database by id */
        Optional<Payment> messageOpt = paymentRepository.findById(Long.parseLong(paymentId));
        
        /* Run payment logic here and update the table */
        Payment message = messageOpt.get();
        message.setActive(false);
        message.setStatus(0);
        paymentRepository.save(message);
    
        /* unschedule or delete after job gets executed */
    
        try {
          context.getScheduler().deleteJob(new JobKey(paymentId));
    
          TriggerKey triggerKey = new TriggerKey(paymentId);
    
          context.getScheduler().unscheduleJob(triggerKey);
    
        } catch (SchedulerException e) {
          LOGGER.error("Error while running job id: {} : {}", paymentId, e);
          e.printStackTrace();
        }
        LOGGER.info("Job complete...");
      }
    }

  29. Create Service Interface:
  30. Create a service interface named "PaymentService" that defines the contract for payment scheduling and unscheduling:

    package com.example.service;
    
    import com.example.dto.PaymentRequestDto;
    import com.example.dto.PaymentResponseDto;
    
    public interface PaymentService {
    
      PaymentResponseDto schedulePayment(PaymentRequestDto paymentRequest);
    
      PaymentResponseDto unschedulePayment(Long paymentId);
    }

  31. Create Service Implementation:
  32. Create an implementation class that implements the PaymentService interface and handles the business logic. Let's create a class called PaymentServiceImpl:

    package com.example.service.impl;
    
    import java.io.IOException;
    import java.util.Date;
    import java.util.Optional;
    import org.quartz.JobBuilder;
    import org.quartz.JobDetail;
    import org.quartz.JobKey;
    import org.quartz.Scheduler;
    import org.quartz.SchedulerException;
    import org.quartz.SimpleScheduleBuilder;
    import org.quartz.SimpleTrigger;
    import org.quartz.TriggerBuilder;
    import org.quartz.TriggerKey;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import com.example.config.QuartzConfig;
    import com.example.dto.PaymentRequestDto;
    import com.example.dto.PaymentResponseDto;
    import com.example.entity.Payment;
    import com.example.job.PaymentJob;
    import com.example.repository.PaymentRepository;
    import com.example.service.PaymentService;
    import com.example.util.CommonUtils;
    import jakarta.transaction.Transactional;
    
    @Service
    public class PaymentServiceImpl implements PaymentService {
    
      @Autowired
      private QuartzConfig quartzConfig;
      @Autowired
      private PaymentRepository paymentRepository;
    
      @Transactional
      @Override
      public PaymentResponseDto schedulePayment(PaymentRequestDto paymentRequest) {
    
        PaymentResponseDto response = new PaymentResponseDto();
    
        try {
          // Scheduling time to run job
          Date triggerJobAt = CommonUtils.convertStringToDate(paymentRequest.getPaymentScheduleAt());
    
          // save messages in table
          Payment payment = Payment.builder().senderId(paymentRequest.getSenderId())
              .receiverId(paymentRequest.getReceiverId()).amount(paymentRequest.getAmount())
              .fee(paymentRequest.getFee()).paymentScheduleAt(triggerJobAt).status(1).build();
    
          payment = paymentRepository.save(payment);
    
          // Creating JobDetail instance
          String paymentId = String.valueOf(payment.getId());
          JobDetail jobDetail = JobBuilder.newJob(PaymentJob.class).withIdentity(paymentId).build();
    
          // Adding JobDataMap to jobDetail
          jobDetail.getJobDataMap().put("paymentId", paymentId);
    
          SimpleTrigger trigger =
              TriggerBuilder.newTrigger().withIdentity(paymentId).startAt(triggerJobAt)
                  .withSchedule(
                      SimpleScheduleBuilder.simpleSchedule().withMisfireHandlingInstructionFireNow())
                  .build();
          // Getting scheduler instance
          Scheduler scheduler = quartzConfig.schedulerFactoryBean().getScheduler();
          scheduler.scheduleJob(jobDetail, trigger);
          scheduler.start();
          response.setStatus(0);
          response.setJobId(payment.getId());
          response.setStatusMessage("Successfully scheduled.");
        } catch (IOException | SchedulerException e) {
          // scheduling failed
          response.setStatus(-1);
          response.setStatusMessage("Error: " + e.getMessage());
          e.printStackTrace();
        }
        return response;
      }
    
      @Transactional
      @Override
      public PaymentResponseDto unschedulePayment(Long paymentId) {
        PaymentResponseDto response = new PaymentResponseDto();
    
        Optional<Payment> existingPaymentOpt = paymentRepository.findById(paymentId);
        if (!existingPaymentOpt.isPresent()) {
          response.setStatus(-1);
          response.setStatusMessage("Payment Not Found");
          return response;
        }
    
        Payment payment = existingPaymentOpt.get();
        payment.setActive(false); // deactivating
        payment.setStatus(-2); // -2 to indicate cancel
        paymentRepository.save(payment);
    
        try {
          Scheduler scheduler = quartzConfig.schedulerFactoryBean().getScheduler();
          String id = String.valueOf(paymentId);
          scheduler.deleteJob(new JobKey(id));
          TriggerKey triggerKey = new TriggerKey(id);
          scheduler.unscheduleJob(triggerKey);
          response.setStatus(0); // 0 to indicate success
          response.setStatusMessage("Successfully unscheduled.");
    
        } catch (IOException | SchedulerException e) {
          response.setStatus(-1);
          response.setStatusMessage("Fail to unschedule. Error: " + e.getMessage());
          e.printStackTrace();
        }
        return response;
      }
    
    }

  33. Create a Controller:
  34. Create a controller class named PaymentController that will handle HTTP requests and interact with the PaymentService:

    package com.example.controller;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.DeleteMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.bind.annotation.RestController;
    import com.example.dto.PaymentRequestDto;
    import com.example.dto.PaymentResponseDto;
    import com.example.service.PaymentService;
    
    @RestController
    @RequestMapping(path = "/payments")
    public class PaymentController {
    
      @Autowired
      private PaymentService paymentService;
    
      @PostMapping(path = "/schedule")
      public @ResponseBody PaymentResponseDto schedulePayment(
          @RequestBody PaymentRequestDto paymentRequestDto) {
    
        return paymentService.schedulePayment(paymentRequestDto);
      }
    
      @DeleteMapping(path = "/unschedule")
      public @ResponseBody PaymentResponseDto unschedulePayment(
          @PathVariable(name = "paymentId") Long paymentId) {
    
        return paymentService.unschedulePayment(paymentId);
      }
    
    }
  35. Enable JPA Auditing:
  36. Annotate the main class with @EnableJpaAuditing annotation. It is used to enable auditing in JPA entities. Auditing allows automatic population of specific fields such as created and updated in JPA entities based on the current date and time. It is commonly used to keep track of when an entity was created or last modified. For example:

    package com.example;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
    
    @EnableJpaAuditing
    @SpringBootApplication
    public class ExampleQuartzSchedulerMysqlApplication {
    
       public static void main(String[] args) {
          SpringApplication.run(ExampleQuartzSchedulerMysqlApplication.class, args);
       }
    
    }

  37. Create Quartz Tables:
  38. You're almost done! The remaining step is to generate Quartz configuration tables in your database. If Quartz is unable to locate its configuration tables, your application might come across this error "Error: Caused by: java.sql.SQLSyntaxErrorException: Table 'database_name.QRTZ_TRIGGERS' doesn't exist".

    NOTE: It is recommended to use a separate database for Quartz.

    To create Quartz configuration tables, simply run the provided MySQL script within your database:

    DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
    
    DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
    
    DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
    
    DROP TABLE IF EXISTS QRTZ_LOCKS;
    
    DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
    
    DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
    
    DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
    
    DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
    
    DROP TABLE IF EXISTS QRTZ_TRIGGERS;
    
    DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
    
    DROP TABLE IF EXISTS QRTZ_CALENDARS;
    
    CREATE TABLE QRTZ_JOB_DETAILS(
    
    SCHED_NAME VARCHAR(120) NOT NULL,
    
    JOB_NAME VARCHAR(190) NOT NULL,
    
    JOB_GROUP VARCHAR(190) NOT NULL,
    
    DESCRIPTION VARCHAR(250) NULL,
    
    JOB_CLASS_NAME VARCHAR(250) NOT NULL,
    
    IS_DURABLE VARCHAR(1) NOT NULL,
    
    IS_NONCONCURRENT VARCHAR(1) NOT NULL,
    
    IS_UPDATE_DATA VARCHAR(1) NOT NULL,
    
    REQUESTS_RECOVERY VARCHAR(1) NOT NULL,
    
    JOB_DATA BLOB NULL,
    
    PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP))
    
    ENGINE=InnoDB;
    
    CREATE TABLE QRTZ_TRIGGERS (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(190) NOT NULL,
    TRIGGER_GROUP VARCHAR(190) NOT NULL,
    JOB_NAME VARCHAR(190) NOT NULL,
    JOB_GROUP VARCHAR(190) NOT NULL,
    DESCRIPTION VARCHAR(250) NULL,
    NEXT_FIRE_TIME BIGINT(13) NULL,
    PREV_FIRE_TIME BIGINT(13) NULL,
    PRIORITY INTEGER NULL,
    TRIGGER_STATE VARCHAR(16) NOT NULL,
    TRIGGER_TYPE VARCHAR(8) NOT NULL,
    START_TIME BIGINT(13) NOT NULL,
    END_TIME BIGINT(13) NULL,
    CALENDAR_NAME VARCHAR(190) NULL,
    MISFIRE_INSTR SMALLINT(2) NULL,
    JOB_DATA BLOB NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
    REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP))
    
    ENGINE=InnoDB;
    
    CREATE TABLE QRTZ_SIMPLE_TRIGGERS (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(190) NOT NULL,
    TRIGGER_GROUP VARCHAR(190) NOT NULL,
    REPEAT_COUNT BIGINT(7) NOT NULL,
    REPEAT_INTERVAL BIGINT(12) NOT NULL,
    TIMES_TRIGGERED BIGINT(10) NOT NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
    REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
    
    ENGINE=InnoDB;
    
    CREATE TABLE QRTZ_CRON_TRIGGERS (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(190) NOT NULL,
    TRIGGER_GROUP VARCHAR(190) NOT NULL,
    CRON_EXPRESSION VARCHAR(120) NOT NULL,
    TIME_ZONE_ID VARCHAR(80),
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
    REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
    
    ENGINE=InnoDB;
    
    CREATE TABLE QRTZ_SIMPROP_TRIGGERS(
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(190) NOT NULL,
    TRIGGER_GROUP VARCHAR(190) NOT NULL,
    STR_PROP_1 VARCHAR(512) NULL,
    STR_PROP_2 VARCHAR(512) NULL,
    STR_PROP_3 VARCHAR(512) NULL,
    INT_PROP_1 INT NULL,
    INT_PROP_2 INT NULL,
    LONG_PROP_1 BIGINT NULL,
    LONG_PROP_2 BIGINT NULL,
    DEC_PROP_1 NUMERIC(13,4) NULL,
    DEC_PROP_2 NUMERIC(13,4) NULL,
    BOOL_PROP_1 VARCHAR(1) NULL,
    BOOL_PROP_2 VARCHAR(1) NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
    REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
    
    ENGINE=InnoDB;
    
    CREATE TABLE QRTZ_BLOB_TRIGGERS (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_NAME VARCHAR(190) NOT NULL,
    TRIGGER_GROUP VARCHAR(190) NOT NULL,
    BLOB_DATA BLOB NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    INDEX (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP),
    FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
    REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
    
    ENGINE=InnoDB;
    
    CREATE TABLE QRTZ_CALENDARS (
    SCHED_NAME VARCHAR(120) NOT NULL,
    CALENDAR_NAME VARCHAR(190) NOT NULL,
    CALENDAR BLOB NOT NULL,
    PRIMARY KEY (SCHED_NAME,CALENDAR_NAME))
    
    ENGINE=InnoDB;
    
    CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS (
    SCHED_NAME VARCHAR(120) NOT NULL,
    TRIGGER_GROUP VARCHAR(190) NOT NULL,
    PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP))
    
    ENGINE=InnoDB;
    
    CREATE TABLE QRTZ_FIRED_TRIGGERS (
    SCHED_NAME VARCHAR(120) NOT NULL,
    ENTRY_ID VARCHAR(95) NOT NULL,
    TRIGGER_NAME VARCHAR(190) NOT NULL,
    TRIGGER_GROUP VARCHAR(190) NOT NULL,
    INSTANCE_NAME VARCHAR(190) NOT NULL,
    FIRED_TIME BIGINT(13) NOT NULL,
    SCHED_TIME BIGINT(13) NOT NULL,
    PRIORITY INTEGER NOT NULL,
    STATE VARCHAR(16) NOT NULL,
    JOB_NAME VARCHAR(190) NULL,
    JOB_GROUP VARCHAR(190) NULL,
    IS_NONCONCURRENT VARCHAR(1) NULL,
    REQUESTS_RECOVERY VARCHAR(1) NULL,
    PRIMARY KEY (SCHED_NAME,ENTRY_ID))
    
    ENGINE=InnoDB;
    
    CREATE TABLE QRTZ_SCHEDULER_STATE (SCHED_NAME VARCHAR(120) NOT NULL,
    INSTANCE_NAME VARCHAR(190) NOT NULL,
    LAST_CHECKIN_TIME BIGINT(13) NOT NULL,
    CHECKIN_INTERVAL BIGINT(13) NOT NULL,
    PRIMARY KEY (SCHED_NAME,INSTANCE_NAME))
    
    ENGINE=InnoDB;
    
    CREATE TABLE QRTZ_LOCKS (SCHED_NAME VARCHAR(120) NOT NULL,
    LOCK_NAME VARCHAR(40) NOT NULL,
    PRIMARY KEY (SCHED_NAME,LOCK_NAME))
    
    ENGINE=InnoDB;
    
    CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY ON QRTZ_JOB_DETAILS(SCHED_NAME,
    REQUESTS_RECOVERY);
    
    CREATE INDEX IDX_QRTZ_J_GRP ON QRTZ_JOB_DETAILS(SCHED_NAME,JOB_GROUP);
    
    CREATE INDEX IDX_QRTZ_T_J ON QRTZ_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP);
    
    CREATE INDEX IDX_QRTZ_T_JG ON QRTZ_TRIGGERS(SCHED_NAME,JOB_GROUP);
    
    CREATE INDEX IDX_QRTZ_T_C ON QRTZ_TRIGGERS(SCHED_NAME,CALENDAR_NAME);
    
    CREATE INDEX IDX_QRTZ_T_G ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP);
    
    CREATE INDEX IDX_QRTZ_T_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE);
    
    CREATE INDEX IDX_QRTZ_T_N_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,
    TRIGGER_GROUP,TRIGGER_STATE);
    
    CREATE INDEX IDX_QRTZ_T_N_G_STATE ON QRTZ_TRIGGERS(SCHED_NAME,
    TRIGGER_GROUP, TRIGGER_STATE);
    
    CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME ON QRTZ_TRIGGERS(SCHED_NAME,
    NEXT_FIRE_TIME);
    
    CREATE INDEX IDX_QRTZ_T_NFT_ST ON QRTZ_TRIGGERS(SCHED_NAME,
    TRIGGER_STATE,NEXT_FIRE_TIME);
    
    CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,
    MISFIRE_INSTR,NEXT_FIRE_TIME);
    
    CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,
    MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE);
    
    CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP ON QRTZ_TRIGGERS(
    SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE);
    
    CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME ON QRTZ_FIRED_TRIGGERS(
    SCHED_NAME,INSTANCE_NAME);
    
    CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY ON QRTZ_FIRED_TRIGGERS(
    SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY);
    
    CREATE INDEX IDX_QRTZ_FT_J_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,
    JOB_NAME,JOB_GROUP);
    
    CREATE INDEX IDX_QRTZ_FT_JG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,
    JOB_GROUP);
    
    CREATE INDEX IDX_QRTZ_FT_T_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,
    TRIGGER_NAME,TRIGGER_GROUP);
    
    CREATE INDEX IDX_QRTZ_FT_TG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,
    TRIGGER_GROUP);
    
    commit;

  39. Run and Test your Spring Boot application:
  40. Use your IDE's build tools (Maven or Gradle) to build your project and resolve dependencies. Once the build is successful, run the main class of your application. The Spring Boot application will start and deploy on an embedded web server (Tomcat) automatically. You should see logs indicating that the application has started.

    Use API testing tools (example: Postman) to test your application's endpoints:

    Scheduling a payment example:



    Unscheduling a payment example: