Skip to contentSkip to navigationSkip to topbar
Rate this page:
On this page

Send Appointment Reminders with Java and Spark


(information)

Info

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 to learn more about scheduling messages!

This is a Java 8 web application written using Spark(link takes you to an external page) that demonstrates how to send appointment reminders to your customers with Twilio SMS.

Check out this application on GitHub(link takes you to an external page) 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.(link takes you to an external page)

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


Create the Quartz job scheduler

create-the-quartz-job-scheduler page anchor

The Quartz scheduler is instantiated in the main method of our web application, before we set up the routes(link takes you to an external page). 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.

Create the Quartz job scheduler

create-the-quartz-job-scheduler-1 page anchor

src/main/java/com/twilio/appointmentreminders/Server.java


_69
package com.twilio.appointmentreminders;
_69
_69
import com.twilio.appointmentreminders.controllers.AppointmentController;
_69
import com.twilio.appointmentreminders.models.AppointmentService;
_69
import com.twilio.appointmentreminders.util.AppSetup;
_69
import com.twilio.appointmentreminders.util.LoggingFilter;
_69
import org.quartz.Scheduler;
_69
import org.quartz.SchedulerException;
_69
import org.quartz.impl.StdSchedulerFactory;
_69
import spark.Spark;
_69
import spark.template.mustache.MustacheTemplateEngine;
_69
_69
import javax.persistence.EntityManagerFactory;
_69
_69
import static spark.Spark.*;
_69
_69
/**
_69
* Main application class. The environment is set up here, and all necessary services are run.
_69
*/
_69
public class Server {
_69
public static void main(String[] args) {
_69
AppSetup appSetup = new AppSetup();
_69
_69
/**
_69
* Sets the port in which the application will run. Takes the port value from PORT
_69
* environment variable, if not set, uses Spark default port 4567.
_69
*/
_69
port(appSetup.getPortNumber());
_69
_69
/**
_69
* Gets the entity manager based on environment variable DATABASE_URL and injects it into
_69
* AppointmentService which handles all DB operations.
_69
*/
_69
EntityManagerFactory factory = appSetup.getEntityManagerFactory();
_69
AppointmentService service = new AppointmentService(factory.createEntityManager());
_69
_69
/**
_69
* Specifies the directory within resources that will be publicly available when the
_69
* application is running. Place static web files in this directory (JS, CSS).
_69
*/
_69
Spark.staticFileLocation("/public");
_69
_69
/** Creates a new instance of Quartz Scheduler and starts it. */
_69
Scheduler scheduler = null;
_69
try {
_69
scheduler = StdSchedulerFactory.getDefaultScheduler();
_69
_69
scheduler.start();
_69
_69
} catch (SchedulerException se) {
_69
System.out.println("Unable to start scheduler service");
_69
}
_69
_69
/** Injects AppointmentService and Scheduler into the controller. */
_69
AppointmentController controller = new AppointmentController(service, scheduler);
_69
_69
/**
_69
* Defines all url paths for the application and assigns a controller method for each.
_69
* If the route renders a page, the templating engine must be specified, and the controller
_69
* should return the appropriate Route object.
_69
*/
_69
get("/", controller.index, new MustacheTemplateEngine());
_69
get("/new", controller.renderCreatePage, new MustacheTemplateEngine());
_69
post("/create", controller.create, new MustacheTemplateEngine());
_69
post("/delete", controller.delete);
_69
_69
afterAfter(new LoggingFilter());
_69
}
_69
}

Next let's see how we create a new 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.

src/main/java/com/twilio/appointmentreminders/controllers/AppointmentController.java


