Home > Articles > Web Development

Developing Scalable Web Applications with Play

You can build upon the foundational understanding of the Play Framework you built in Part 1 of this series, Introduction to Play 2 with Java, to build a real-world Play web application. We explore Play’s support for domain-driven development and use Play’s Scala Templates to build web pages. Now you can create and build a functional Play web application.
Like this article? We recommend

The previous article in this series, Introduction to Play 2 for Java, introduced the Play 2 Framework, demonstrated how to set up a Play environment, and presented a simple Hello, World application.  Here I expand upon that foundation to show you how to build a typical web application using Play’s Scala Templates and how Play implements a domain-driven design. We build a simple Widget management application that presents a list of widgets and allows the user to add a new widget, update an existing widget, and delete a widget.

Domain-Driven Design

If you come from a Java EE or from a Spring background, then you’re probably familiar with separating persistence logic from domain objects: A domain object contains the object’s properties, and a separate repository object contains logic for persisting domain objects to and from external storage, such as a database. Play implements things a little differently: The domain object not only encapsulates the object’s properties, but it also defines persistence methods. Play does not force you to implement your applications like this, but if you want your Play application to be consistent with other Play applications, then it is considered a best practice.

Domain objects should be stored, by default, in a “models” package and follow the structure shown in listing 1.

Listing 1. Structure of a Play domain object

public class Widget {
  // Fields are public and getters and setters will be automatically generated and use, 
  //e.g. id = "a" would be translated to setId("a") by Play
  public String id;  
  public String name;

  // Finders are part of the domain object, but are static
  public static Widget findById( String id ) { ...  }
  public static List<Widget> findAll() {...}

  // Update is part of the domain object and updates this instance with a command like JPA.em().merge(this);
  public void update() { ... }

  // Save is part of the domain object and inserts a new instance with a command like JPA.em().persist(this);
  public void save() { .. }

  // Delete is part of the domain object and deletes the current instance with a 
  // command like JPA.em().remove(this);
  public void delete() { ... }
}

Play domain objects define all their fields as public and then Play implicitly wraps assignment calls with set() methods. It makes development a little easier, but it requires a little faith on your part to trust Play will protect your fields for you.

All query methods are defined in the domain object, but they are static, so they do not require an object instance to execute them. You’ll typically see a findAll() that returns a list, findByXX() that finds a single object, and so forth.

Finally, methods that operate on the object instance are defined as instance methods: save() inserts a new object into the data store, update() updates an existing object in the data store, and delete() removes an object from the data store.

For our example we’ll bypass using a database and instead store the “cache” of data objects in memory as a static array in the Widget class itself. Note that this is not a recommended approach, but considering we’re learning Play and not JPA/Hibernate, this simplifies our implementation. Listing 2 shows the source code for the Widget class.

Listing 2. Widget.java

package models;

import java.util.List;
import java.util.ArrayList;

public class Widget
{
    public String id;
    public String name;
    public String description;

    public Widget() {
    }

    public Widget(String id, String name, String description) {
        this.id = id;
        this.name = name;
        this.description = description;
    }

    public static Widget findById( String id ) {
        for( Widget widget : widgets ) {
            if( widget.id.equals( id ) ) {
                return widget;
            }
        }
        return null;
    }

    public static List<Widget> findAll() {
        return widgets;
    }

    public void save() {
        widgets.add( this );
    }

    public void update() {
        for( Widget widget : widgets ) {
            if( widget.id.equals( id ) ) {
                widget.name = name;
                widget.description = description;
            }
        }
    }

    public void delete() {
        widgets.remove( this );
    }

    private static List<Widget> widgets;

    static {
        widgets = new ArrayList<Widget>();
        widgets.add( new Widget( "1", "Widget 1", "My Widget 1" ) );
        widgets.add( new Widget( "2", "Widget 2", "My Widget 2" ) );
        widgets.add( new Widget( "3", "Widget 3", "My Widget 3" ) );
        widgets.add( new Widget( "4", "Widget 4", "My Widget 4" ) );
    }
}

