Field Notes

Guice and Play, part 2: the moment Guice can't guess

Guice and Play, part 2: the moment Guice can't guess

In part 1 I switched a Play 3 template from the cake pattern to Guice. Everything I injected was a concrete class, so Guice never had to make a decision: it found an @Inject() constructor and called it. This part is about the first time Guice could not do that on its own, and what I had to add so it could.

A test that could not pass

After part 1, my Greeter depended on a small concrete class, Clocky, and put the current time into every greeting. I had also added a name check with cats Validated. The service looked like this:

class Greeter @Inject() (clocky: Clocky) {
  def greet(name: String): String = {
    val validationResult = validateName(name).fold(b => b, b => b)
    s"Hello, $validationResult! at ${clocky.now}"
  }
  // validateName: Invalid when the name is longer than 6 characters
}

The controller test was still asserting an exact string. With LocalTime.now() inside the result, that assertion cannot pass, whatever the code does. The test needed to control the time, so Greeter had to depend on something I could replace. In other words, a trait.

Step one: a trait and @ImplementedBy

A trait has no constructor, so Guice cannot create one just-in-time. It needs to be told which implementation to use. The quickest way is an annotation on the trait:

@ImplementedBy(classOf[SystemClocky])
trait Clocky {
  def now: LocalTime
}

@Singleton
class SystemClocky @Inject() () extends Clocky {
  def now: LocalTime = LocalTime.now()
}

The test double lives in test/, so production code never sees it:

class FixedClocky(val now: LocalTime) extends Clocky

private val greeter = new Greeter(new FixedClocky(LocalTime.of(9, 0)))
// controller.hello("Zak") -> "Hello, Zak! at 09:00"

Three tests passed. The part I want to point out is what did not change: Greeter still declares clocky: Clocky, and I did not edit it at all. It went from depending on a class to depending on a trait with the same source code.

But @ImplementedBy has a design problem. The trait names its own implementation, so the abstraction depends on the concrete class, which is the wrong direction. Swapping the implementation means editing the trait.

Step two: the decision moves into a Module

I removed the annotation and put the binding in a Guice module:

import com.google.inject.AbstractModule
import services.{Clocky, SystemClocky}

class Module extends AbstractModule {
  override def configure(): Unit = {
    bind(classOf[Clocky]).to(classOf[SystemClocky])
  }
}

There is no registration step, because of a Play convention: a class named exactly Module in the root package is registered automatically. If you rename it or move it into a package, you have to add it to play.modules.enabled in application.conf. Otherwise Play never loads it, and the first sign is a missing-binding error. An explicit bind in a module also takes precedence over @ImplementedBy, so the annotation is only ever a fallback.

The @Singleton stays on SystemClocky. Scope belongs to the implementation class, and bind(...).to(...) respects it.

Comparison of @ImplementedBy and a Guice Module: where the binding lives, dependency direction, how to swap the implementation, and precedence
@ImplementedBy is a fallback attached to the trait. A Module is the place where the decision actually lives.

Finally, a test that builds the real graph

In part 1 the unit tests passed without ever creating a Guice injector, and I had to start the app to check the wiring. Now that there is a real binding to get wrong, I added a spec that builds the application with the same modules sbt run uses:

class GuiceGraphSpec extends PlaySpec {
  "The Guice graph" should {
    "bind Clocky to SystemClocky via the root Module" in {
      val app = new GuiceApplicationBuilder().build()
      running(app) {
        app.injector.instanceOf[Clocky] mustBe a[SystemClocky]
      }
    }

    "let a test override a binding without touching production code" in {
      val app = new GuiceApplicationBuilder()
        .overrides(bind[Clocky].toInstance(new FixedClocky(LocalTime.of(9, 0))))
        .build()
      running(app) {
        app.injector.instanceOf[Greeter].greet("Zak") mustBe "Hello, Zak! at 09:00"
      }
    }
  }
}

There is a third case in the spec that checks instanceOf[Clocky] eq instanceOf[Clocky], confirming the singleton. All six tests passed.

Breaking it on purpose

With the cake pattern, a missing dependency is a compile error. With Guice it is a runtime error, so I wanted to see exactly what it looks like. I commented out the bind line and ran sbt "testOnly wiring.GuiceGraphSpec":

Guice MissingImplementation error: no implementation for services.Clocky was bound, requested by Greeter's clocky parameter, from HomeController, from router.Routes
The real output. Every line is one step back up the graph, to the router that started it.

The message is more useful than I expected. It names the missing type, then the exact constructor parameter that asked for it (clocky in Greeter.scala:11), then who asked for Greeter, and so on up to router.Routes. Read from top to bottom, it is the path through the graph. That is roughly what the compiler would have reported in the cake version, just later.

The override that hid the hole

Two tests failed. The third, the override test, passed. .overrides(...) supplied a Clocky, so the graph was complete in that test even though production had no binding. That is the expected behaviour, but it is an easy trap: if every Guice test overrides the same binding, nothing tests the production one. I now keep at least one test that builds the application with no overrides at all.

How this compares to my cake slices

A cake slice does two things: it decides which implementation backs each abstraction, and it constructs every object, one lazy val at a time. The Guice module only does the first. Greeter and HomeController are not mentioned anywhere in it, because Guice still builds concrete classes on its own.

That changes how the wiring grows. In the cake version, every new class is a new line in a slice. With a module, the file only grows when I introduce a new abstraction with a real choice behind it. For the projects where my wiring traits became hard to navigate, that is the difference I was looking for.

Next up, part 3: scopes and lifecycle in detail, including eager singletons and stopping resources cleanly with ApplicationLifecycle.

Reference: Play 3.0.x, Scala dependency injection.

Keep reading