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 that demonstrates how to send appointment reminders to your customers with Twilio SMS.
Check out this application on GitHub 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.
Let's get started! Click the button below to move on to the next step of the tutorial.
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.
src/main/java/com/twilio/appointmentreminders/Server.java
1package com.twilio.appointmentreminders;23import com.twilio.appointmentreminders.controllers.AppointmentController;4import com.twilio.appointmentreminders.models.AppointmentService;5import com.twilio.appointmentreminders.util.AppSetup;6import com.twilio.appointmentreminders.util.LoggingFilter;7import org.quartz.Scheduler;8import org.quartz.SchedulerException;9import org.quartz.impl.StdSchedulerFactory;10import spark.Spark;11import spark.template.mustache.MustacheTemplateEngine;1213import javax.persistence.EntityManagerFactory;1415import static spark.Spark.*;1617/**18* Main application class. The environment is set up here, and all necessary services are run.19*/20public class Server {21public static void main(String[] args) {22AppSetup appSetup = new AppSetup();2324/**25* Sets the port in which the application will run. Takes the port value from PORT26* environment variable, if not set, uses Spark default port 4567.27*/28port(appSetup.getPortNumber());2930/**31* Gets the entity manager based on environment variable DATABASE_URL and injects it into32* AppointmentService which handles all DB operations.33*/34EntityManagerFactory factory = appSetup.getEntityManagerFactory();35AppointmentService service = new AppointmentService(factory.createEntityManager());3637/**38* Specifies the directory within resources that will be publicly available when the39* application is running. Place static web files in this directory (JS, CSS).40*/41Spark.staticFileLocation("/public");4243/** Creates a new instance of Quartz Scheduler and starts it. */44Scheduler scheduler = null;45try {46scheduler = StdSchedulerFactory.getDefaultScheduler();4748scheduler.start();4950} catch (SchedulerException se) {51System.out.println("Unable to start scheduler service");52}5354/** Injects AppointmentService and Scheduler into the controller. */55AppointmentController controller = new AppointmentController(service, scheduler);5657/**58* Defines all url paths for the application and assigns a controller method for each.59* If the route renders a page, the templating engine must be specified, and the controller60* should return the appropriate Route object.61*/62get("/", controller.index, new MustacheTemplateEngine());63get("/new", controller.renderCreatePage, new MustacheTemplateEngine());64post("/create", controller.create, new MustacheTemplateEngine());65post("/delete", controller.delete);6667afterAfter(new LoggingFilter());68}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
1package com.twilio.appointmentreminders.controllers;23import com.twilio.appointmentreminders.models.Appointment;4import com.twilio.appointmentreminders.models.AppointmentService;5import com.twilio.appointmentreminders.util.AppointmentScheduler;6import com.twilio.appointmentreminders.util.FieldValidator;7import com.twilio.appointmentreminders.util.TimeZones;8import org.joda.time.DateTime;9import org.joda.time.DateTimeZone;10import org.joda.time.format.DateTimeFormat;11import org.joda.time.format.DateTimeFormatter;12import org.quartz.JobDetail;13import org.quartz.Scheduler;14import org.quartz.SchedulerException;15import org.quartz.Trigger;16import spark.ModelAndView;17import spark.Route;18import spark.TemplateViewRoute;1920import java.util.Date;21import java.util.HashMap;22import java.util.List;23import java.util.Map;2425import static org.quartz.JobBuilder.newJob;26import static org.quartz.TriggerBuilder.newTrigger;2728/**29* Appointment controller class. Holds all the methods that handle the applications requests.30* This methods are mapped to a specific URL on the main Server file of the application.31*/32@SuppressWarnings({"rawtypes", "unchecked"})33public class AppointmentController {34private Scheduler scheduler;35private AppointmentService service;3637public AppointmentController(AppointmentService service, Scheduler scheduler) {38this.service = service;39this.scheduler = scheduler;40}4142public TemplateViewRoute renderCreatePage = (request, response) -> {43Map map = new HashMap();4445map.put("zones", timeZones());46return new ModelAndView(map, "new.mustache");47};4849public TemplateViewRoute index = (request, response) -> {50Map map = new HashMap();5152List<Appointment> appointments = service.findAll();53map.put("appointments", appointments);5455return new ModelAndView(map, "index.mustache");56};5758public Route delete = (request, response) -> {59String id = request.queryParams("id");60Long idLong = Long.parseLong(id, 10);6162Appointment appointment = service.getAppointment(idLong);63service.delete(appointment);6465response.redirect("/");66return response;67};6869/**70* Controller method that creates a new appointment. Also, schedules an71* appointment reminder once the actual appointment is persisted to the database.72*/73public TemplateViewRoute create = (request, response) -> {74FieldValidator validator =75new FieldValidator(new String[] {"name", "phoneNumber", "date", "delta", "timeZone"});7677if (validator.valid(request)) {78String name = request.queryParams("name");79String phoneNumber = request.queryParams("phoneNumber");80String date = request.queryParams("date");81int delta = 0;82try {83delta = Integer.parseInt(request.queryParams("delta"));84} catch (NumberFormatException e) {85System.out.println("Invalid format number for appointment delta");86}87String timeZone = request.queryParams("timeZone");8889DateTimeZone zone = DateTimeZone.forID(timeZone);90DateTimeZone zoneUTC = DateTimeZone.UTC;9192DateTime dt;93DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");94formatter = formatter.withZone(zone);95dt = formatter.parseDateTime(date);96formatter = formatter.withZone(zoneUTC);97String dateUTC = dt.toString(formatter);9899Appointment appointment = new Appointment(name, phoneNumber, delta, dateUTC, timeZone);100service.create(appointment);101102scheduleJob(appointment);103104response.redirect("/");105}106107Map map = new HashMap();108109map.put("zones", timeZones());110return new ModelAndView(map, "new.mustache");111};112113/**114* Schedules a AppointmentScheduler instance to be created and executed in the specified future115* date coming from the appointment entity116* @param appointment The newly created Appointment that has already been persisted to the DB.117*/118private void scheduleJob(Appointment appointment) {119String appointmentId = appointment.getId().toString();120121DateTimeZone zone = DateTimeZone.forID(appointment.getTimeZone());122DateTime dt;123DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");124formatter = formatter.withZone(zone);125dt = formatter.parseDateTime(appointment.getDate());126Date finalDate = dt.minusMinutes(appointment.getDelta()).toDate();127128JobDetail job =129newJob(AppointmentScheduler.class).withIdentity("Appointment_J_" + appointmentId)130.usingJobData("appointmentId", appointmentId).build();131132Trigger trigger =133newTrigger().withIdentity("Appointment_T_" + appointmentId).startAt(finalDate).build();134135try {136scheduler.scheduleJob(job, trigger);137} catch (SchedulerException e) {138System.out.println("Unable to schedule the Job");139}140}141142private List<String> timeZones() {143TimeZones tz = new TimeZones();144145return tz.getTimeZones();146}147}
We will dig further into that function next.
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
1package com.twilio.appointmentreminders.controllers;23import com.twilio.appointmentreminders.models.Appointment;4import com.twilio.appointmentreminders.models.AppointmentService;5import com.twilio.appointmentreminders.util.AppointmentScheduler;6import com.twilio.appointmentreminders.util.FieldValidator;7import com.twilio.appointmentreminders.util.TimeZones;8import org.joda.time.DateTime;9import org.joda.time.DateTimeZone;10import org.joda.time.format.DateTimeFormat;11import org.joda.time.format.DateTimeFormatter;12import org.quartz.JobDetail;13import org.quartz.Scheduler;14import org.quartz.SchedulerException;15import org.quartz.Trigger;16import spark.ModelAndView;17import spark.Route;18import spark.TemplateViewRoute;1920import java.util.Date;21import java.util.HashMap;22import java.util.List;23import java.util.Map;2425import static org.quartz.JobBuilder.newJob;26import static org.quartz.TriggerBuilder.newTrigger;2728/**29* Appointment controller class. Holds all the methods that handle the applications requests.30* This methods are mapped to a specific URL on the main Server file of the application.31*/32@SuppressWarnings({"rawtypes", "unchecked"})33public class AppointmentController {34private Scheduler scheduler;35private AppointmentService service;3637public AppointmentController(AppointmentService service, Scheduler scheduler) {38this.service = service;39this.scheduler = scheduler;40}4142public TemplateViewRoute renderCreatePage = (request, response) -> {43Map map = new HashMap();4445map.put("zones", timeZones());46return new ModelAndView(map, "new.mustache");47};4849public TemplateViewRoute index = (request, response) -> {50Map map = new HashMap();5152List<Appointment> appointments = service.findAll();53map.put("appointments", appointments);5455return new ModelAndView(map, "index.mustache");56};5758public Route delete = (request, response) -> {59String id = request.queryParams("id");60Long idLong = Long.parseLong(id, 10);6162Appointment appointment = service.getAppointment(idLong);63service.delete(appointment);6465response.redirect("/");66return response;67};6869/**70* Controller method that creates a new appointment. Also, schedules an71* appointment reminder once the actual appointment is persisted to the database.72*/73public TemplateViewRoute create = (request, response) -> {74FieldValidator validator =75new FieldValidator(new String[] {"name", "phoneNumber", "date", "delta", "timeZone"});7677if (validator.valid(request)) {78String name = request.queryParams("name");79String phoneNumber = request.queryParams("phoneNumber");80String date = request.queryParams("date");81int delta = 0;82try {83delta = Integer.parseInt(request.queryParams("delta"));84} catch (NumberFormatException e) {85System.out.println("Invalid format number for appointment delta");86}87String timeZone = request.queryParams("timeZone");8889DateTimeZone zone = DateTimeZone.forID(timeZone);90DateTimeZone zoneUTC = DateTimeZone.UTC;9192DateTime dt;93DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");94formatter = formatter.withZone(zone);95dt = formatter.parseDateTime(date);96formatter = formatter.withZone(zoneUTC);97String dateUTC = dt.toString(formatter);9899Appointment appointment = new Appointment(name, phoneNumber, delta, dateUTC, timeZone);100service.create(appointment);101102scheduleJob(appointment);103104response.redirect("/");105}106107Map map = new HashMap();108109map.put("zones", timeZones());110return new ModelAndView(map, "new.mustache");111};112113/**114* Schedules a AppointmentScheduler instance to be created and executed in the specified future115* date coming from the appointment entity116* @param appointment The newly created Appointment that has already been persisted to the DB.117*/118private void scheduleJob(Appointment appointment) {119String appointmentId = appointment.getId().toString();120121DateTimeZone zone = DateTimeZone.forID(appointment.getTimeZone());122DateTime dt;123DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");124formatter = formatter.withZone(zone);125dt = formatter.parseDateTime(appointment.getDate());126Date finalDate = dt.minusMinutes(appointment.getDelta()).toDate();127128JobDetail job =129newJob(AppointmentScheduler.class).withIdentity("Appointment_J_" + appointmentId)130.usingJobData("appointmentId", appointmentId).build();131132Trigger trigger =133newTrigger().withIdentity("Appointment_T_" + appointmentId).startAt(finalDate).build();134135try {136scheduler.scheduleJob(job, trigger);137} catch (SchedulerException e) {138System.out.println("Unable to schedule the Job");139}140}141142private List<String> timeZones() {143TimeZones tz = new TimeZones();144145return tz.getTimeZones();146}147}
Let's look at how we handle this trigger.
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.
src/main/java/com/twilio/appointmentreminders/util/AppointmentScheduler.java
1package com.twilio.appointmentreminders.util;23import com.twilio.Twilio;4import com.twilio.appointmentreminders.models.Appointment;5import com.twilio.appointmentreminders.models.AppointmentService;6import com.twilio.exception.TwilioException;7import com.twilio.rest.api.v2010.account.Message;8import com.twilio.type.PhoneNumber;9import org.quartz.Job;10import org.quartz.JobDataMap;11import org.quartz.JobExecutionContext;12import org.quartz.JobExecutionException;13import org.slf4j.Logger;14import org.slf4j.LoggerFactory;1516import javax.persistence.EntityManagerFactory;1718public class AppointmentScheduler implements Job {1920private static Logger logger = LoggerFactory.getLogger(AppointmentScheduler.class);2122private static AppSetup appSetup = new AppSetup();2324public static final String ACCOUNT_SID = appSetup.getAccountSid();25public static final String AUTH_TOKEN = appSetup.getAuthToken();26public static final String TWILIO_NUMBER = appSetup.getTwilioPhoneNumber();2728public AppointmentScheduler() {}2930public void execute(JobExecutionContext context) throws JobExecutionException {31AppSetup appSetup = new AppSetup();3233EntityManagerFactory factory = appSetup.getEntityManagerFactory();34AppointmentService service = new AppointmentService(factory.createEntityManager());3536// Initialize the Twilio client37Twilio.init(ACCOUNT_SID, AUTH_TOKEN);3839JobDataMap dataMap = context.getJobDetail().getJobDataMap();4041String appointmentId = dataMap.getString("appointmentId");4243Appointment appointment = service.getAppointment(Long.parseLong(appointmentId, 10));44if (appointment != null) {45String name = appointment.getName();46String phoneNumber = appointment.getPhoneNumber();47String date = appointment.getDate();48String messageBody = "Remember: " + name + ", on " + date + " you have an appointment!";4950try {51Message message = Message52.creator(new PhoneNumber(phoneNumber), new PhoneNumber(TWILIO_NUMBER), messageBody)53.create();54System.out.println("Message sent! Message SID: " + message.getSid());55} catch(TwilioException e) {56logger.error("An exception occurred trying to send the message \"{}\" to {}." +57" \nTwilio returned: {} \n", messageBody, phoneNumber, e.getMessage());58}596061}62}63}
Next let's look at how the SMS is sent.
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.
src/main/java/com/twilio/appointmentreminders/util/AppointmentScheduler.java
1package com.twilio.appointmentreminders.util;23import com.twilio.Twilio;4import com.twilio.appointmentreminders.models.Appointment;5import com.twilio.appointmentreminders.models.AppointmentService;6import com.twilio.exception.TwilioException;7import com.twilio.rest.api.v2010.account.Message;8import com.twilio.type.PhoneNumber;9import org.quartz.Job;10import org.quartz.JobDataMap;11import org.quartz.JobExecutionContext;12import org.quartz.JobExecutionException;13import org.slf4j.Logger;14import org.slf4j.LoggerFactory;1516import javax.persistence.EntityManagerFactory;1718public class AppointmentScheduler implements Job {1920private static Logger logger = LoggerFactory.getLogger(AppointmentScheduler.class);2122private static AppSetup appSetup = new AppSetup();2324public static final String ACCOUNT_SID = appSetup.getAccountSid();25public static final String AUTH_TOKEN = appSetup.getAuthToken();26public static final String TWILIO_NUMBER = appSetup.getTwilioPhoneNumber();2728public AppointmentScheduler() {}2930public void execute(JobExecutionContext context) throws JobExecutionException {31AppSetup appSetup = new AppSetup();3233EntityManagerFactory factory = appSetup.getEntityManagerFactory();34AppointmentService service = new AppointmentService(factory.createEntityManager());3536// Initialize the Twilio client37Twilio.init(ACCOUNT_SID, AUTH_TOKEN);3839JobDataMap dataMap = context.getJobDetail().getJobDataMap();4041String appointmentId = dataMap.getString("appointmentId");4243Appointment appointment = service.getAppointment(Long.parseLong(appointmentId, 10));44if (appointment != null) {45String name = appointment.getName();46String phoneNumber = appointment.getPhoneNumber();47String date = appointment.getDate();48String messageBody = "Remember: " + name + ", on " + date + " you have an appointment!";4950try {51Message message = Message52.creator(new PhoneNumber(phoneNumber), new PhoneNumber(TWILIO_NUMBER), messageBody)53.create();54System.out.println("Message sent! Message SID: " + message.getSid());55} catch(TwilioException e) {56logger.error("An exception occurred trying to send the message \"{}\" to {}." +57" \nTwilio returned: {} \n", messageBody, phoneNumber, e.getMessage());58}596061}62}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 and our guides for SMS and voice.