The complete picture: Lagom and Play in action (Java)

(, EN )

If you ever came across Lightbend, the driving force behind Scala and Akka, you might also have heard about Lagom and Play.

Lagom and Play fall into the category of reactive microservices. I’ll not go into detail about this, there are plenty of resources out there (e.g. here and on a broader perspective, here). The most important takeaway is that you can build a microservice architecture with Lagom. Play complements Lagom as it provides the web-framework that serves content to the user using Lagom’s microservices in the background.

What puzzles me, though, is that Play and Lagom are handled in isolation when searching for information or going through the tutorials (at least that is my experience).

How can you combine Lagom and Play to set up a complete system?

There are some examples, such as the online auction or chirper, but they have been archived already. This leaves (advanced) beginners with a challenge: how can I combine the moving parts to build a complete system? Once you went through the initial guides of Lagom and Play, some random tutorials and examples, you’ll finally arrive at the magical gap between “I know the basics” and “I know enough, so I can do something useful with it”.

How do I get there?

Lagom and Play don’t make it easy to get a foot on the ground, this post should help. But first things first: what do I want to end up with? A minimal example could look like this (microservices are denoted by “μs”):

Architecture Simple

The user is served a single-page application (web-framework/UI in the diagram) using e.g. React. The web-framework connects to a frontend-gateway (a Play app) which handles calls to the Lagom-based microservices.

Having a frontend gateway comes with additional benefits:

  1. you can serve your single-page app (SPA) or static resources with this setup
  2. APIs of microservices can be changed without the need to adapt the API that is used by the SPA.
  3. gateways can shield your services from the “outside”
  4. you can transparently add extra functionality, such as caching or authentication

More information on this can be found on microservices.io.

You can make the architecture as complex as you want. A more complex or demanding architecture might look like this (microservices are denoted by “μs”):

Architecture Complex

The client-side stays the same, but the server-side now has a reverse proxy, e.g. haproxy, a caching proxy, e.g. nginx, a node.js server with e.g. Express, Koa or hapi to enable server-side rendering. You might use multiple frontend-gateways to split concerns and group microservices. This setup is usually too sophisticated for most purposes, with the minimal architecture you can already cover most use cases.

So let’s dig deeper into the minimal example.

Getting started with Lagom Microservices

Before starting, I assume that you already installed sbt. If not, there are plenty of tutorials on this, Google is your best friend! The start with Lagom is quite straight forward, just make use of the giter8 template to create a new project (I called my project hello-world, the default):

sbt new lagom/lagom-java.g8
cd hello-world
sbt runAll

You can test it with curl or, my preferred tool, httpie:

curl http://localhost:9000/api/hello/lagom
http :9000/api/hello/lagom

results in

HTTP/1.1 200 OK

Hello, lagom!

So far, so good. Hello World is running! Time to add play to our project.

Adding the Play Frontend Gateway

This snippet will get you stated with play:

cd hello-world
sbt new playframework/play-java-seed.g8

By default, Play applications use the Play application layout. Lagom uses the sbt-layout, so we have to adapt the Play-app to the sbt-layout. One would expect that this layout is already available as giter8-seed, but I could not find anything well maintained. To get it into sbt-layout (the hard way), you have to move some stuff around (I called my play project frontend):

cd hello-world/frontend
mkdir -p src/main/{java,twirl}
mkdir -p src/test/java
mv app/controllers src/main/java
mv app/views src/main/twirl
mv conf src/main/resources
mv public src/main/
mv test/controllers src/test/java

rm -r app
rm -r test

You still have to adjust the sbt setup (build.sbt and plugins.sbt) of the Lagom project:

cd hello-world
cat frontend/project/plugins.sbt >>project/plugins.sbt
rm -r frontend/build.sbt frontend/project

Add this to your build.sbt:

lazy val frontend = (project in file("frontend"))
  .settings(common)
  .enablePlugins(PlayJava, LagomPlay)
  .disablePlugins(PlayLayoutPlugin)
  .settings(
    libraryDependencies ++= Seq(
      lagomJavadslClient,
      guice
    )
  )
  .dependsOn(`hello-world-api`)

You also have to add the frontend project to the aggregate root project:

lazy val `hello-world` = (project in file("."))
  .aggregate(`hello-world-api`, `hello-world-impl`, `hello-world-stream-api`, `hello-world-stream-impl`, `frontend`)

That should do it. Let’s try it

cd hello-world
sbt runAll

At this step, most people bump into some kind of dependency injection error. It is not explained very well in the tutorials for Lagom or Play and the error itself is not very clear, either:

com.google.inject.CreationException: Unable to create injector, see the following errors:

1) Could not find a suitable constructor in com.lightbend.lagom.javadsl.api.ServiceInfo. Classes must have 
either one (and only one) constructor annotated with @Inject or a zero-argument constructor that is not private.
  at com.lightbend.lagom.javadsl.api.ServiceInfo.class(ServiceInfo.java:47)
  while locating com.lightbend.lagom.javadsl.api.ServiceInfo
    for the 3rd parameter of com.lightbend.lagom.internal.javadsl.client.JavadslServiceClientImplementor.
    <init>(JavadslServiceClientImplementor.scala:46)
  at com.lightbend.lagom.internal.javadsl.client.ServiceClientModule.bindings(ServiceClientModule.scala:15):