The Widget class defines three instance variables: id, name, and description. Because a Widget is a generic term for a “thing,” its attributes are not important, so these will suffice. The bottom of Listing 2 defines a static ArrayList named widgets and a static code block that initializes it with some sample values. The other methods operate on this static object: findAll() returns the widgets list; findById() searches through all widgets for a matching one; save() adds the object to the widgets list; update() finds the widget with the matching ID and updates its fields; and delete() removes the object from the widgets list. Replace these method implementations with your database or NoSQL persistence methods and your domain object will be complete.

Scala Templates

With our domain object defined, let’s turn our attention to the application workflow. We’re going to create controller actions for the following routes:

  • GET /widgets: Returns a list of widgets
  • POST /widgets: Creates a new widget
  • GET /widget/id: Returns a specific widget
  • POST /widget-update/id: Updates a widget
  • DELETE /widget/id: Deletes a specific widget

Let’s start by reviewing the GET /widgets URI, which returns a list of widgets. We need to add a new entry in the routes file:

GET     /widgets                    controllers.WidgetController.list()

This route maps a call to GET /widgets to the WidgetController’s list() action method. The list() method is simple: It retrieves the list of Widgets by calling Widget.findAll() (that we created in the previous section) and sends that to our list template:

public static Result list() {
    return ok( list.render( Widget.findAll()) );
}

The list template (list.scala.html) accepts a list of Widgets and renders them in HTML, which is shown in Listing 3.

Listing 3. list.scala.html

@( widgets: List[Widget] )

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Widgets</title>
</head>
<body>
    <h2>Widgets</h2>
    <table>
        <thead><th>ID</th><th>Name</th><th>Description</th></thead>
        <tbody>
            @for( widget <- widgets ) {
            <tr>
                <td><a href="@routes.WidgetController.details(widget.id)">  @widget.id </a></td>
                <td>@widget.name</td>
                <td>@widget.description</td>
            </tr>
            }
        </tbody>
    </table>
</body>
</html>

The first line in Listing 3 tells Play that the template requires a List of Widgets and names it “widgets” on the page. The page builds a table that shows the Widget’s id, name, and description fields. The Scala notation for iterating over a collection is the @for command. The iteration logic is backward from Java’s notation but it reads: For all widget instances in the widgets collection do this.

@for( widget <- widgets )

Now we have a widget variable in the body of the for loop that we can access by prefixing it with an at symbol (@):

  • @widget.id: Returns the widget’s ID
  • @widget.name: Returns the widget’s name
  • @widget.description: Returns the widget’s description

We also added a link to the id that invokes the details action, which we review next. Note that rather than using the URI of the details action, we reference it through its routes value:

@routes.WidgetController.details(widget.id)

The details action accepts the ID of the Widget to display, loads the Widget from the Widget.findById() method, fills in a Form, and renders that form using the details template:

    private static Form<Widget> widgetForm = Form.form( Widget.class );

    public static Result details( String id ) {
        Widget widget = Widget.findById( id );
        if( widget == null ) {
            return notFound( "No widget found with id: " + id );
        }

        // Create a filled form with the contents of the widget
        Form<Widget> filledForm = widgetForm.fill( widget );

        // Return an HTTP 200 OK with the form rendered by the details template
        return ok( details.render( filledForm ) );
    }

The WidgetController class defines a Play form (Form<Widget>) object, which will be used to both render an existing Widget object as well as serve as a mechanism for the user to POST a Widget to the controller to create a new Widget object. (The full WidgetController is shown below.) The details() method queries for a Widget and, if it is found, then it fills the form by invoking the widget form’s fill() method, renders the form using the details template, and returns an HTTP OK response. If the Widget is not found, then it returns an HTTP 404 Not Found response by invoking the notFound() method.

The routes file maps the following URI to the details action:

GET     /widget/:id                 controllers.WidgetController.details(id : String)

We use the HTTP GET verb for the URI /widget/:id and map that to the details() method, which accepts the ID as a String.

