Compile Time DI in Play, Part 1

Part 1 of 3: Introduction

Starting in version 2.4, Play started pushing users towards implementing dependency injection in their apps. DI is a simple but powerful concept that says that your components should declare their dependencies instead of building or finding those dependencies themselves.

DI directly improves our code at Even by decoupling components and removing global state. But even more compellingly, it improves the value of both our unit and system tests.

In Part 1 of this 3-part blog post, I’ll describe DI and how it has improved recent versions of Play. In Part 2, we’ll dive into the implementation details of compile time DI in Play which dramatically builds to Part 3, where I discuss exactly how DI can increase the value of a test suite.

DI, Concretely

Consider the following code for a client in Play:

import play.api.libs.ws.WS
object UserClient {
def getUser(id: Long): Future[Option[JsValue]] = {
WS.url("example.com/users/" + id).get().map { response =>
if (response.status == 200) Some(response.json) else None
}
}
}

UserClient defines the logic to fetch a user from a remote API, but it also resolves its dependency by referencing the singleton WS. This is the type of overachievement that makes everyone else look bad. Dependency injection instead bravely asks the question that everyone else is afraid to:

Wherein Homer represents dependency injection

Instead of finding WS on it’s own, DI says that UserClient should just say that it needs an implementation of (the equivalent) WSClient, and let someone else figure out the details.

So, we’ll change UserClient to be a class whose constructor takes a WSClient parameter. This has the added benefit of making that dependency a part of UserClient's type signature, meaning that the type checker can help statically verify the dependency hierarchy.

import play.api.libs.ws.WSClient
class UserClient(ws: WSClient) {
def getUser(id: Long): Future[Option[JsValue]] = {
ws.url("example.com/users/" + id).get().map { response =>
if (response.status == 200) Some(response.json) else None
}
}
}

At the end of the day, despite some fancy language, this is pretty simple stuff.

DI, in Play

So how does this fit into Play?

Historically, Play apps defined a global, static instance of the Application trait which pretty much every other component depended on, at least transitively. In fact, the non-DI code above is actually worse than I made it seem because WS.url would’ve expected an implicit Application instance, which I would’ve needed to supply via this static import:

import play.api.Play.current

This dependence on the global, staticApplication instance really sucked because it reduced the modularity of components like our UserClient, which in turn reduced the potential quality of tests.

So Play began to migrate towards DI by deprecating singletons like WS whose interfaces expected an implicit Application. For example, in Play 2.4 and 2.5, you will get warned about using WS thusly:

object WS in package ws is deprecated: Inject WSClient into your component

In Play 2.6, WS (and singletons like it) will be removed entirely.

Dependency Resolution

It’s easy to pass the buck up the dependency hierarchy, but we do need the root node to actually take responsibility for building and passing in all the dependencies.

Wherein Harry Truman represents the root node in a dependency hierarchy

Play has built-in support for a runtime approach to this using Guice, a Java DI library built by Google. It also supports a compile time approach that can be done without any special libraries (although, you could integrate a compile time DI library on your own).

I decided to use the compile time approach (sans special libraries) on all of our Play apps at Even. Here are some pros and cons:

  • Pro #1: I can use regular Scala which is nice in and of itself, but it also means that I didn’t have to learn the Guice DSL.
  • Con #1: Compile time DI in Play is a second-class citizen compared to runtime DI. I had to read a fair amount of the Play source to get it to work, and it requires some weird boilerplate code.
  • Pro #2: Everything gets resolved at compile time! You know what they say about your fancy static type checker: if you don’t use it, you lose it.
  • Con #2: Actually, not everything is guaranteed to be resolved at compile time. If you’re not careful, you can encounter some dependency resolution errors at runtime.

In Part 2 of this post, I’ll implement a contrived-yet-realistic Play app that exhibits compile time DI. I’ll also discuss helpful boilerplate code (see Con #1) and how to avoid unexpected runtime errors (see Con #2).

Like what you read? Give Kevin Hyland a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.