Separate multi-project deployment packages in Play! Framework

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 ( Github repo). multiproject is the default top level project to be used in development, and uses two services.

Project structure

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()

Running multiple projects at onceIn 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

ServiceA runningNotice 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.

We wrote this post while working on Kifi — tools that help people outsmart information overload together. Learn more.

23 comments
nigui
nigui

Hi,


I'm testing your project, but when i run with activator, the subproject "serviceA", it print the "No router defined." message on my browser.


Can you help me? Thx

j_j
j_j

Hi,


Is there any way to define confDirectory for each module, or any way of refer the confDirectory of each module in order to be included in the multi-project?


In fact, what I want is to use the Play.application().configuration() to read the properties defined in "module-application.conf". 

mordonez_me
mordonez_me

Is there a way to put this in a sbt project, I have something like this


sbt

-project

  -modules

    -client

  - web(play)

  build.sbt


to run my web app I use play "project web" run but it doesn't work, for some reason the modules inside web (play) are not included when I run it, any ideas?

stevefturner
stevefturner

Trying out the sample and it appears that when executing the play run task on a submodule, it does not load the service*-application.conf. However, I see that you specify the right config in Test. Is there a way to specify the javaOption in "Run" ? Otherwise the submodules do not resolve their independent routers.


antonisamy
antonisamy

 @Andrew Conner  This is great and works fine in Dev mode. When I run in Prod (play start or play stage and then target/start) only the common url /status works. ServiceA or serviceB urls cannot be found.

 

Same with dist. I guess thats where you are referring to setting up your own reverse router. Can you please elaborate on that.

Shawn Liu
Shawn Liu

Very helpful blog. I have a question which is also the problem in my project now. Is it possible to use this approach to package service A & B into different war files using play2war plugin? 

 

I guess it may be difficult since this approach uses a main project and only has one build script for all sub-projects. 

virtualeyes
virtualeyes

 @Andrew Conner  did you guys make any headway with Guillaume's suggestion to create an SBT Task to copy application routes into a jar stored in common base Project?

 

Sub projects are an absolute god send, but the lack of type safety in non-dependent sub project reverse routing is an invitation to runtime errors, driving me nuts -- Play is after all part of TypeSafe ;-)

alpeb
alpeb

Do you know if it's possible to modify the routing (in a plugin or the Global object) so that each module can be served through a different domain name? Kinda like Apache's name-based virtual hosting.

Jared Jacobs
Jared Jacobs

Is there really no way to include another project's compiled reverse routes without including the entire project? What's the specific difficulty there? Thanks.

antonisamy
antonisamy

 @Andrew Conner Btw, simple to reproduce... checkout the git project... play run work fine.... play start does not.

Andrew Conner
Andrew Conner

Not yet, but that's one of my next things to do. I'm glad it's possible. When I get it working cleanly and tested in our production, I'll post the code in the mailing list. I'm glad that I'm not the only one wanting this :).

Andrew Conner
Andrew Conner

 @Jared Jacobs Play's default reverse router works by generating classes automatically that map real classes to URL paths. That's the reason you can do "GET / controllers.Application.home()" and achieve type safety.

 

Unfortunately, this means the routed class needs to be present for this process to work. There may be a way to get sbt to include just the reverse route classes — during build time and after source generation — into a common package. I'd love to get some feedback from the Play/sbt maintainers if this is possible.

vaedama
vaedama

@jakubkahovec @stevefturner I guess you can set `javaOptions in run` just like you do `javaOptions in Test` or even both like `javaOptions in (Test, run)` is you would like.

Andrew Conner
Andrew Conner

 @antonisamy The projects are intended to be run separately, where each names which router it uses (like serviceA.Routes). If you want to run the projects together, just name the combined router by setting the `application.router` setting in your configuration.

virtualeyes
virtualeyes

 @Andrew Conner Everyone using sub projects wants it, how could they not? The alternative is manual string paths or maintaining your own reverse router as you guys are doing. None of it is type safe to the actual conf/routes.

 

I did a test today and yes, it does work, copying reverse route .class files to a base project; however, it is slow, better suited as an SBT Task rather than as part of each compile (routes aren't changing that often).

alpeb
alpeb

 @Andrew Conner  @alpeb Ok so the reverse proxy will sort out the different domains, but if I wanna use the same Play instance I guess I would have to do some url-rewriting in nginx to route requests from domain A to /serviceA and requests from domain B to /serviceB for example.

antonisamy
antonisamy

 @Andrew Conner  @Jared Jacobs  What revers route classes do we need to include in the common package.

 

I Would like to try it out manually at least. And if it work, can try do it as part of sbt builds. But right now, I am not sure what route classes needed to be copied.

jdev
jdev

@Andrew Conner @antonisamy Andrew, great work on this.  I'm confused by your answer to antonisamy though.  I'm also having trouble running these separately -- i did the following

1) project serviceB

2) stage

3) (exit out of play) ./modules/serviceB/target/start

It runs fine on port 9000, but the only url I can hit is /status.  All other requests (like /serviceB and /serviceB/lottery) return "Action not found" errors

Andrew Conner
Andrew Conner

 @alpeb Ah. Yes, exactly. You don't need to prefix routes with /serviceA/, for example, but you need to make sure you don't have conflicting routes. Prefixing just helps to namespace things.

For service-to-service communication, we prefix all routes with /internal/<serviceName>/, and explicitly block outside requests to all /internal/ routes.