Listing 4 shows the source code for the details template.

Listing 4. details.scala.html

@(widgetForm: Form[Widget])
@import helper._

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
<body>

    <h2>Widget</h2>

    @helper.form( action = routes.WidgetController.update() ) {
        @helper.inputText( widgetForm( "id" ), '_label -> "ID" )
        @helper.inputText( widgetForm( "name" ), '_label -> "Name" )
        @helper.inputText( widgetForm( "description" ), '_label -> "Description" )
        <input type="submit" value="Update">
    }

</body>
</html>

The details template accepts a Form[Widget] and assigns it to the widgetForm variable. It uses a helper class to manage the form, so it imports helper._ (Scala notation of import helper.*). The helper class creates a form using its form() method and passes the action URI to which to POST the form body to, which in this case is the routes.WidgetController.update() action. The body of the form is wrapped in braces and inside the form you can see additional helper methods for creating and populating form elements. The inputText() method creates a form text field; it accepts the form field as its first parameter and the label as its second parameter. If the form has values in it, as it does in this case, then the value of the field is set in the form. Finally, the submit button is used to submit the form to update() action method. The resultant HTML for viewing “Widget 1” is the following:

<!DOCTYPE html>
<html>
<head lang="en">
    <meta charset="UTF-8">
    <title></title>
</head>
<body>

    <h2>Widget</h2>

<form action="/widget-update" method="POST" >

  <dl class=" " id="id_field">
    <dt><label for="id">ID</label></dt>
    <dd>
      <input type="text" id="id" name="id" value="1" >
    </dd>
    <dd class="info">Required</dd>
  </dl>

  <dl class=" " id="name_field">
    <dt><label for="name">Name</label></dt>
    <dd>
      <input type="text" id="name" name="name" value="Widget 1" >
    </dd>
    <dd class="info">Required</dd>
  </dl>

  <dl class=" " id="description_field">
    <dt><label for="description">Description</label></dt>
    <dd>
      <input type="text" id="description" name="description" value="My Widget 1" >
    </dd>
  </dl>
  <input type="submit" value="Update">
</form>

</body>
</html>

The next thing that we might want to do is add a new Widget to our list. Let’s augment our homepage (the list template) to add a form to the bottom of the page that allows our users to add new Widgets. As we did in the previous section, we need to create a Form object and pass it to the template, but this time the Form object should not be populated:

    private static Form<Widget> widgetForm = Form.form( Widget.class );

    public static Result list() {
        return ok( list.render( Widget.findAll(), widgetForm ) );
    }

We already created the widgetForm in the previous example, but it is shown here again for completeness. When we render the list template, we’re going to pass the list of all Widgets as well as the unpopulated widgetForm. Listing 5 adds the new form to the list template.

Listing 5. list.scala.html

@( widgets: List[Widget], widgetForm: Form[Widget] )
@import helper._

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Widgets</title>
</head>
<body>

    <h2>Widgets</h2>

    <table>
        <thead><th>ID</th><th>Name</th><th>Description</th></thead>
        <tbody>
            @for( widget <- widgets ) {
            <tr>
                <td><a href="@routes.WidgetController.details(widget.id)">  @widget.id </a></td>
                <td>@widget.name</td>
                <td>@widget.description</td>
            </tr>
            }
        </tbody>
    </table>

    <h2>Add New Widget</h2>

    @helper.form( action = routes.WidgetController.save() ) {

        @helper.inputText( widgetForm( "id" ), '_label -> "ID" )
        @helper.inputText( widgetForm( "name" ), '_label -> "Name" )
        @helper.inputText( widgetForm( "description" ), '_label -> "Description" )
        <input type="submit" value="Add">

    }

</body>
</html>

Listing 5 updates the expected parameters to include both a List of Widgets as well as a Widget Form and then it imports the form helper classes. The bottom of Listing 5 creates the form using the helper.form() method, with the action directed to the routes.WidgetController.save() action. This form looks a whole lot like the form in Listing 4.

To complete this example, we need to add one more feature: a delete link. Deleting web resources is accomplished by using the DELETE HTTP method, which we cannot simply invoke by adding a link or a form. Instead we need to make the call using JavaScript. Let’s add a new delete method to our routes file:

DELETE  /widget/:id                 controllers.WidgetController.delete(id : String)

When the DELETE HTTP verb is passed to the /widget/id URI, then the WidgetController’s delete() action will be invoked:

    public static Result delete( String id ) {
        Widget widget = Widget.findById( id );
        if( widget == null ) {
            return notFound( "No widget found with id: " + id );
        }
        widget.delete();
        return redirect( routes.WidgetController.list() );
    }

The delete() method finds the Widget with the specified ID and, if it is found, it deletes it by invoking the Widget’s delete() method; if the Widget is not found, then it returns a notFound() response (404) with an error message. Finally, the method redirects the caller to the WidgetController’s list() action.

Listing 6 shows the final version of our list.scala.html template that includes the new delete button.

Listing 6. list.scala.html (final)

@( widgets: List[Widget], widgetForm: Form[Widget] )
@import helper._

@main( "Widgets" ) {

    <h2>Widgets</h2>

    <script>
        function del(url) {
            $.ajax({
                url: url,
                type: 'DELETE',
                success: function(results) {
                    // Refresh the page
                    location.reload();
                }
            });
        }
    </script>

    <table>
        <thead><th>ID</th><th>Name</th><th>Description</th><th>Delete</th></thead>
        <tbody>
            @for( widget <- widgets ) {
            <tr>
                <td><a href="@routes.WidgetController.details(widget.id)">  @widget.id </a></td>
                <td>@widget.name</td>
                <td>@widget.description</td>
                <td><a href="#" onclick="javascript:del('@routes.WidgetController.delete(widget.id)')">Delete</a></td>
            </tr>
            }
        </tbody>
    </table>

    <h2>Add New Widget</h2>

    @helper.form( action = routes.WidgetController.save() ) {

        @helper.inputText( widgetForm( "id" ), '_label -> "ID" )
        @helper.inputText( widgetForm( "name" ), '_label -> "Name" )
        @helper.inputText( widgetForm( "description" ), '_label -> "Description" )
        <input type="submit" value="Add">
    }
}

Listing 6 may look a little strange compared to our previous listings, primarily because it is lacking the HTML headers and footers and the body is now wrapped in a main() method. When you created your project, Play created a main.scala.html file for you that accepts a title String and content as Html. Listing 7 shows the contents of the main.scala.html template.

Listing 7. main.scala.html

@(title: String)(content: Html)

<!DOCTYPE html>

<html>
    <head>
        <title>@title</title>
        <link rel="stylesheet" media="screen" href="@routes.Assets.at("stylesheets/main.css")">
        <link rel="shortcut icon" type="image/png" href="@routes.Assets.at("images/favicon.png")">
        <script src="@routes.Assets.at("javascripts/jquery-1.9.0.min.js")" type="text/javascript"></script>
    </head>
    <body>
        @content
    </body>
</html>

The title is pasted in as the <head> <title> element and the content is pasted inside the <body> of the document. Furthermore, the header imports JQuery for us, which you’ll find in the public/javascripts folder – we need JQuery to simplify our Ajax delete call. If you want to maintain a consistent look-and-feel to your pages, you should add styling information, menus, and other common resources to the main template and then wrap your other templates in a call to main().

Listing 6 adds a delete link to each Widget with the following line:

<td><a href="#" onclick="javascript:del('@routes.WidgetController.delete(widget.id)')">Delete</a></td>

We invoke the del() JavaScript method, which was shown in Listing 6, passing it the route to the WidgetController’s delete() action and the id of the Widget to delete. The del() method uses JQuery’s ajax() method to invoke the specified URL with the specified type (HTTP DELETE verb) and a success method to invoke upon completion (reload the page).

When you’ve completed your code, launch your application using the play command from your application’s home directory and execute run from the play shell. Open a browser to the following URL and take it for a spin:

http://localhost:9000/widgets

You can download the source code for this article here.

Summary

The Play Framework is not a traditional Java web framework and actually requires us to think about developing web applications differently. It runs in its own JVM, not inside a Servlet container, and it supports instant redeployment of applications without a build cycle. When building Play applications you are required to think in terms of HTTP and not in terms of Java.

The “Introduction to Play 2 for Java” article presented an overview of Play, showed how to set up a Play environment, and then built a Hello, World application. Here we built a more complicated Play application that manages CRUD (create, read, update, and delete) operations for a Widget, which uses Play’s domain-driven paradigm, and that better utilizes Play’s Scala templates.

The final article, Integrating Play with Akka, integrates Play with Akka to realize the true power of asynchronous messaging and to show how to suspend a request while waiting for a response so that we can support more simultaneous requests than a traditional Java web application.

InformIT Promotional Mailings & Special Offers

I would like to receive exclusive offers and hear about products from InformIT and its family of brands. I can unsubscribe at any time.

Overview


Pearson Education, Inc., 221 River Street, Hoboken, New Jersey 07030, (Pearson) presents this site to provide information about products and services that can be purchased through this site.

This privacy notice provides an overview of our commitment to privacy and describes how we collect, protect, use and share personal information collected through this site. Please note that other Pearson websites and online products and services have their own separate privacy policies.

Collection and Use of Information


To conduct business and deliver products and services, Pearson collects and uses personal information in several ways in connection with this site, including:

Questions and Inquiries

For inquiries and questions, we collect the inquiry or question, together with name, contact details (email address, phone number and mailing address) and any other additional information voluntarily submitted to us through a Contact Us form or an email. We use this information to address the inquiry and respond to the question.

Online Store

For orders and purchases placed through our online store on this site, we collect order details, name, institution name and address (if applicable), email address, phone number, shipping and billing addresses, credit/debit card information, shipping options and any instructions. We use this information to complete transactions, fulfill orders, communicate with individuals placing orders or visiting the online store, and for related purposes.

Surveys

Pearson may offer opportunities to provide feedback or participate in surveys, including surveys evaluating Pearson products, services or sites. Participation is voluntary. Pearson collects information requested in the survey questions and uses the information to evaluate, support, maintain and improve products, services or sites, develop new products and services, conduct educational research and for other purposes specified in the survey.

Contests and Drawings

Occasionally, we may sponsor a contest or drawing. Participation is optional. Pearson collects name, contact information and other information specified on the entry form for the contest or drawing to conduct the contest or drawing. Pearson may collect additional personal information from the winners of a contest or drawing in order to award the prize and for tax reporting purposes, as required by law.

Newsletters

If you have elected to receive email newsletters or promotional mailings and special offers but want to unsubscribe, simply email information@informit.com.

Service Announcements

On rare occasions it is necessary to send out a strictly service related announcement. For instance, if our service is temporarily suspended for maintenance we might send users an email. Generally, users may not opt-out of these communications, though they can deactivate their account information. However, these communications are not promotional in nature.

Customer Service

We communicate with users on a regular basis to provide requested services and in regard to issues relating to their account we reply via email or phone in accordance with the users' wishes when a user submits their information through our Contact Us form.

Other Collection and Use of Information


Application and System Logs

Pearson automatically collects log data to help ensure the delivery, availability and security of this site. Log data may include technical information about how a user or visitor connected to this site, such as browser type, type of computer/device, operating system, internet service provider and IP address. We use this information for support purposes and to monitor the health of the site, identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents and appropriately scale computing resources.

Web Analytics

Pearson may use third party web trend analytical services, including Google Analytics, to collect visitor information, such as IP addresses, browser types, referring pages, pages visited and time spent on a particular site. While these analytical services collect and report information on an anonymous basis, they may use cookies to gather web trend information. The information gathered may enable Pearson (but not the third party web trend services) to link information with application and system log data. Pearson uses this information for system administration and to identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents, appropriately scale computing resources and otherwise support and deliver this site and its services.

Cookies and Related Technologies

This site uses cookies and similar technologies to personalize content, measure traffic patterns, control security, track use and access of information on this site, and provide interest-based messages and advertising. Users can manage and block the use of cookies through their browser. Disabling or blocking certain cookies may limit the functionality of this site.

Do Not Track

This site currently does not respond to Do Not Track signals.

Security


Pearson uses appropriate physical, administrative and technical security measures to protect personal information from unauthorized access, use and disclosure.

Children


This site is not directed to children under the age of 13.

Marketing


Pearson may send or direct marketing communications to users, provided that

  • Pearson will not use personal information collected or processed as a K-12 school service provider for the purpose of directed or targeted advertising.
  • Such marketing is consistent with applicable law and Pearson's legal obligations.
  • Pearson will not knowingly direct or send marketing communications to an individual who has expressed a preference not to receive marketing.
  • Where required by applicable law, express or implied consent to marketing exists and has not been withdrawn.

Pearson may provide personal information to a third party service provider on a restricted basis to provide marketing solely on behalf of Pearson or an affiliate or customer for whom Pearson is a service provider. Marketing preferences may be changed at any time.

Correcting/Updating Personal Information


If a user's personally identifiable information changes (such as your postal address or email address), we provide a way to correct or update that user's personal data provided to us. This can be done on the Account page. If a user no longer desires our service and desires to delete his or her account, please contact us at customer-service@informit.com and we will process the deletion of a user's account.

Choice/Opt-out


Users can always make an informed choice as to whether they should proceed with certain services offered by InformIT. If you choose to remove yourself from our mailing list(s) simply visit the following page and uncheck any communication you no longer want to receive: www.informit.com/u.aspx.

Sale of Personal Information


Pearson does not rent or sell personal information in exchange for any payment of money.

While Pearson does not sell personal information, as defined in Nevada law, Nevada residents may email a request for no sale of their personal information to NevadaDesignatedRequest@pearson.com.

Supplemental Privacy Statement for California Residents


California residents should read our Supplemental privacy statement for California residents in conjunction with this Privacy Notice. The Supplemental privacy statement for California residents explains Pearson's commitment to comply with California law and applies to personal information of California residents collected in connection with this site and the Services.

Sharing and Disclosure


Pearson may disclose personal information, as follows:

  • As required by law.
  • With the consent of the individual (or their parent, if the individual is a minor)
  • In response to a subpoena, court order or legal process, to the extent permitted or required by law
  • To protect the security and safety of individuals, data, assets and systems, consistent with applicable law
  • In connection the sale, joint venture or other transfer of some or all of its company or assets, subject to the provisions of this Privacy Notice
  • To investigate or address actual or suspected fraud or other illegal activities
  • To exercise its legal rights, including enforcement of the Terms of Use for this site or another contract
  • To affiliated Pearson companies and other companies and organizations who perform work for Pearson and are obligated to protect the privacy of personal information consistent with this Privacy Notice
  • To a school, organization, company or government agency, where Pearson collects or processes the personal information in a school setting or on behalf of such organization, company or government agency.

Links


This web site contains links to other sites. Please be aware that we are not responsible for the privacy practices of such other sites. We encourage our users to be aware when they leave our site and to read the privacy statements of each and every web site that collects Personal Information. This privacy statement applies solely to information collected by this web site.

Requests and Contact


Please contact us about this Privacy Notice or if you have any requests or questions relating to the privacy of your personal information.

Changes to this Privacy Notice


We may revise this Privacy Notice through an updated posting. We will identify the effective date of the revision in the posting. Often, updates are made to provide greater clarity or to comply with changes in regulatory requirements. If the updates involve material changes to the collection, protection, use or disclosure of Personal Information, Pearson will provide notice of the change through a conspicuous notice on this site or other appropriate way. Continued use of the site after the effective date of a posted revision evidences acceptance. Please contact us if you have questions or concerns about the Privacy Notice or any objection to any revisions.

Last Update: November 17, 2020