How to Build an Interactive Voice Response System with Java and Gradle

November 17, 2023
Written by
Twilion
Reviewed by
Twilion

In this article, you will learn how to build an Interactive Voice Response system (IVR) using Twilio's Programmable Voice and Java with Gradle. The example call center demonstrated in this case is for a Party Cookie Dessert hotline.  

Prerequisites

Set up the project directory

Follow the tutorial on how to start a Java Servlets Project as a base for this project. Once you have the codebase described there, proceed.

Create a src/main/java/com/twilio/phonetree/servlet subdirectory, and inside that create the following subfolders:

  • common
  • commuter
  • ivr
  • menu

Add the Twilio dependencies

Open the build.gradle file and add the following line to the dependencies { } block:  

implementation group: 'com.twilio.sdk', name: 'twilio', version: '9.14.1'

Create an interactive voice response app

Delete the HelloWorldServlet.java file and create a WelcomeServlet.java file within the src/main/java/com/twilio/phonetree/servlet/ivr subfolder. Add the following code:

package com.twilio.phonetree.servlet.ivr;

import com.twilio.twiml.TwiMLException;
import com.twilio.twiml.VoiceResponse;
import com.twilio.twiml.voice.Gather;
import com.twilio.twiml.voice.Play;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class WelcomeServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
            throws IOException {
        VoiceResponse response = new VoiceResponse.Builder()
                .gather(new Gather.Builder()
                        .action("/menu/show")
                        .numDigits(1)
                        .say(new Say.Builder("Hello! Please press 1. Press anything else to repeat the message.")
                                .build())
                        .build())
                .build();
    
        servletResponse.setContentType("text/xml");
        try {
            servletResponse.getWriter().write(response.toXml());
        } catch (TwiMLException e) {
            throw new RuntimeException(e);
        }
    }
}

The WelcomeServlet class handles HTTP requests. The doPost takes in two parameters, HttpServletRequest servletRequest and HttpServletResponse servletResponse.

An instance of VoiceResponse is created using the VoiceResponse.Builder class. This object is used to construct a Twilio XML response, typically used for generating voice responses in telephony applications.

Inside the VoiceResponse object, the gather method is used to prompt the caller to enter input. It specifies the action URL to "/menu/show" and the number of digits to collect as 1.

After configuring the VoiceResponse object, the servlet sets the content type of the response to "text/xml".

The try block attempts to write the XML response generated by the VoiceResponse object to the servlet response. If there is a TwiMLException during this process, it is caught, and the servlet throws a RuntimeException. However as the TwiML we've written is hard-coded the TwiMLException shouldn't be thrown.

Create the menu for the interactive voice response app

Create a file named ShowServlet.java inside the menu subdirectory. Paste the following import statements to the file:

package com.twilio.phonetree.servlet.menu;

import com.twilio.twiml.TwiMLException;
import com.twilio.twiml.VoiceResponse;
import com.twilio.twiml.voice.Gather;
import com.twilio.twiml.voice.Hangup;
import com.twilio.twiml.voice.Say;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

Write the class definition for the ShowServlet class right below:

public class ShowServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
            throws IOException {

        String selectedOption = servletRequest.getParameter("Digits");

        VoiceResponse response;
        switch (selectedOption) {
            case "1":
                response = getOptions();
                break;
            default:
                response = com.twilio.phonetree.servlet.common.Redirect.toMainMenu();
        }

        servletResponse.setContentType("text/xml");
        try {
            servletResponse.getWriter().write(response.toXml());
        } catch (TwiMLException e) {
            throw new RuntimeException(e);
        }
    }
}

The selectedOption string retrieves the value of the "Digits" parameter from the incoming POST request. In the context of a Twilio IVR system, the "Digits" parameter typically contains the user's response, which is the digit they pressed on their phone's keypad.

The switch statement checks the value of the user's input and determines the appropriate response based on their choice.

If the user pressed "1," it calls the getOptions() method to generate a TwiML response which requires the app to gather more input from the user.

If the user's choice doesn't match "1" it redirects the user to the main menu using the Redirect.toMainMenu() method.

The ShowServlet class handles incoming Twilio IVR requests, processes the user's input, determines the appropriate TwiML response based on their choice, and sends the response back to Twilio for further interaction with the caller.

Write a response for the interactive voice response to read aloud

Create the getOptions() functions underneath the doPost() function:

    private VoiceResponse getOptions() {

        VoiceResponse response = new VoiceResponse.Builder()
                .gather(new Gather.Builder()
                        .action("/commuter/connect")
                        .numDigits(1)
                        .build())
                .say(new Say.Builder(
                        "Welcome to Party Cookie Dessert of the Day!"
                        + "Press 2 to check the status of your delivery."
                        + "Press 3 to hear the collection of cookies available.")
                        .voice(Say.Voice.POLLY_AMY)
                        .language(Say.Language.EN_GB)
                        .loop(3)
                        .build()
                ).build();

        return response;
    }

The Gather verb is used to collect our caller's input after they press "2". This time, the action verb points to the options route, which will switch our response based on what the caller chooses.

These separate functions will use the Twilio VoiceResponse object to build a response that will be read aloud over the phone. The object is built using TwiML attributes in order to make the response object loop 3 times and speak in the British English female voice, Amy.

To make this IVR more interactive, this app will redirect the user to new phone number lines when they press "2" or "3".

Write the default response redirect

