# Send Appointment Reminders with Java and Spark

> \[!NOTE]
>
> Ahoy! We now recommend you build your appointment reminders with Twilio's built-in Message Scheduling functionality. Head on over to the [Message Scheduling documentation](/docs/messaging/features/message-scheduling) to learn more about scheduling messages.

This is a Java 8 web application written using [Spark](https://github.com/perwendel/spark) that demonstrates how to send appointment reminders to your customers with Twilio SMS.

Check out [this application on GitHub](https://github.com/TwilioDevEd/appointment-reminders-java) to download the code and read instructions on how to run it yourself. In this tutorial, we'll show you the key bits of code necessary to drive this use case.

*[Check out how Yelp uses SMS to confirm restaurant reservations for diners.](https://customers.twilio.com/1154/yelp/?utm_source=docs\&utm_campaign=docs_to_stories)*

Let's get started! Click the button below to move on to the next step of the tutorial.

## Create the Quartz job scheduler

The Quartz scheduler is instantiated in the main method of our web application, before we set up the routes. We pass a reference to this scheduler to the controller so it can schedule jobs to send out appointment reminders. Note that by default, Quartz temporarily stores jobs in memory, but in production you can configure Quartz to store jobs in a data store of your choice.

```java title="Create the Quartz job scheduler" description="src/main/java/com/twilio/appointmentreminders/Server.java"
// !mark(43:52)
package com.twilio.appointmentreminders;

import com.twilio.appointmentreminders.controllers.AppointmentController;
import com.twilio.appointmentreminders.models.AppointmentService;
import com.twilio.appointmentreminders.util.AppSetup;
import com.twilio.appointmentreminders.util.LoggingFilter;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.impl.StdSchedulerFactory;
import spark.Spark;
import spark.template.mustache.MustacheTemplateEngine;

import javax.persistence.EntityManagerFactory;

import static spark.Spark.*;

/**
 * Main application class. The environment is set up here, and all necessary services are run.
 */
public class Server {
  public static void main(String[] args) {
    AppSetup appSetup = new AppSetup();

    /**
     * Sets the port in which the application will run. Takes the port value from PORT
     * environment variable, if not set, uses Spark default port 4567.
     */
    port(appSetup.getPortNumber());

    /**
     * Gets the entity manager based on environment variable DATABASE_URL and injects it into
     * AppointmentService which handles all DB operations.
     */
    EntityManagerFactory factory = appSetup.getEntityManagerFactory();
    AppointmentService service = new AppointmentService(factory.createEntityManager());

    /**
     * Specifies the directory within resources that will be publicly available when the
     * application is running. Place static web files in this directory (JS, CSS).
     */
    Spark.staticFileLocation("/public");

    /** Creates a new instance of Quartz Scheduler and starts it. */
    Scheduler scheduler = null;
    try {
      scheduler = StdSchedulerFactory.getDefaultScheduler();

      scheduler.start();

    } catch (SchedulerException se) {
      System.out.println("Unable to start scheduler service");
    }

    /** Injects AppointmentService and Scheduler into the controller. */
    AppointmentController controller = new AppointmentController(service, scheduler);

    /**
     * Defines all url paths for the application and assigns a controller method for each.
     * If the route renders a page, the templating engine must be specified, and the controller
     * should return the appropriate Route object.
     */
    get("/", controller.index, new MustacheTemplateEngine());
    get("/new", controller.renderCreatePage, new MustacheTemplateEngine());
    post("/create", controller.create, new MustacheTemplateEngine());
    post("/delete", controller.delete);

    afterAfter(new LoggingFilter());
  }
}
```

Next let's see how we create a new `Appointment`.

## Create an appointment

Once validations pass and the appointment is persisted to the database.

With `scheduleJob` a notification is scheduled based on the time of the appointment.

```java title="Create an appointment" description="src/main/java/com/twilio/appointmentreminders/controllers/AppointmentController.java"
// !mark(69:111)
package com.twilio.appointmentreminders.controllers;

import com.twilio.appointmentreminders.models.Appointment;
import com.twilio.appointmentreminders.models.AppointmentService;
import com.twilio.appointmentreminders.util.AppointmentScheduler;
import com.twilio.appointmentreminders.util.FieldValidator;
import com.twilio.appointmentreminders.util.TimeZones;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import spark.ModelAndView;
import spark.Route;
import spark.TemplateViewRoute;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;

/**
 * Appointment controller class. Holds all the methods that handle the applications requests.
 * This methods are mapped to a specific URL on the main Server file of the application.
 */
@SuppressWarnings({"rawtypes", "unchecked"})
public class AppointmentController {
  private Scheduler scheduler;
  private AppointmentService service;

  public AppointmentController(AppointmentService service, Scheduler scheduler) {
    this.service = service;
    this.scheduler = scheduler;
  }

  public TemplateViewRoute renderCreatePage = (request, response) -> {
    Map map = new HashMap();

    map.put("zones", timeZones());
    return new ModelAndView(map, "new.mustache");
  };

  public TemplateViewRoute index = (request, response) -> {
    Map map = new HashMap();

    List<Appointment> appointments = service.findAll();
    map.put("appointments", appointments);

    return new ModelAndView(map, "index.mustache");
  };

  public Route delete = (request, response) -> {
    String id = request.queryParams("id");
    Long idLong = Long.parseLong(id, 10);

    Appointment appointment = service.getAppointment(idLong);
    service.delete(appointment);

    response.redirect("/");
    return response;
  };

  /**
   * Controller method that creates a new appointment. Also, schedules an
   * appointment reminder once the actual appointment is persisted to the database.
   */
  public TemplateViewRoute create = (request, response) -> {
    FieldValidator validator =
        new FieldValidator(new String[] {"name", "phoneNumber", "date", "delta", "timeZone"});

    if (validator.valid(request)) {
      String name = request.queryParams("name");
      String phoneNumber = request.queryParams("phoneNumber");
      String date = request.queryParams("date");
      int delta = 0;
      try {
        delta = Integer.parseInt(request.queryParams("delta"));
      } catch (NumberFormatException e) {
        System.out.println("Invalid format number for appointment delta");
      }
      String timeZone = request.queryParams("timeZone");

      DateTimeZone zone = DateTimeZone.forID(timeZone);
      DateTimeZone zoneUTC = DateTimeZone.UTC;

      DateTime dt;
      DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");
      formatter = formatter.withZone(zone);
      dt = formatter.parseDateTime(date);
      formatter = formatter.withZone(zoneUTC);
      String dateUTC = dt.toString(formatter);

      Appointment appointment = new Appointment(name, phoneNumber, delta, dateUTC, timeZone);
      service.create(appointment);

      scheduleJob(appointment);

      response.redirect("/");
    }

    Map map = new HashMap();

    map.put("zones", timeZones());
    return new ModelAndView(map, "new.mustache");
  };

  /**
   * Schedules a AppointmentScheduler instance to be created and executed in the specified future
   * date coming from the appointment entity
   * @param appointment The newly created Appointment that has already been persisted to the DB.
   */
  private void scheduleJob(Appointment appointment) {
    String appointmentId = appointment.getId().toString();

    DateTimeZone zone = DateTimeZone.forID(appointment.getTimeZone());
    DateTime dt;
    DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");
    formatter = formatter.withZone(zone);
    dt = formatter.parseDateTime(appointment.getDate());
    Date finalDate = dt.minusMinutes(appointment.getDelta()).toDate();

    JobDetail job =
        newJob(AppointmentScheduler.class).withIdentity("Appointment_J_" + appointmentId)
            .usingJobData("appointmentId", appointmentId).build();

    Trigger trigger =
        newTrigger().withIdentity("Appointment_T_" + appointmentId).startAt(finalDate).build();

    try {
      scheduler.scheduleJob(job, trigger);
    } catch (SchedulerException e) {
      System.out.println("Unable to schedule the Job");
    }
  }

  private List<String> timeZones() {
    TimeZones tz = new TimeZones();

    return tz.getTimeZones();
  }
}
```

We will dig further into that function next.

## Schedule the reminder job

The controller uses the injected scheduler to set up a notification. The `AppointmentScheduler` class is used here to actually send out the notification via SMS through a Quartz trigger.

```java title="Schedule the reminder job" description="src/main/java/com/twilio/appointmentreminders/controllers/AppointmentController.java"
// !mark(113:140)
package com.twilio.appointmentreminders.controllers;

import com.twilio.appointmentreminders.models.Appointment;
import com.twilio.appointmentreminders.models.AppointmentService;
import com.twilio.appointmentreminders.util.AppointmentScheduler;
import com.twilio.appointmentreminders.util.FieldValidator;
import com.twilio.appointmentreminders.util.TimeZones;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import spark.ModelAndView;
import spark.Route;
import spark.TemplateViewRoute;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.quartz.JobBuilder.newJob;
import static org.quartz.TriggerBuilder.newTrigger;

/**
 * Appointment controller class. Holds all the methods that handle the applications requests.
 * This methods are mapped to a specific URL on the main Server file of the application.
 */
@SuppressWarnings({"rawtypes", "unchecked"})
public class AppointmentController {
  private Scheduler scheduler;
  private AppointmentService service;

  public AppointmentController(AppointmentService service, Scheduler scheduler) {
    this.service = service;
    this.scheduler = scheduler;
  }

  public TemplateViewRoute renderCreatePage = (request, response) -> {
    Map map = new HashMap();

    map.put("zones", timeZones());
    return new ModelAndView(map, "new.mustache");
  };

  public TemplateViewRoute index = (request, response) -> {
    Map map = new HashMap();

    List<Appointment> appointments = service.findAll();
    map.put("appointments", appointments);

    return new ModelAndView(map, "index.mustache");
  };

  public Route delete = (request, response) -> {
    String id = request.queryParams("id");
    Long idLong = Long.parseLong(id, 10);

    Appointment appointment = service.getAppointment(idLong);
    service.delete(appointment);

    response.redirect("/");
    return response;
  };

  /**
   * Controller method that creates a new appointment. Also, schedules an
   * appointment reminder once the actual appointment is persisted to the database.
   */
  public TemplateViewRoute create = (request, response) -> {
    FieldValidator validator =
        new FieldValidator(new String[] {"name", "phoneNumber", "date", "delta", "timeZone"});

    if (validator.valid(request)) {
      String name = request.queryParams("name");
      String phoneNumber = request.queryParams("phoneNumber");
      String date = request.queryParams("date");
      int delta = 0;
      try {
        delta = Integer.parseInt(request.queryParams("delta"));
      } catch (NumberFormatException e) {
        System.out.println("Invalid format number for appointment delta");
      }
      String timeZone = request.queryParams("timeZone");

      DateTimeZone zone = DateTimeZone.forID(timeZone);
      DateTimeZone zoneUTC = DateTimeZone.UTC;

      DateTime dt;
      DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");
      formatter = formatter.withZone(zone);
      dt = formatter.parseDateTime(date);
      formatter = formatter.withZone(zoneUTC);
      String dateUTC = dt.toString(formatter);

      Appointment appointment = new Appointment(name, phoneNumber, delta, dateUTC, timeZone);
      service.create(appointment);

      scheduleJob(appointment);

      response.redirect("/");
    }

    Map map = new HashMap();

    map.put("zones", timeZones());
    return new ModelAndView(map, "new.mustache");
  };

  /**
   * Schedules a AppointmentScheduler instance to be created and executed in the specified future
   * date coming from the appointment entity
   * @param appointment The newly created Appointment that has already been persisted to the DB.
   */
  private void scheduleJob(Appointment appointment) {
    String appointmentId = appointment.getId().toString();

    DateTimeZone zone = DateTimeZone.forID(appointment.getTimeZone());
    DateTime dt;
    DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");
    formatter = formatter.withZone(zone);
    dt = formatter.parseDateTime(appointment.getDate());
    Date finalDate = dt.minusMinutes(appointment.getDelta()).toDate();

    JobDetail job =
        newJob(AppointmentScheduler.class).withIdentity("Appointment_J_" + appointmentId)
            .usingJobData("appointmentId", appointmentId).build();

    Trigger trigger =
        newTrigger().withIdentity("Appointment_T_" + appointmentId).startAt(finalDate).build();

    try {
      scheduler.scheduleJob(job, trigger);
    } catch (SchedulerException e) {
      System.out.println("Unable to schedule the Job");
    }
  }

  private List<String> timeZones() {
    TimeZones tz = new TimeZones();

    return tz.getTimeZones();
  }
}
```

Let's look at how we handle this trigger.

## Configure the application to send SMS messages

Every time a scheduled job is triggered by Quartz, an instance of the `AppointmentScheduler` class is created to handle the job. When the class is loaded, we create a `RestClient` to interact with the Twilio API using our account credentials.

```java title="Configure the application to send SMS messages" description="src/main/java/com/twilio/appointmentreminders/util/AppointmentScheduler.java"
// !mark(18:30,36:37)
package com.twilio.appointmentreminders.util;

import com.twilio.Twilio;
import com.twilio.appointmentreminders.models.Appointment;
import com.twilio.appointmentreminders.models.AppointmentService;
import com.twilio.exception.TwilioException;
import com.twilio.rest.api.v2010.account.Message;
import com.twilio.type.PhoneNumber;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.persistence.EntityManagerFactory;

public class AppointmentScheduler implements Job {

  private static Logger logger = LoggerFactory.getLogger(AppointmentScheduler.class);

  private static AppSetup appSetup = new AppSetup();

  public static final String ACCOUNT_SID = appSetup.getAccountSid();
  public static final String AUTH_TOKEN = appSetup.getAuthToken();
  public static final String TWILIO_NUMBER = appSetup.getTwilioPhoneNumber();

  public AppointmentScheduler() {}

  public void execute(JobExecutionContext context) throws JobExecutionException {
    AppSetup appSetup = new AppSetup();

    EntityManagerFactory factory = appSetup.getEntityManagerFactory();
    AppointmentService service = new AppointmentService(factory.createEntityManager());

    // Initialize the Twilio client
    Twilio.init(ACCOUNT_SID, AUTH_TOKEN);

    JobDataMap dataMap = context.getJobDetail().getJobDataMap();

    String appointmentId = dataMap.getString("appointmentId");

    Appointment appointment = service.getAppointment(Long.parseLong(appointmentId, 10));
    if (appointment != null) {
      String name = appointment.getName();
      String phoneNumber = appointment.getPhoneNumber();
      String date = appointment.getDate();
      String messageBody = "Remember: " + name + ", on " + date + " you have an appointment!";

      try {
        Message message = Message
                .creator(new PhoneNumber(phoneNumber), new PhoneNumber(TWILIO_NUMBER), messageBody)
                .create();
        System.out.println("Message sent! Message SID: " + message.getSid());
      } catch(TwilioException e) {
        logger.error("An exception occurred trying to send the message \"{}\" to {}." +
                " \nTwilio returned: {} \n", messageBody, phoneNumber, e.getMessage());
      }


    }
  }
}
```

Next let's look at how the SMS is sent.

## Send an SMS message from a background job

When the `execute` method is called on an `AppointmentScheduler` instance, we use the Twilio REST API client to actually send a formatted reminder message to our customer via SMS.

```java title="Scheduled task to send SMS messages" description="src/main/java/com/twilio/appointmentreminders/util/AppointmentScheduler.java"
// !mark(30:61)
package com.twilio.appointmentreminders.util;

import com.twilio.Twilio;
import com.twilio.appointmentreminders.models.Appointment;
import com.twilio.appointmentreminders.models.AppointmentService;
import com.twilio.exception.TwilioException;
import com.twilio.rest.api.v2010.account.Message;
import com.twilio.type.PhoneNumber;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.persistence.EntityManagerFactory;

public class AppointmentScheduler implements Job {

  private static Logger logger = LoggerFactory.getLogger(AppointmentScheduler.class);

  private static AppSetup appSetup = new AppSetup();

  public static final String ACCOUNT_SID = appSetup.getAccountSid();
  public static final String AUTH_TOKEN = appSetup.getAuthToken();
  public static final String TWILIO_NUMBER = appSetup.getTwilioPhoneNumber();

  public AppointmentScheduler() {}

  public void execute(JobExecutionContext context) throws JobExecutionException {
    AppSetup appSetup = new AppSetup();

    EntityManagerFactory factory = appSetup.getEntityManagerFactory();
    AppointmentService service = new AppointmentService(factory.createEntityManager());

    // Initialize the Twilio client
    Twilio.init(ACCOUNT_SID, AUTH_TOKEN);

    JobDataMap dataMap = context.getJobDetail().getJobDataMap();

    String appointmentId = dataMap.getString("appointmentId");

    Appointment appointment = service.getAppointment(Long.parseLong(appointmentId, 10));
    if (appointment != null) {
      String name = appointment.getName();
      String phoneNumber = appointment.getPhoneNumber();
      String date = appointment.getDate();
      String messageBody = "Remember: " + name + ", on " + date + " you have an appointment!";

      try {
        Message message = Message
                .creator(new PhoneNumber(phoneNumber), new PhoneNumber(TWILIO_NUMBER), messageBody)
                .create();
        System.out.println("Message sent! Message SID: " + message.getSid());
      } catch(TwilioException e) {
        logger.error("An exception occurred trying to send the message \"{}\" to {}." +
                " \nTwilio returned: {} \n", messageBody, phoneNumber, e.getMessage());
      }


    }
  }
}
```

That's it! We've successfully set up automated appointment reminders for our customers, which will be delivered via SMS.

## Where to next?

To learn more, see the [JavaDoc for the Twilio SDK](https://twilio.github.io/twilio-java/) and our guides for [SMS](/docs/messaging/tutorials/how-to-send-sms-messages) and [voice](/docs/voice/tutorials/how-to-make-outbound-phone-calls).