_147
package com.twilio.appointmentreminders.controllers;
_147
_147
import com.twilio.appointmentreminders.models.Appointment;
_147
import com.twilio.appointmentreminders.models.AppointmentService;
_147
import com.twilio.appointmentreminders.util.AppointmentScheduler;
_147
import com.twilio.appointmentreminders.util.FieldValidator;
_147
import com.twilio.appointmentreminders.util.TimeZones;
_147
import org.joda.time.DateTime;
_147
import org.joda.time.DateTimeZone;
_147
import org.joda.time.format.DateTimeFormat;
_147
import org.joda.time.format.DateTimeFormatter;
_147
import org.quartz.JobDetail;
_147
import org.quartz.Scheduler;
_147
import org.quartz.SchedulerException;
_147
import org.quartz.Trigger;
_147
import spark.ModelAndView;
_147
import spark.Route;
_147
import spark.TemplateViewRoute;
_147
_147
import java.util.Date;
_147
import java.util.HashMap;
_147
import java.util.List;
_147
import java.util.Map;
_147
_147
import static org.quartz.JobBuilder.newJob;
_147
import static org.quartz.TriggerBuilder.newTrigger;
_147
_147
/**
_147
* Appointment controller class. Holds all the methods that handle the applications requests.
_147
* This methods are mapped to a specific URL on the main Server file of the application.
_147
*/
_147
@SuppressWarnings({"rawtypes", "unchecked"})
_147
public class AppointmentController {
_147
private Scheduler scheduler;
_147
private AppointmentService service;
_147
_147
public AppointmentController(AppointmentService service, Scheduler scheduler) {
_147
this.service = service;
_147
this.scheduler = scheduler;
_147
}
_147
_147
public TemplateViewRoute renderCreatePage = (request, response) -> {
_147
Map map = new HashMap();
_147
_147
map.put("zones", timeZones());
_147
return new ModelAndView(map, "new.mustache");
_147
};
_147
_147
public TemplateViewRoute index = (request, response) -> {
_147
Map map = new HashMap();
_147
_147
List<Appointment> appointments = service.findAll();
_147
map.put("appointments", appointments);
_147
_147
return new ModelAndView(map, "index.mustache");
_147
};
_147
_147
public Route delete = (request, response) -> {
_147
String id = request.queryParams("id");
_147
Long idLong = Long.parseLong(id, 10);
_147
_147
Appointment appointment = service.getAppointment(idLong);
_147
service.delete(appointment);
_147
_147
response.redirect("/");
_147
return response;
_147
};
_147
_147
/**
_147
* Controller method that creates a new appointment. Also, schedules an
_147
* appointment reminder once the actual appointment is persisted to the database.
_147
*/
_147
public TemplateViewRoute create = (request, response) -> {
_147
FieldValidator validator =
_147
new FieldValidator(new String[] {"name", "phoneNumber", "date", "delta", "timeZone"});
_147
_147
if (validator.valid(request)) {
_147
String name = request.queryParams("name");
_147
String phoneNumber = request.queryParams("phoneNumber");
_147
String date = request.queryParams("date");
_147
int delta = 0;
_147
try {
_147
delta = Integer.parseInt(request.queryParams("delta"));
_147
} catch (NumberFormatException e) {
_147
System.out.println("Invalid format number for appointment delta");
_147
}
_147
String timeZone = request.queryParams("timeZone");
_147
_147
DateTimeZone zone = DateTimeZone.forID(timeZone);
_147
DateTimeZone zoneUTC = DateTimeZone.UTC;
_147
_147
DateTime dt;
_147
DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");
_147
formatter = formatter.withZone(zone);
_147
dt = formatter.parseDateTime(date);
_147
formatter = formatter.withZone(zoneUTC);
_147
String dateUTC = dt.toString(formatter);
_147
_147
Appointment appointment = new Appointment(name, phoneNumber, delta, dateUTC, timeZone);
_147
service.create(appointment);
_147
_147
scheduleJob(appointment);
_147
_147
response.redirect("/");
_147
}
_147
_147
Map map = new HashMap();
_147
_147
map.put("zones", timeZones());
_147
return new ModelAndView(map, "new.mustache");
_147
};
_147
_147
/**
_147
* Schedules a AppointmentScheduler instance to be created and executed in the specified future
_147
* date coming from the appointment entity
_147
* @param appointment The newly created Appointment that has already been persisted to the DB.
_147
*/
_147
private void scheduleJob(Appointment appointment) {
_147
String appointmentId = appointment.getId().toString();
_147
_147
DateTimeZone zone = DateTimeZone.forID(appointment.getTimeZone());
_147
DateTime dt;
_147
DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");
_147
formatter = formatter.withZone(zone);
_147
dt = formatter.parseDateTime(appointment.getDate());
_147
Date finalDate = dt.minusMinutes(appointment.getDelta()).toDate();
_147
_147
JobDetail job =
_147
newJob(AppointmentScheduler.class).withIdentity("Appointment_J_" + appointmentId)
_147
.usingJobData("appointmentId", appointmentId).build();
_147
_147
Trigger trigger =
_147
newTrigger().withIdentity("Appointment_T_" + appointmentId).startAt(finalDate).build();
_147
_147
try {
_147
scheduler.scheduleJob(job, trigger);
_147
} catch (SchedulerException e) {
_147
System.out.println("Unable to schedule the Job");
_147
}
_147
}
_147
_147
private List<String> timeZones() {
_147
TimeZones tz = new TimeZones();
_147
_147
return tz.getTimeZones();
_147
}
_147
}

