Quartz with MongoDB and Spring Boot Example

In this tutorial, you will learn how to create a scheduler application using Spring Quartz and MongoDB 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 MongoDB database via JDBC.

Follow these steps to create a scheduler application in Spring Boot using Quartz with MongoDB, 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, Spring Data MongoDB, 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 MongoDB, we also need to add Quartz MongoDB library. Find the latest version of Quartz MongoDB library in the Quartz MongoDB Maven Repository.

    Here is a list of dependencies that the project requires: Quartz Scheduler, and Spring Data MongoDB.

    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-mongodb</artifactId>
    </dependency>
    
    <dependency>
        <groupId>io.fluidsonic.mirror</groupId>
        <artifactId>quartz-mongodb</artifactId>
        <version>2.2.0-rc2</version>
    </dependency>

    For Gradle:

    implementation 'org.springframework.boot:spring-boot-starter-quartz'
    implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
    implementation group: 'io.fluidsonic.mirror', name: 'quartz-mongodb', version: '2.2.0-rc2'

  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:

    # Server port
    server.port = 8080
    
    # Mongo Configuration
    spring.data.mongodb.host = localhost
    spring.data.mongodb.port = 27017
    spring.data.mongodb.database = database_name
    spring.data.mongodb.username = your-username
    spring.data.mongodb.password = your-password
    
    #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:

    # Use the MongoDB store
    org.quartz.jobStore.class=com.novemberain.quartz.mongodb.MongoDBJobStore
    
    # MongoDB URI (optional if 'org.quartz.jobStore.addresses' is set)
    org.quartz.jobStore.mongoUri=mongodb://jake:Testing123$@localhost:27017/quartz
    
    # comma separated list of mongodb hosts/replica set seeds (optional if 'org.quartz.jobStore.mongoUri' is set)
    # org.quartz.jobStore.addresses=host1
    
    # database name
    org.quartz.jobStore.dbName=quartz
    
    # Will be used to create collections like mycol_jobs, mycol_triggers, mycol_calendars, mycol_locks
    org.quartz.jobStore.collectionPrefix=mycol
    
    # thread count setting is ignored by the MongoDB store but Quartz requries it
    org.quartz.threadPool.threadCount=1
    
    # turn clustering on:
    org.quartz.jobStore.isClustered=true
    
    # Must be unique for each node or AUTO to use autogenerated:
    org.quartz.scheduler.instanceId=AUTO
    
    # org.quartz.scheduler.instanceId=node1
    
    # The same cluster name on each node:
    org.quartz.scheduler.instanceName=clusterName

    Here are some explanations of the above configurations:

    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 "com.novemberain.quartz.mongodb.MongoDBJobStore" will tell Quartz to use MongoDB database for storing Quartz`s data.

    org.quartz.jobStore.dbName: This specifies the database name.

    org.quartz.scheduler.instanceId: 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.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 clusterName.


  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.Id;
    import org.springframework.data.annotation.LastModifiedDate;
    import org.springframework.data.mongodb.core.mapping.Document;
    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    @Builder
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Document(collection = "payment")
    public class Payment {
    
      @Id
      private String id;
      private String senderId;
      private String receiverId;
      private double amount;
      private double fee;
      private Date paymentScheduleAt;
      private int status;
      private Boolean active;
      private String createdBy;
      private String modifiedBy;
      @CreatedDate
      private Date created;
      @LastModifiedDate
      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 @Document(collection = "table_name") 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 "collection" 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.


  19. Create a Repository:
  20. Create a PaymentRepository interface 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, String> {
    
    }

  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 String 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(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(String 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;
    
    @Service
    public class PaymentServiceImpl implements PaymentService {
    
      @Autowired
      private QuartzConfig quartzConfig;
      @Autowired
      private PaymentRepository paymentRepository;
    
      @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;
      }
    
    
      @Override
      public PaymentResponseDto unschedulePayment(String 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/{paymentId}")
      public @ResponseBody PaymentResponseDto unschedulePayment(
          @PathVariable(name = "paymentId") String paymentId) {
    
        return paymentService.unschedulePayment(paymentId);
      }
    }
  35. Enable JPA Auditing:
  36. Annotate the main class with @EnableMongoAuditing 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.mongodb.config.EnableMongoAuditing;
    
    @EnableMongoAuditing
    @SpringBootApplication
    public class ExampleQuartzSchedulerMongodbApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(ExampleQuartzSchedulerMongodbApplication.class, args);
    	}
    
    }

  37. Run and Test your Spring Boot application:
  38. 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: