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.

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
}
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.
Plainprintedfalse. defvsvaldecides how often a member of one object is computed. OneTimeDefinstance printed two different times; oneTimeValprinted 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 pattern | Guice |
|---|---|
def x = new X | No 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 X | An 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.