We will dig further into that function next.


Schedule the reminder job

schedule-the-reminder-job page anchor

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.

src/main/java/com/twilio/appointmentreminders/controllers/AppointmentController.java


_147
package com.twilio.appointmentreminders.controllers;
_147
_147
import com.twilio.appointmentreminders.models.Appointment;
_147
import com.twilio.appointmentreminders.models.AppointmentService;
_147
import com.twilio.appointmentreminders.util.AppointmentScheduler;
_147
import com.twilio.appointmentreminders.util.FieldValidator;
_147
import com.twilio.appointmentreminders.util.TimeZones;
_147
import org.joda.time.DateTime;
_147
import org.joda.time.DateTimeZone;
_147
import org.joda.time.format.DateTimeFormat;
_147
import org.joda.time.format.DateTimeFormatter;
_147
import org.quartz.JobDetail;
_147
import org.quartz.Scheduler;
_147
import org.quartz.SchedulerException;
_147
import org.quartz.Trigger;
_147
import spark.ModelAndView;
_147
import spark.Route;
_147
import spark.TemplateViewRoute;
_147
_147
import java.util.Date;
_147
import java.util.HashMap;
_147
import java.util.List;
_147
import java.util.Map;
_147
_147
import static org.quartz.JobBuilder.newJob;
_147
import static org.quartz.TriggerBuilder.newTrigger;
_147
_147
/**
_147
* Appointment controller class. Holds all the methods that handle the applications requests.
_147
* This methods are mapped to a specific URL on the main Server file of the application.
_147
*/
_147
@SuppressWarnings({"rawtypes", "unchecked"})
_147
public class AppointmentController {
_147
private Scheduler scheduler;
_147
private AppointmentService service;
_147
_147
public AppointmentController(AppointmentService service, Scheduler scheduler) {
_147
this.service = service;
_147
this.scheduler = scheduler;
_147
}
_147
_147
public TemplateViewRoute renderCreatePage = (request, response) -> {
_147
Map map = new HashMap();
_147
_147
map.put("zones", timeZones());
_147
return new ModelAndView(map, "new.mustache");
_147
};
_147
_147
public TemplateViewRoute index = (request, response) -> {
_147
Map map = new HashMap();
_147
_147
List<Appointment> appointments = service.findAll();
_147
map.put("appointments", appointments);
_147
_147
return new ModelAndView(map, "index.mustache");
_147
};
_147
_147
public Route delete = (request, response) -> {
_147
String id = request.queryParams("id");
_147
Long idLong = Long.parseLong(id, 10);
_147
_147
Appointment appointment = service.getAppointment(idLong);
_147
service.delete(appointment);
_147
_147
response.redirect("/");
_147
return response;
_147
};
_147
_147
/**
_147
* Controller method that creates a new appointment. Also, schedules an
_147
* appointment reminder once the actual appointment is persisted to the database.
_147
*/
_147
public TemplateViewRoute create = (request, response) -> {
_147
FieldValidator validator =
_147
new FieldValidator(new String[] {"name", "phoneNumber", "date", "delta", "timeZone"});
_147
_147
if (validator.valid(request)) {
_147
String name = request.queryParams("name");
_147
String phoneNumber = request.queryParams("phoneNumber");
_147
String date = request.queryParams("date");
_147
int delta = 0;
_147
try {
_147
delta = Integer.parseInt(request.queryParams("delta"));
_147
} catch (NumberFormatException e) {
_147
System.out.println("Invalid format number for appointment delta");
_147
}
_147
String timeZone = request.queryParams("timeZone");
_147
_147
DateTimeZone zone = DateTimeZone.forID(timeZone);
_147
DateTimeZone zoneUTC = DateTimeZone.UTC;
_147
_147
DateTime dt;
_147
DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");
_147
formatter = formatter.withZone(zone);
_147
dt = formatter.parseDateTime(date);
_147
formatter = formatter.withZone(zoneUTC);
_147
String dateUTC = dt.toString(formatter);
_147
_147
Appointment appointment = new Appointment(name, phoneNumber, delta, dateUTC, timeZone);
_147
service.create(appointment);
_147
_147
scheduleJob(appointment);
_147
_147
response.redirect("/");
_147
}
_147
_147
Map map = new HashMap();
_147
_147
map.put("zones", timeZones());
_147
return new ModelAndView(map, "new.mustache");
_147
};
_147
_147
/**
_147
* Schedules a AppointmentScheduler instance to be created and executed in the specified future
_147
* date coming from the appointment entity
_147
* @param appointment The newly created Appointment that has already been persisted to the DB.
_147
*/
_147
private void scheduleJob(Appointment appointment) {
_147
String appointmentId = appointment.getId().toString();
_147
_147
DateTimeZone zone = DateTimeZone.forID(appointment.getTimeZone());
_147
DateTime dt;
_147
DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");
_147
formatter = formatter.withZone(zone);
_147
dt = formatter.parseDateTime(appointment.getDate());
_147
Date finalDate = dt.minusMinutes(appointment.getDelta()).toDate();
_147
_147
JobDetail job =
_147
newJob(AppointmentScheduler.class).withIdentity("Appointment_J_" + appointmentId)
_147
.usingJobData("appointmentId", appointmentId).build();
_147
_147
Trigger trigger =
_147
newTrigger().withIdentity("Appointment_T_" + appointmentId).startAt(finalDate).build();
_147
_147
try {
_147
scheduler.scheduleJob(job, trigger);
_147
} catch (SchedulerException e) {
_147
System.out.println("Unable to schedule the Job");
_147
}
_147
}
_147
_147
private List<String> timeZones() {
_147
TimeZones tz = new TimeZones();
_147
_147
return tz.getTimeZones();
_147
}
_147
}

