Posts Play modes
Post
Cancel

Play modes

Play has 3 modes: Dev, Prod and Test.

Whenever a play application is started, it is started using one of the 3 modes above.
For example, when running Play with sbt run the mode will be Dev.
When running with sbt test the mode will be Test.
When running the app after creating a distribution of it (e.g with sbt dist or sbt docker:publishLocal) the mode will be Prod.

It is important to note that these 3 modes are part of Play’s API.
Many companies have environments for staging and production.
The distributed play application will run in Prod mode in all live environments - including the staging environment.

Using Play’s modes allows us to inject different dependencies for each mode, which provides better development experience.
For instance, we can inject a service implementation that interacts with the DB in Prod mode, and an in-memory implementation in Dev and Test mode.

Instantiate the components class according to the mode

Inside the package com.example.playground.configuration.components, create 3 new classes that inherit from AppComponents: DevComponents, ProdComponents and TestComponents. Here is an example for DevComponents:

1
2
3
4
5
6
7
package com.example.playground.configuration.components

import play.api.ApplicationLoader.Context

class DevComponents(context: Context) extends AppComponents(context) {
  // wire dependencies specific for dev
}

Now we have a components class for each mode, and therefore AppComponents can be made abstract:

1
abstract class AppComponents(context: Context) extends /* .. */

Also, you may print the mode to convince yourself the app is started with the expected mode. To do so, add println(s"running in ${environment.mode} mode") in AppComponents’s body.

Let’s provide a factory that will provide the components class depending on the mode that is given to us in the context by the application loader.
In the same file (AppComponents.scala), create a companion object for the AppComponents class that will do so:

1
2
3
4
5
6
7
8
9
10
11
12
abstract class AppComponents(context: Context) extends /* .. */ {

} // end of AppComponents class

object AppComponents {
  def apply(context: Context): AppComponents =
    context.environment.mode match {
      case Mode.Dev  => new DevComponents(context)
      case Mode.Prod => new ProdComponents(context)
      case Mode.Test => new TestComponents(context)
    }
}

Now in AppLoader we no longer want (or can) to create a new AppComponents instance, since we already have a components class for each mode, and since AppComponents is abstract.
Instead, we will call the apply method by omitting the new keyword:

1
val components = AppComponents(context)

apply is a special method, and x.apply(args) is equivalent to x(args).

Run the app

Run the app in dev mode with by running run in the sbt-shell, or in prod mode, e.g by publishing a docker image and running it:

1
2
sbt docker:publishLocal
docker run --rm -p 9000:9000 playground-api

Everything should work the same.

Inject different implementations for different modes

Lets inject the in-memory dish library in Dev and Test modes, and the db implementation in Prod mode.

First, we’ll go to AppComponents class and make dish library abstract, by deleting the implementation:

1
2
3
abstract class AppComponents(context: Context) extends /* .. */ {

  val dishLibrary: DishLibrary

We will override dishLibrary in ProdComponents and provide the db implementation:

1
2
3
4
5
6
import com.example.playground.dish.{DishLibrary, DishLibraryDb}

class ProdComponents(context: Context) extends AppComponents(context) {
  // wire dependencies specific for prod
  override val dishLibrary: DishLibrary = new DishLibraryDb(dbApi)
}

And we will provide the in-memory implementation in DevComponents, and similarly in TestComponents:

1
2
3
4
5
val dishes = mutable.Set(
  Dish("Avocado Sandwich", "Whole grain bread with Brie cheese, tomatoes and avocado", 10),
  Dish("Ice Cream", "Chocolate + Vanilla Ice Cream", 8)
)
override val dishLibrary: DishLibrary = new DishLibraryInMemory(dishes)

Running the app now will provide the appropriate implementation of the dish library according the the given mode.

We will want to do the same for other dependencies as well.
For example, in the future we might want to emit stats with java-statsd-client.
In that case, we will create an abstract member of type StatsDClient in AppComponents with:

1
val statsDClient: StatsDClient

Then in ProdComponents we will override it by instantiating a new NonBlockingStatsDClient, while in dev and test modes we will override it by instantiating a new NoOpStatsDClient, which is a null StatsD client.

Allow overriding the config in Prod mode

Lastly, we want to provide the option to override db settings in conf/application.conf.
To do so, we will introduce an optional substitution for each setting that we would like to make overridable.
Change the db object in application.conf:

1
2
3
4
5
6
7
8
9
10
11
12
db {
    dish_db {
        driver = "org.h2.Driver"
        driver = ${?dish_db_driver}
        url = "jdbc:h2:mem:my_app_db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=MYSQL;INIT=CREATE SCHEMA IF NOT EXISTS dish_db\\;"
        url = ${?dish_db_url}
        username = ""
        username = ${?dish_db_username}
        password = ""
        password = ${?dish_db_password}
    }
}

The driver, for example, is “org.h2.Driver” by default.
However, if a substitution is provided under dish_db_driver, then it will be used.
A substitution that is not found in the configuration tree, is searched in the environment variables.
In practice, it means that if we will provide an environment variable called dish_db_driver with the value com.amazon.redshift.jdbc.Driver, then it will set Redshift as the driver for the db (Also, we will obviously add redshift driver to the classpath in such case by adding it as a library dependency in build.sbt).
The values for this config can be obtained with secret management tools such as HashiCorp Vault.

Also, keep in mind that in our case there was no problem in providing the db implementation of the dish library in Dev mode, since we have already made sure that the db will work out-of-the-box upon cloning the repository, by using h2 and evolutions.

Unfortunately, many repos do not work this way, and requires you to install a local db.