Binding(class com.lightbend.lagom.internal.javadsl.client.JavadslServiceClientImplementor to self) (via modules: 
com.google.inject.util.Modules$OverrideModule -> play.api.inject.guice.GuiceableModuleConversions$$anon$4)

2) Could not find a suitable constructor in com.lightbend.lagom.javadsl.api.ServiceInfo. Classes must have either
one (and only one) constructor annotated with @Inject or a zero-argument constructor that is not private.
  at com.lightbend.lagom.javadsl.api.ServiceInfo.class(ServiceInfo.java:47)
  while locating com.lightbend.lagom.javadsl.api.ServiceInfo
    for the 2nd parameter of com.lightbend.lagom.play.PlayRegisterWithServiceRegistry.<init>(LagomPlayModule.scala:87)
  at com.lightbend.lagom.play.LagomPlayModule.bindings(LagomPlayModule.scala:35):
Binding(class com.lightbend.lagom.play.PlayRegisterWithServiceRegistry to self eagerly) (via modules: com.google.
inject.util.Modules$OverrideModule -> play.api.inject.guice.GuiceableModuleConversions$$anon$4)

2 errors
  at com.google.inject.internal.Errors.throwCreationExceptionIfErrorsExist(Errors.java:543)
  at com.google.inject.internal.InternalInjectorCreator.initializeStatically(InternalInjectorCreator.java:159)
  at com.google.inject.internal.InternalInjectorCreator.build(InternalInjectorCreator.java:106)
  ...

This error message translates into: we need to create hello-world/frontend/src/main/java/Module.java and bind service meta data:

import com.google.inject.AbstractModule;
import com.lightbend.lagom.javadsl.api.ServiceAcl;
import com.lightbend.lagom.javadsl.api.ServiceInfo;
import com.lightbend.lagom.javadsl.client.ServiceClientGuiceSupport;

public class Module extends AbstractModule implements ServiceClientGuiceSupport {
    @Override
    protected void configure() {
        bindServiceInfo(ServiceInfo.of( "frontend", ServiceAcl.path("(?!/api/).*") ));
    }
}

The service frontend is binding all paths except the /api/... paths (which are used by the lagom microservices). Let’s try it again:

sbt runAll

Welcome to Play

Better! Time to add a route to the play frontend which will invoke the Lagom backend.

We need to bind the HelloWorldService, resulting in the following frontend/src/main/java/Module.java:

import com.example.helloworld.api.HelloWorldService;
import com.google.inject.AbstractModule;
import com.lightbend.lagom.javadsl.api.ServiceAcl;
import com.lightbend.lagom.javadsl.api.ServiceInfo;
import com.lightbend.lagom.javadsl.client.ServiceClientGuiceSupport;

public class Module extends AbstractModule implements ServiceClientGuiceSupport {
    @Override
    protected void configure() {
        bindServiceInfo(ServiceInfo.of( "frontend", ServiceAcl.path("(?!/api/).*") ));
        bindClient(HelloWorldService.class);
    }
}

Add the service to the Controller (frontend/src/main/java/controllers/HomeController.java):

package controllers;

import com.example.helloworld.api.HelloWorldService;
import com.google.inject.Inject;
import play.mvc.*;

import java.util.concurrent.CompletionStage;

/**
 * This controller contains an action to handle HTTP requests
 * to the application's home page.
 */
public class HomeController extends Controller {

    private HelloWorldService helloWorldService;

    @Inject
    public HomeController(HelloWorldService helloWorldService) {
        this.helloWorldService = helloWorldService;
    }
    /**
     * An action that renders an HTML page with a welcome message.
     * The configuration in the <code>routes</code> file means that
     * this method will be called when the application receives a
     * <code>GET</code> request with a path of <code>/</code>.
     */
    public Result index() {
        return ok(views.html.index.render());
    }

    public CompletionStage<Result> hello(String id) {
        CompletionStage<String> message = helloWorldService.hello(id).invoke();

        return message.thenApply(s -> ok(s + " (Proxied by frontend)" ));
    }

}

And last, but not least, create add a route in frontend/src/main/resources/routes:

# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# An example controller showing a sample home page
GET     /                           controllers.HomeController.index
GET     /hello/:id                  controllers.HomeController.hello(id: String)

# Map static resources from the /public folder to the /assets URL path
GET     /assets/*file               controllers.Assets.versioned(path="/public", file: Asset)

We can now call the HelloWorldService directly:

$ http :9000/api/hello/lagom
HTTP/1.1 200 OK

Hello, lagom!

Or via the frontend:

$ http :9000/hello/lagom
HTTP/1.1 200 OK

Hello, lagom! (Proxied by frontend)

Yeah!

Conclusion

Stripped down to the minimum, this post provides you with a working example of Lagom and Play in in action.

In the context of microservices, the controllers in the frontend gateway are used to have overarching logic, which spans or combines multiple microservices. An example could be a setup with two microservices, a ShoppingBagService and an OrderService. With a Play-controller, both services can be combined into a CheckoutGateway. The CheckoutGateway might provide routes such as POST /payment, which closes the shoppingbag and creates an order, making it possible to “cross” microservice boundaries.

Have fun!