Let's look at how we handle this trigger.


Configure the application to send SMS messages

configure-the-application-to-send-sms-messages page anchor

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.

Configure the application to send SMS messages

configure-the-application-to-send-sms-messages-1 page anchor

src/main/java/com/twilio/appointmentreminders/util/AppointmentScheduler.java


_63
package com.twilio.appointmentreminders.util;
_63
_63
import com.twilio.Twilio;
_63
import com.twilio.appointmentreminders.models.Appointment;
_63
import com.twilio.appointmentreminders.models.AppointmentService;
_63
import com.twilio.exception.TwilioException;
_63
import com.twilio.rest.api.v2010.account.Message;
_63
import com.twilio.type.PhoneNumber;
_63
import org.quartz.Job;
_63
import org.quartz.JobDataMap;
_63
import org.quartz.JobExecutionContext;
_63
import org.quartz.JobExecutionException;
_63
import org.slf4j.Logger;
_63
import org.slf4j.LoggerFactory;
_63
_63
import javax.persistence.EntityManagerFactory;
_63
_63
public class AppointmentScheduler implements Job {
_63
_63
private static Logger logger = LoggerFactory.getLogger(AppointmentScheduler.class);
_63
_63
private static AppSetup appSetup = new AppSetup();
_63
_63
public static final String ACCOUNT_SID = appSetup.getAccountSid();
_63
public static final String AUTH_TOKEN = appSetup.getAuthToken();
_63
public static final String TWILIO_NUMBER = appSetup.getTwilioPhoneNumber();
_63
_63
public AppointmentScheduler() {}
_63
_63
public void execute(JobExecutionContext context) throws JobExecutionException {
_63
AppSetup appSetup = new AppSetup();
_63
_63
EntityManagerFactory factory = appSetup.getEntityManagerFactory();
_63
AppointmentService service = new AppointmentService(factory.createEntityManager());
_63
_63
// Initialize the Twilio client
_63
Twilio.init(ACCOUNT_SID, AUTH_TOKEN);
_63
_63
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
_63
_63
String appointmentId = dataMap.getString("appointmentId");
_63
_63
Appointment appointment = service.getAppointment(Long.parseLong(appointmentId, 10));
_63
if (appointment != null) {
_63
String name = appointment.getName();
_63
String phoneNumber = appointment.getPhoneNumber();
_63
String date = appointment.getDate();
_63
String messageBody = "Remember: " + name + ", on " + date + " you have an appointment!";
_63
_63
try {
_63
Message message = Message
_63
.creator(new PhoneNumber(phoneNumber), new PhoneNumber(TWILIO_NUMBER), messageBody)
_63
.create();
_63
System.out.println("Message sent! Message SID: " + message.getSid());
_63
} catch(TwilioException e) {
_63
logger.error("An exception occurred trying to send the message \"{}\" to {}." +
_63
" \nTwilio returned: {} \n", messageBody, phoneNumber, e.getMessage());
_63
}
_63
_63
_63
}
_63
}
_63
}

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


