Skip to contentSkip to navigationSkip to topbar
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

1
package com.twilio.appointmentreminders;
2
3
import com.twilio.appointmentreminders.controllers.AppointmentController;
4
import com.twilio.appointmentreminders.models.AppointmentService;
5
import com.twilio.appointmentreminders.util.AppSetup;
6
import com.twilio.appointmentreminders.util.LoggingFilter;
7
import org.quartz.Scheduler;
8
import org.quartz.SchedulerException;
9
import org.quartz.impl.StdSchedulerFactory;
10
import spark.Spark;
11
import spark.template.mustache.MustacheTemplateEngine;
12
13
import javax.persistence.EntityManagerFactory;
14
15
import static spark.Spark.*;
16
17
/**
18
* Main application class. The environment is set up here, and all necessary services are run.
19
*/
20
public class Server {
21
public static void main(String[] args) {
22
AppSetup appSetup = new AppSetup();
23
24
/**
25
* Sets the port in which the application will run. Takes the port value from PORT
26
* environment variable, if not set, uses Spark default port 4567.
27
*/
28
port(appSetup.getPortNumber());
29
30
/**
31
* Gets the entity manager based on environment variable DATABASE_URL and injects it into
32
* AppointmentService which handles all DB operations.
33
*/
34
EntityManagerFactory factory = appSetup.getEntityManagerFactory();
35
AppointmentService service = new AppointmentService(factory.createEntityManager());
36
37
/**
38
* Specifies the directory within resources that will be publicly available when the
39
* application is running. Place static web files in this directory (JS, CSS).
40
*/
41
Spark.staticFileLocation("/public");
42
43
/** Creates a new instance of Quartz Scheduler and starts it. */
44
Scheduler scheduler = null;
45
try {
46
scheduler = StdSchedulerFactory.getDefaultScheduler();
47
48
scheduler.start();
49
50
} catch (SchedulerException se) {
51
System.out.println("Unable to start scheduler service");
52
}
53
54
/** Injects AppointmentService and Scheduler into the controller. */
55
AppointmentController controller = new AppointmentController(service, scheduler);
56
57
/**
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 controller
60
* should return the appropriate Route object.
61
*/
62
get("/", controller.index, new MustacheTemplateEngine());
63
get("/new", controller.renderCreatePage, new MustacheTemplateEngine());
64
post("/create", controller.create, new MustacheTemplateEngine());
65
post("/delete", controller.delete);
66
67
afterAfter(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

1
package com.twilio.appointmentreminders.controllers;
2
3
import com.twilio.appointmentreminders.models.Appointment;
4
import com.twilio.appointmentreminders.models.AppointmentService;
5
import com.twilio.appointmentreminders.util.AppointmentScheduler;
6
import com.twilio.appointmentreminders.util.FieldValidator;
7
import com.twilio.appointmentreminders.util.TimeZones;
8
import org.joda.time.DateTime;
9
import org.joda.time.DateTimeZone;
10
import org.joda.time.format.DateTimeFormat;
11
import org.joda.time.format.DateTimeFormatter;
12
import org.quartz.JobDetail;
13
import org.quartz.Scheduler;
14
import org.quartz.SchedulerException;
15
import org.quartz.Trigger;
16
import spark.ModelAndView;
17
import spark.Route;
18
import spark.TemplateViewRoute;
19
20
import java.util.Date;
21
import java.util.HashMap;
22
import java.util.List;
23
import java.util.Map;
24
25
import static org.quartz.JobBuilder.newJob;
26
import static org.quartz.TriggerBuilder.newTrigger;
27
28
/**
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"})
33
public class AppointmentController {
34
private Scheduler scheduler;
35
private AppointmentService service;
36
37
public AppointmentController(AppointmentService service, Scheduler scheduler) {
38
this.service = service;
39
this.scheduler = scheduler;
40
}
41
42
public TemplateViewRoute renderCreatePage = (request, response) -> {
43
Map map = new HashMap();
44
45
map.put("zones", timeZones());
46
return new ModelAndView(map, "new.mustache");
47
};
48
49
public TemplateViewRoute index = (request, response) -> {
50
Map map = new HashMap();
51
52
List<Appointment> appointments = service.findAll();
53
map.put("appointments", appointments);
54
55
return new ModelAndView(map, "index.mustache");
56
};
57
58
public Route delete = (request, response) -> {
59
String id = request.queryParams("id");
60
Long idLong = Long.parseLong(id, 10);
61
62
Appointment appointment = service.getAppointment(idLong);
63
service.delete(appointment);
64
65
response.redirect("/");
66
return response;
67
};
68
69
/**
70
* Controller method that creates a new appointment. Also, schedules an
71
* appointment reminder once the actual appointment is persisted to the database.
72
*/
73
public TemplateViewRoute create = (request, response) -> {
74
FieldValidator validator =
75
new FieldValidator(new String[] {"name", "phoneNumber", "date", "delta", "timeZone"});
76
77
if (validator.valid(request)) {
78
String name = request.queryParams("name");
79
String phoneNumber = request.queryParams("phoneNumber");
80
String date = request.queryParams("date");
81
int delta = 0;
82
try {
83
delta = Integer.parseInt(request.queryParams("delta"));
84
} catch (NumberFormatException e) {
85
System.out.println("Invalid format number for appointment delta");
86
}
87
String timeZone = request.queryParams("timeZone");
88
89
DateTimeZone zone = DateTimeZone.forID(timeZone);
90
DateTimeZone zoneUTC = DateTimeZone.UTC;
91
92
DateTime dt;
93
DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");
94
formatter = formatter.withZone(zone);
95
dt = formatter.parseDateTime(date);
96
formatter = formatter.withZone(zoneUTC);
97
String dateUTC = dt.toString(formatter);
98
99
Appointment appointment = new Appointment(name, phoneNumber, delta, dateUTC, timeZone);
100
service.create(appointment);
101
102
scheduleJob(appointment);
103
104
response.redirect("/");
105
}
106
107
Map map = new HashMap();
108
109
map.put("zones", timeZones());
110
return new ModelAndView(map, "new.mustache");
111
};
112
113
/**
114
* Schedules a AppointmentScheduler instance to be created and executed in the specified future
115
* date coming from the appointment entity
116
* @param appointment The newly created Appointment that has already been persisted to the DB.
117
*/
118
private void scheduleJob(Appointment appointment) {
119
String appointmentId = appointment.getId().toString();
120
121
DateTimeZone zone = DateTimeZone.forID(appointment.getTimeZone());
122
DateTime dt;
123
DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");
124
formatter = formatter.withZone(zone);
125
dt = formatter.parseDateTime(appointment.getDate());
126
Date finalDate = dt.minusMinutes(appointment.getDelta()).toDate();
127
128
JobDetail job =
129
newJob(AppointmentScheduler.class).withIdentity("Appointment_J_" + appointmentId)
130
.usingJobData("appointmentId", appointmentId).build();
131
132
Trigger trigger =
133
newTrigger().withIdentity("Appointment_T_" + appointmentId).startAt(finalDate).build();
134
135
try {
136
scheduler.scheduleJob(job, trigger);
137
} catch (SchedulerException e) {
138
System.out.println("Unable to schedule the Job");
139
}
140
}
141
142
private List<String> timeZones() {
143
TimeZones tz = new TimeZones();
144
145
return tz.getTimeZones();
146
}
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

1
package com.twilio.appointmentreminders.controllers;
2
3
import com.twilio.appointmentreminders.models.Appointment;
4
import com.twilio.appointmentreminders.models.AppointmentService;
5
import com.twilio.appointmentreminders.util.AppointmentScheduler;
6
import com.twilio.appointmentreminders.util.FieldValidator;
7
import com.twilio.appointmentreminders.util.TimeZones;
8
import org.joda.time.DateTime;
9
import org.joda.time.DateTimeZone;
10
import org.joda.time.format.DateTimeFormat;
11
import org.joda.time.format.DateTimeFormatter;
12
import org.quartz.JobDetail;
13
import org.quartz.Scheduler;
14
import org.quartz.SchedulerException;
15
import org.quartz.Trigger;
16
import spark.ModelAndView;
17
import spark.Route;
18
import spark.TemplateViewRoute;
19
20
import java.util.Date;
21
import java.util.HashMap;
22
import java.util.List;
23
import java.util.Map;
24
25
import static org.quartz.JobBuilder.newJob;
26
import static org.quartz.TriggerBuilder.newTrigger;
27
28
/**
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"})
33
public class AppointmentController {
34
private Scheduler scheduler;
35
private AppointmentService service;
36
37
public AppointmentController(AppointmentService service, Scheduler scheduler) {
38
this.service = service;
39
this.scheduler = scheduler;
40
}
41
42
public TemplateViewRoute renderCreatePage = (request, response) -> {
43
Map map = new HashMap();
44
45
map.put("zones", timeZones());
46
return new ModelAndView(map, "new.mustache");
47
};
48
49
public TemplateViewRoute index = (request, response) -> {
50
Map map = new HashMap();
51
52
List<Appointment> appointments = service.findAll();
53
map.put("appointments", appointments);
54
55
return new ModelAndView(map, "index.mustache");
56
};
57
58
public Route delete = (request, response) -> {
59
String id = request.queryParams("id");
60
Long idLong = Long.parseLong(id, 10);
61
62
Appointment appointment = service.getAppointment(idLong);
63
service.delete(appointment);
64
65
response.redirect("/");
66
return response;
67
};
68
69
/**
70
* Controller method that creates a new appointment. Also, schedules an
71
* appointment reminder once the actual appointment is persisted to the database.
72
*/
73
public TemplateViewRoute create = (request, response) -> {
74
FieldValidator validator =
75
new FieldValidator(new String[] {"name", "phoneNumber", "date", "delta", "timeZone"});
76
77
if (validator.valid(request)) {
78
String name = request.queryParams("name");
79
String phoneNumber = request.queryParams("phoneNumber");
80
String date = request.queryParams("date");
81
int delta = 0;
82
try {
83
delta = Integer.parseInt(request.queryParams("delta"));
84
} catch (NumberFormatException e) {
85
System.out.println("Invalid format number for appointment delta");
86
}
87
String timeZone = request.queryParams("timeZone");
88
89
DateTimeZone zone = DateTimeZone.forID(timeZone);
90
DateTimeZone zoneUTC = DateTimeZone.UTC;
91
92
DateTime dt;
93
DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");
94
formatter = formatter.withZone(zone);
95
dt = formatter.parseDateTime(date);
96
formatter = formatter.withZone(zoneUTC);
97
String dateUTC = dt.toString(formatter);
98
99
Appointment appointment = new Appointment(name, phoneNumber, delta, dateUTC, timeZone);
100
service.create(appointment);
101
102
scheduleJob(appointment);
103
104
response.redirect("/");
105
}
106
107
Map map = new HashMap();
108
109
map.put("zones", timeZones());
110
return new ModelAndView(map, "new.mustache");
111
};
112
113
/**
114
* Schedules a AppointmentScheduler instance to be created and executed in the specified future
115
* date coming from the appointment entity
116
* @param appointment The newly created Appointment that has already been persisted to the DB.
117
*/
118
private void scheduleJob(Appointment appointment) {
119
String appointmentId = appointment.getId().toString();
120
121
DateTimeZone zone = DateTimeZone.forID(appointment.getTimeZone());
122
DateTime dt;
123
DateTimeFormatter formatter = DateTimeFormat.forPattern("MM-dd-yyyy hh:mma");
124
formatter = formatter.withZone(zone);
125
dt = formatter.parseDateTime(appointment.getDate());
126
Date finalDate = dt.minusMinutes(appointment.getDelta()).toDate();
127
128
JobDetail job =
129
newJob(AppointmentScheduler.class).withIdentity("Appointment_J_" + appointmentId)
130
.usingJobData("appointmentId", appointmentId).build();
131
132
Trigger trigger =
133
newTrigger().withIdentity("Appointment_T_" + appointmentId).startAt(finalDate).build();
134
135
try {
136
scheduler.scheduleJob(job, trigger);
137
} catch (SchedulerException e) {
138
System.out.println("Unable to schedule the Job");
139
}
140
}
141
142
private List<String> timeZones() {
143
TimeZones tz = new TimeZones();
144
145
return tz.getTimeZones();
146
}
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

1
package com.twilio.appointmentreminders.util;
2
3
import com.twilio.Twilio;
4
import com.twilio.appointmentreminders.models.Appointment;
5
import com.twilio.appointmentreminders.models.AppointmentService;
6
import com.twilio.exception.TwilioException;
7
import com.twilio.rest.api.v2010.account.Message;
8
import com.twilio.type.PhoneNumber;
9
import org.quartz.Job;
10
import org.quartz.JobDataMap;
11
import org.quartz.JobExecutionContext;
12
import org.quartz.JobExecutionException;
13
import org.slf4j.Logger;
14
import org.slf4j.LoggerFactory;
15
16
import javax.persistence.EntityManagerFactory;
17
18
public class AppointmentScheduler implements Job {
19
20
private static Logger logger = LoggerFactory.getLogger(AppointmentScheduler.class);
21
22
private static AppSetup appSetup = new AppSetup();
23
24
public static final String ACCOUNT_SID = appSetup.getAccountSid();
25
public static final String AUTH_TOKEN = appSetup.getAuthToken();
26
public static final String TWILIO_NUMBER = appSetup.getTwilioPhoneNumber();
27
28
public AppointmentScheduler() {}
29
30
public void execute(JobExecutionContext context) throws JobExecutionException {
31
AppSetup appSetup = new AppSetup();
32
33
EntityManagerFactory factory = appSetup.getEntityManagerFactory();
34
AppointmentService service = new AppointmentService(factory.createEntityManager());
35
36
// Initialize the Twilio client
37
Twilio.init(ACCOUNT_SID, AUTH_TOKEN);
38
39
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
40
41
String appointmentId = dataMap.getString("appointmentId");
42
43
Appointment appointment = service.getAppointment(Long.parseLong(appointmentId, 10));
44
if (appointment != null) {
45
String name = appointment.getName();
46
String phoneNumber = appointment.getPhoneNumber();
47
String date = appointment.getDate();
48
String messageBody = "Remember: " + name + ", on " + date + " you have an appointment!";
49
50
try {
51
Message message = Message
52
.creator(new PhoneNumber(phoneNumber), new PhoneNumber(TWILIO_NUMBER), messageBody)
53
.create();
54
System.out.println("Message sent! Message SID: " + message.getSid());
55
} catch(TwilioException e) {
56
logger.error("An exception occurred trying to send the message \"{}\" to {}." +
57
" \nTwilio returned: {} \n", messageBody, phoneNumber, e.getMessage());
58
}
59
60
61
}
62
}
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

1
package com.twilio.appointmentreminders.util;
2
3
import com.twilio.Twilio;
4
import com.twilio.appointmentreminders.models.Appointment;
5
import com.twilio.appointmentreminders.models.AppointmentService;
6
import com.twilio.exception.TwilioException;
7
import com.twilio.rest.api.v2010.account.Message;
8
import com.twilio.type.PhoneNumber;
9
import org.quartz.Job;
10
import org.quartz.JobDataMap;
11
import org.quartz.JobExecutionContext;
12
import org.quartz.JobExecutionException;
13
import org.slf4j.Logger;
14
import org.slf4j.LoggerFactory;
15
16
import javax.persistence.EntityManagerFactory;
17
18
public class AppointmentScheduler implements Job {
19
20
private static Logger logger = LoggerFactory.getLogger(AppointmentScheduler.class);
21
22
private static AppSetup appSetup = new AppSetup();
23
24
public static final String ACCOUNT_SID = appSetup.getAccountSid();
25
public static final String AUTH_TOKEN = appSetup.getAuthToken();
26
public static final String TWILIO_NUMBER = appSetup.getTwilioPhoneNumber();
27
28
public AppointmentScheduler() {}
29
30
public void execute(JobExecutionContext context) throws JobExecutionException {
31
AppSetup appSetup = new AppSetup();
32
33
EntityManagerFactory factory = appSetup.getEntityManagerFactory();
34
AppointmentService service = new AppointmentService(factory.createEntityManager());
35
36
// Initialize the Twilio client
37
Twilio.init(ACCOUNT_SID, AUTH_TOKEN);
38
39
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
40
41
String appointmentId = dataMap.getString("appointmentId");
42
43
Appointment appointment = service.getAppointment(Long.parseLong(appointmentId, 10));
44
if (appointment != null) {
45
String name = appointment.getName();
46
String phoneNumber = appointment.getPhoneNumber();
47
String date = appointment.getDate();
48
String messageBody = "Remember: " + name + ", on " + date + " you have an appointment!";
49
50
try {
51
Message message = Message
52
.creator(new PhoneNumber(phoneNumber), new PhoneNumber(TWILIO_NUMBER), messageBody)
53
.create();
54
System.out.println("Message sent! Message SID: " + message.getSid());
55
} catch(TwilioException e) {
56
logger.error("An exception occurred trying to send the message \"{}\" to {}." +
57
" \nTwilio returned: {} \n", messageBody, phoneNumber, e.getMessage());
58
}
59
60
61
}
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(link takes you to an external page) and our guides for SMS and voice.

Need some help?

Terms of service

Copyright © 2024 Twilio Inc.