Play Framework projects can build from several Play / sbt projects. A common use case is to keep code modular and composable. The official documentation shows how to combine Play projects, but does not specifically show how to create separate deployment packages that each are composed of different services.
At FortyTwo, we use a single repository for our backend code. On deployment, however, our packages only contain the code they need. We had the following objectives:
- Development should be simple.
play run
should be enough to run all services at the same time. - Common dependencies and modules should be easily shared.
- We should be able to compile / test / run each service separately in development and production.
- Deployment packages should contain only the classes that the service needs.
- Routes should be split by service.
Play and SBT documentation show how to create a multi-project Build.scala
file. When you run Play / sbt, you’ll load one of the projects by default. It makes whatever project variable name comes first alphebetically the default — so you’ll likely want to name your top-level project aaaMain
or similar.
In the sbt console, you can see all of the projects with projects
. To change the project, use project projectName
. From there, you can compile / test independently.
We built a sample Play application to show how to do these (Image may be NSFW.
Clik here to view. Github repo).
multiproject
is the default top level project to be used in development, and uses two services.
Image may be NSFW.
Clik here to view.
serviceA is like a user-facing web service. Its routes look like:
GET / controllers.serviceA.Application.home() GET /serviceA controllers.serviceA.Application.main() GET /serviceA/:name controllers.serviceA.Application.greet(name: String)
serviceB’s routes look like:
GET /serviceB controllers.serviceB.Application.main() GET /serviceB/lottery controllers.serviceB.Application.lottery()
Image may be NSFW.
Clik here to view.In
multiproject
, when you run play run
, both serviceA
and serviceB
will run together, so you can work on either service and they can communicate to each other. multiproject is configured with Build.scala
such that compile
and test
run across all sub-projects as well.
play.Project("multiproject", appVersion, commonDependencies ++ serviceADependencies ++ serviceBDependencies).settings().dependsOn(common, serviceA, serviceB).aggregate(common, serviceA, serviceB)
andrew$ play [info] Set current project to multiproject (in build file:/Users/andrew/Documents/workspace/multiproject/) _ _ _ __ | | __ _ _ _| | | '_ \| |/ _' | || |_| | __/|_|\____|\__ (_) |_| |__/ play! 2.1.1 (using Java 1.6.0_45 and Scala 2.10.0), http://www.playframework.org > Type "help play" or "license" for more information. > Type "exit" or use Ctrl+D to leave this console. [multiproject] $ projects [info] In file:/Users/andrew/Documents/workspace/mp2/ [info] common [info] * multiproject [info] serviceA [info] serviceB [multiproject] $ run --- (Running the application from SBT, auto-reloading is enabled) --- [info] play - Listening for HTTP on /0:0:0:0:0:0:0:0%0:9000 (Server started, use Ctrl+D to stop and go back to the console...)
Additionally, you can also compile / test / run each service (or even the common package) independently.
[multiproject] $ project serviceA [info] Set current project to serviceA (in build file:/Users/andrew/Documents/workspace/multiproject/) [serviceA] $ test [info] ApplicationSpec [info] [info] CommonApplication should [info] + send 404 on a bad request [info] + render the status page ... snip (sbt runs tests for just serviceA and common) ... [serviceA] $ run --- (Running the application from SBT, auto-reloading is enabled) --- [info] play - Listening for HTTP on /0:0:0:0:0:0:0:0%0:9000
Image may be NSFW.
Clik here to view.Notice how serviceA only needed to compile and test
serviceA
and common
, rather than everything. This is especially cool for deployments. While you get all the benefits of a unified codebase, you can deploy completely separate packages for each service.
[multiproject] $ dist ... snip ... Your application is ready in /Users/andrew/Documents/workspace/multiproject/modules/common/dist/common-master-dc4e606-20130617-151706.zip Your application is ready in /Users/andrew/Documents/workspace/multiproject/dist/multiproject-master-dc4e606-20130617-151706.zip Your application is ready in /Users/andrew/Documents/workspace/multiproject/modules/serviceB/dist/serviceb-master-dc4e606-20130617-151706.zip Your application is ready in /Users/andrew/Documents/workspace/multiproject/modules/serviceA/dist/servicea-master-dc4e606-20130617-151706.zip
Since each of these are self contained, your deployment manager can push these distributions to their respective application servers.
As a note, since each deployment will not have all of the classes, the default Play reverse router will not be able to determine URLs cross-service. You will likely want to write your own reverse router in a common module.
Clone the separate multi-project deployment packages git repository on Github to play with this.