Under the common subdirectory, create a file named Redirect.java and add the following code:

package com.twilio.phonetree.servlet.common;

import com.twilio.twiml.VoiceResponse;
import com.twilio.twiml.voice.Say;

public final class Redirect {

    private Redirect() {    }

    public static VoiceResponse toMainMenu() {

        VoiceResponse response = new VoiceResponse.Builder()
                .say(new Say.Builder("Returning to the main menu")
                        .voice(Say.Voice.POLLY_AMY)
                        .language(Say.Language.EN_GB)
                        .build())
                .redirect(new com.twilio.twiml.voice.Redirect.Builder("/ivr/welcome").build())
                .build();

        return response;
    }
}

If the caller does not press the appropriate numbers, the app will redirect them to this response.

Connect caller response to another phone line  

Under the commuter subdirectory, create a file named ConnectServlet.java and add the following code:

package com.twilio.phonetree.servlet.commuter;

import com.twilio.phonetree.servlet.common.Redirect;
import com.twilio.twiml.voice.Dial;
import com.twilio.twiml.voice.Number;
import com.twilio.twiml.TwiMLException;
import com.twilio.twiml.VoiceResponse;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class ConnectServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
            throws IOException {

        String selectedOption = servletRequest.getParameter("Digits");
        Map<String, String> optionPhones = Map.of("2", "+1929XXXXXXX", "3", "+1726XXXXXXX");

        VoiceResponse twiMLResponse = optionPhones.containsKey(selectedOption)
                ? dial(optionPhones.get(selectedOption))
                : Redirect.toMainMenu();

        servletResponse.setContentType("text/xml");
        try {
            servletResponse.getWriter().write(twiMLResponse.toXml());
        } catch (TwiMLException e) {
            throw new RuntimeException(e);
        }
    }

    private VoiceResponse dial(String phoneNumber) {
        Number number = new Number.Builder(phoneNumber).build();
        return new VoiceResponse.Builder()
                .dial(new Dial.Builder().number(number).build())
                .build();
    }
}

Another function handling HTTP POST requests is made to intake the caller's response. To store the directory of multiple phone numbers in your app, use a HashMap. Please remember to replace the phone number in E.164 format.  

If the selectedOption exists in the optionPhones map it calls the dial method with the associated phone number. If the option doesn't exist, it redirects the call to the main menu.

The dial() function builds a VoiceResponse object using the newly collected phone number from the HashMap directory. The app redirects the caller on the line to the other phone number.

Configure the servlet

XML is used to interpret data so when used with a servlet, it can store and transport data for the web application, which is necessary for dynamic websites.

The servlet is named "welcome" and needs to be mapped to an associated set of URLs. That means every character between the url-pattern tag will be interpreted and matched up when interpreting the URL path on the web browser. For this project, the url-pattern is a forward slash, which is also the default match for mapping if not defined.

Clear the existing file and paste in the following XML for the project:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0"
        metadata-complete="true"
        xmlns="http://java.sun.com/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

   <servlet>
       <servlet-name>welcome</servlet-name>
       <servlet-class>com.twilio.phonetree.servlet.ivr.WelcomeServlet</servlet-class>
   </servlet>
   <servlet-mapping>
       <servlet-name>welcome</servlet-name>
       <url-pattern>/ivr/welcome</url-pattern>
   </servlet-mapping>

    <servlet>
        <servlet-name>show</servlet-name>
        <servlet-class>com.twilio.phonetree.servlet.menu.ShowServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>show</servlet-name>
        <url-pattern>/menu/show</url-pattern>
    </servlet-mapping>

    <servlet>
        <servlet-name>connect</servlet-name>
        <servlet-class>com.twilio.phonetree.servlet.commuter.ConnectServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>connect</servlet-name>
        <url-pattern>/commuter/connect</url-pattern>
    </servlet-mapping>
</web-app>

Compile and run the application

View the full code in this GitHub repository. Run the following command in your terminal to clean and compile the application. If you need to upgrade your version of Gradle, you can run the gradle wrapper beforehand.

./gradlew appRun

As the app is running on http://localhost:8080, expose the application to a public port such as ngrok using the command ngrok http 8080.

Ngrok is a great tool because it allows you to create a temporary public domain that redirects HTTP requests to our local port 8080. If you do not have ngrok installed, follow the instructions on this article to set up ngrok.

ngrok port opening up on the terminal

Your ngrok terminal will now look like the picture above. As you can see, there are URLs in the “Forwarding” section. These are public URLs that ngrok uses to redirect requests into our flask server.

Configure Twilio service

Go to the Twilio Console and navigate to the Phone Numbers section in order to configure the webhook.

active numbers dashboard on the twilio console

Test out the interactive voice response app

Grab your cellular device and dial the phone number to test out the Party Cookie Dessert hotline. It's time to fulfill your customers' sweet tooths by selling desserts to them!

gif of hallmark channel plates of cookies

What's next for interactive voice response applications in Java?

Congratulations on building a small call center for a local dessert shop!

Now that you have an IVR up and running, check out this article on how you can implement best practices for your call center.  

If you are looking for a customizable product to use at scale and build faster, you can build with Flex.

For those looking to build faster with a team, consider building with Twilio Studio which requires no coding experience.

Diane Phan is a developer on the Twilio Voices team. She loves to help programmers tackle difficult challenges that might prevent them from bringing their projects to life. She can be reached at dphan [at] twilio.com or LinkedIn.