Send an SMS message from a background job

send-an-sms-message-from-a-background-job page anchor

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.

Scheduled task to send SMS messages

scheduled-task-to-send-sms-messages page anchor

src/main/java/com/twilio/appointmentreminders/util/AppointmentScheduler.java


_63
package com.twilio.appointmentreminders.util;
_63
_63
import com.twilio.Twilio;
_63
import com.twilio.appointmentreminders.models.Appointment;
_63
import com.twilio.appointmentreminders.models.AppointmentService;
_63
import com.twilio.exception.TwilioException;
_63
import com.twilio.rest.api.v2010.account.Message;
_63
import com.twilio.type.PhoneNumber;
_63
import org.quartz.Job;
_63
import org.quartz.JobDataMap;
_63
import org.quartz.JobExecutionContext;
_63
import org.quartz.JobExecutionException;
_63
import org.slf4j.Logger;
_63
import org.slf4j.LoggerFactory;
_63
_63
import javax.persistence.EntityManagerFactory;
_63
_63
public class AppointmentScheduler implements Job {
_63
_63
private static Logger logger = LoggerFactory.getLogger(AppointmentScheduler.class);
_63
_63
private static AppSetup appSetup = new AppSetup();
_63
_63
public static final String ACCOUNT_SID = appSetup.getAccountSid();
_63
public static final String AUTH_TOKEN = appSetup.getAuthToken();
_63
public static final String TWILIO_NUMBER = appSetup.getTwilioPhoneNumber();
_63
_63
public AppointmentScheduler() {}
_63
_63
public void execute(JobExecutionContext context) throws JobExecutionException {
_63
AppSetup appSetup = new AppSetup();
_63
_63
EntityManagerFactory factory = appSetup.getEntityManagerFactory();
_63
AppointmentService service = new AppointmentService(factory.createEntityManager());
_63
_63
// Initialize the Twilio client
_63
Twilio.init(ACCOUNT_SID, AUTH_TOKEN);
_63
_63
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
_63
_63
String appointmentId = dataMap.getString("appointmentId");
_63
_63
Appointment appointment = service.getAppointment(Long.parseLong(appointmentId, 10));
_63
if (appointment != null) {
_63
String name = appointment.getName();
_63
String phoneNumber = appointment.getPhoneNumber();
_63
String date = appointment.getDate();
_63
String messageBody = "Remember: " + name + ", on " + date + " you have an appointment!";
_63
_63
try {
_63
Message message = Message
_63
.creator(new PhoneNumber(phoneNumber), new PhoneNumber(TWILIO_NUMBER), messageBody)
_63
.create();
_63
System.out.println("Message sent! Message SID: " + message.getSid());
_63
} catch(TwilioException e) {
_63
logger.error("An exception occurred trying to send the message \"{}\" to {}." +
_63
" \nTwilio returned: {} \n", messageBody, phoneNumber, e.getMessage());
_63
}
_63
_63
_63
}
_63
}
_63
}

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


If you haven't already, be sure to check out the JavaDoc for the Twilio helper library(link takes you to an external page) and our guides for SMS and voice.

Did this help?

Thanks for checking out this tutorial! If you have any feedback to share with us, please reach out on Twitter(link takes you to an external page)... we'd love to hear your thoughts, and know what you're building!


Rate this page: