Field Notes

Guice and Play, part 1: I deleted my wiring file and Guice built the graph

Guice and Play, part 1: I deleted my wiring file and Guice built the graph

At work I wire Play applications by hand. Every service is a lazy val in a trait, every trait has a self-type, and AppComponents mixes them all together. It is the cake pattern, and I like what it gives me: if a dependency is missing, the build fails. What I like less is how it grows. Each new class means editing the wiring as well as writing the class, and on a large project the wiring becomes a codebase of its own.

For my own projects I wanted to try the other option Play ships with: runtime dependency injection with Guice. This series is my notes as I learn it properly, one step at a time. Part 1 covers the switch itself, the first injected class, and a timestamp that convinced me, for about five minutes, that Guice was broken.

The starting point: a hand-written object graph

My Play 3 starter template (Play 3.0.12, Scala 2.13) uses compile-time DI. The whole graph lives in one class:

class AppComponents(context: ApplicationLoader.Context)
    extends BuiltInComponentsFromContext(context) with HttpFiltersComponents {
  lazy val homeController = new HomeController(controllerComponents)
  lazy val router: Router = new Routes(httpErrorHandler, homeController)
}

A custom ApplicationLoader builds it, and application.conf points Play at that loader with play.application.loader = "wiring.AppLoader". Note who calls new here: I do, for everything, including the generated router.

Switching Play to Guice is a deletion

The interesting part is how little there is to do. build.sbt already contained guice in libraryDependencies, because the Play template includes it by default. Guice was always on the classpath; my config line was overriding it. Play's default loader is GuiceApplicationLoader, so I removed the loader setting and deleted AppLoader.scala completely.

The routes file kept working without changes. Play's router generator produces an injectable router.Routes class whose constructor takes the controllers. In compile-time mode I called that constructor myself. Now Guice calls it.

The first injected class

To have something to inject, I added a small service and asked the controller for it:

class Greeter @Inject() () {
  def greet(name: String): String = s"Hello, $name!"
}

class HomeController @Inject() (val controllerComponents: ControllerComponents, greeter: Greeter)
    extends BaseController {
  def hello(name: String): Action[AnyContent] = Action {
    Ok(greeter.greet(name))
  }
}

plus a route, GET /hello/:name. There is no module and no binding for Greeter. When the router needs a HomeController, Guice finds the constructor annotated with @Inject() and resolves each parameter. Play's built-in module binds ControllerComponents. Greeter is a concrete class with an injectable constructor, so Guice creates a just-in-time binding for it and resolves its dependencies the same way.

One Scala detail caught me straight away: the empty parentheses in @Inject() (...) are required. Without them, Scala treats the constructor parameter list as arguments to the annotation, and the resulting compile error does not point at the real cause.

Side-by-side comparison of compile-time cake wiring and Guice runtime wiring: where the wiring lives, what a new service costs, who builds the routes, and when a missing dependency is reported
The wiring moves from AppComponents into the constructors. So does the moment you find out it is wrong.

Green tests that proved nothing about the graph

sbt test passed. But the controller spec builds the controller by hand:

private val controller = new HomeController(stubControllerComponents(), new Greeter)

That is a good property of constructor injection: @Inject() is only an annotation, and the class stays a plain class in a unit test. It also means the test never touches Guice. To see whether Guice could actually build the graph, I started the app with sbt run and requested /hello/Zakaria. It returned 200 Hello, Zakaria!. With the cake pattern the compiler would have checked this for me. With Guice, the check only happens when the injector starts.

“It's not a singleton!”

Next I gave Greeter its own dependency, a Clocky class with def now = java.time.LocalTime.now(), and printed the time in the greeting. I refreshed the page twice and got two different timestamps. My first reaction was that Guice was building a new Greeter on every request.

The screenshots did not show that. now was a def, so it runs again on every call, and a single instance returns a new time every time. Still, my instinct was pointing at something real, so I wrote a small experiment against a bare injector with no modules at all:

object ScopeDemo extends App {
  class Plain @Inject() ()
  @Singleton class Single @Inject() ()
  class TimeDef @Inject() () { def now = java.time.LocalTime.now() }
  class TimeVal @Inject() () { val now = java.time.LocalTime.now() }

  val injector = Guice.createInjector()
  def sameInstance[A <: AnyRef](cls: Class[A]): Boolean =
    injector.getInstance(cls) eq injector.getInstance(cls)
  // ... print the results
}
Output of ScopeDemo: Plain gives a different instance each time, Single gives the same instance, one TimeDef instance returns two different times, one TimeVal instance returns the same time
One TimeDef instance, two different times. The first screenshots were measuring def, not the scope.

The output separates two questions I had been treating as one:

  • Scope decides how many objects exist. Without an annotation, Guice creates a new instance for every injection point and every lookup. Plain printed false.
  • def vs val decides how often a member of one object is computed. One TimeDef instance printed two different times; one TimeVal printed the same time twice.

Mapping it back to the cake pattern

The Play documentation describes the default directly: new instances are created every time a component is needed, and they are created lazily. Coming from the cake pattern, this is the opposite of what I was used to, because a lazy val in a component gives one shared instance. The mapping I now keep in my head:

Cake patternGuice
def x = new XNo annotation (the default): a new instance per injection
lazy val x = new X@Singleton: one instance per injector, created on first use
val x = new XAn eager singleton binding, bind(...).asEagerSingleton() in a module

In my app, the router asks for HomeController once and the controller asks for Greeter once, so in practice there was only one of each. That comes from the shape of the graph, not from a guarantee. As soon as a second class injects Greeter, there are two. Anything that holds state or an expensive resource (a cache, a connection pool, a MongoClient) should say @Singleton explicitly.

There is also a dev-mode detail: when sbt run reloads after a code change, Play builds a new injector, so even singletons are recreated. A singleton is one instance per injector, not one per JVM.

What I gave up, so far

After part 1, the trade is clear. I deleted a wiring file, added a service by writing only the service, and never touched a component trait. In return, a missing or ambiguous binding now fails when the application starts, not when it compiles. That is the main thing I will need to manage as the project grows, and a later part of this series will add a test that builds the real Guice graph, so the failure shows up in sbt test and not in the first request.

Next up, part 2: traits with more than one implementation, where Guice can no longer guess, and the choice between @ImplementedBy and a Module with explicit bindings. That is the part that maps most closely to the slices of a cake.

Reference: Play 3.0.x, Scala dependency injection.

Keep reading