From Runtime Dependency Injection to the Cake Pattern and Back

 | 

At Livongo we’ve been building our web APIs in Scala using the Play Framework since our inception in 2015, and overall we’ve been quite happy with both of these choices. We did, however, come to regret our early decision to use the Cake Pattern for dependency injection (DI), and have recently spent a LOT of time ripping it out of our large code base and replacing it with Guice runtime DI. This post will describe our journey, what we learned along the way, and where we may go next.

Choosing Cake
layer cake
Photo by Crazy House Capers / CC BY-NC-ND 2.0

Prior to Livongo, the only production app we’d built in Scala was a fairly small Akka-based app that didn’t need DI. We’d also built large web apps in Java using Servlets and traditional runtime DI like Spring and Guice, and in general we felt this traditional DI approach worked quite well.

But when we started looking at how Scala Play apps were built in those days (2015, Play 2.3), we saw that Play offered no built-in support for runtime DI and thus that most Play apps used the Cake Pattern. Also, in researching how DI was or should be done in Scala apps in general, our takeaway was that the Cake Pattern was the most idiomatic choice. So the Cake Pattern it was.

Our web APIs are comprised of three execution layers — controllers, services, and repositories — and a domain model comprised of case classes.  The lightweight controller methods dispatch to service methods and deal with JSON / domain model conversions. The service methods implement the bulk of the business logic (the rest is implemented in domain classes) and call the repository layer to read or write to a DB or to interact with an external web service.

With the Cake Pattern, then, here’s what a skeletal web API implementation for user authentication looked like:

//
// repository
//
trait UserAuthRepositoryComponent {
  val userAuthRepo: UserAuthRepository

  trait UserAuthRepository {
    def authenticate(login: String, password: String): Future[Option[User]]
  }
}

trait UserAuthRepositoryComponentImpl extends UserAuthRepositoryComponent {
  override val userAuthRepo: UserAuthRepository = new UserAuthRepositoryImpl

  class UserAuthRepositoryImpl extends UserAuthRepository {
    def authenticate(login: String, password: String): Future[Option[User]] = ???
  }
}

//
// service
//
trait UserAuthServiceComponent {
  val userAuthService: UserAuthService

  trait UserAuthService {
    def authenticate(login: String, password: String): Future[Option[User]]
  }
}

trait UserAuthServiceComponentImpl extends UserAuthServiceComponent {
  self: UserAuthRepositoryComponent =>

  override val userAuthService: UserAuthService = new UserAuthServiceImpl

  class UserAuthServiceImpl extends UserAuthService {
    def authenticate(login: String, password: String): Future[Option[User]] = ???
  }
}

//
// controller
//
trait UserAuthController extends play.api.mvc.Controller {
  self: UserAuthServiceComponent =>

  def auth = Action.async { request =>
    ???
  }
}

object UserAuthController extends UserAuthController
  with UserAuthServiceComponentImpl
  with UserAuthRepositoryComponentImpl

//
// domain class
//
case class User(id: Int, login: String)
Pains Using Cake

There were some day to day pains for us in using the Cake Pattern. The obvious first one was the need for all the boilerplate *Component and *ComponentImpl code for every service and repository, which is tedious to write and read. To facilitate unit testing, we also created mock implementations of each component (with a mock service or repository inside), which was yet more boilerplate code. In retrospect, we might have been able to mitigate this pain by writing some code-generation macros.

The second and bigger pain was that when wiring (cooking?) up all the layers of the cake in the controllers (or in controller or service unit tests), it was necessary to explicitly include not just the controller’s direct dependencies (typically just a single service), but rather all of the controller’s transitive dependencies. Since many of our services themselves depended on other services as well as their own repository, having to explicitly resolve and include all of these was a big hassle. Further, any time a new dependency was added to a service, we’d need to go and update any controllers impacted by the new transitive dependencies.

We ultimately “drowned” this pain by cooking up a single, big, ugly ReallyGiantCake trait that mixed in every single service and repository, then having each controller simply extend that. After that, new service dependencies, or new services or repositories, needed only to be added to the ReallyGiantCake.

In retrospect, a better way would have been to implement another bit of boilerplate per service, namely a service “module” trait that extended its ServiceComponentImpltrait with all its dependencies (its matching RepositoryComponentImpl trait and the service module traits of its service dependencies). That way each controller would need to only have its direct dependencies’ modules wired in. At first glance, though, this wouldn’t have helped in setting up our controller and service unit tests.

Pain Removing Cake

With the 2.4.x release, the Play Framework began supporting runtime dependency injection using Guice as part of a longer-term initiative to completely remove all dependence (by the framework and applications built with it) on global objects and state. Given the dissatisfaction we had after using the Cake Pattern for a couple years coupled with this new direction for the Play Framework, we happily decided to “de-cake” our application by refactoring it to use runtime DI with Guice instead.

What we quickly discovered when we began to refactor though was that the Cake Pattern is perfectly happy to let you create circular dependencies (cycles in the dependency graph), even though you likely did not want or intend to create them. Here’s a simple example that shows what a piece of cake it is (boilerplate aside) to have two services that each require the other:

//
// service A
//
trait AServiceComponent {
  val aService: AService
  
  trait AService {
    def methodBNeeds: String
    def methodINeedB: String
  }
}

trait AServiceComponentImpl extends AServiceComponent {
  self: BServiceComponent =>

  val aService: AService = new AServiceImpl

  class AServiceImpl extends AService {
    def methodBNeeds: String = "from A"
    def methodINeedB: String = bService.methodANeeds + "!"
  }
}

//
// service B
//
trait BServiceComponent {
  val bService: BService

  trait BService {
    def methodANeeds: String
    def methodINeedA: String
  }
}

trait BServiceComponentImpl extends BServiceComponent {
  self: AServiceComponent =>

  val bService: BService = new BServiceImpl

  class BServiceImpl extends BService {
    def methodANeeds: String = "from B"
    def methodINeedA: String = aService.methodBNeeds + "?"
  }
}

//
// Baking the circular cake
//
trait SomeDependee {
  self: AServiceComponent with BServiceComponent =>

  def fromA: String = aService.methodINeedB
  def fromB: String = bService.methodINeedA
}

object SomeDependee
  extends AServiceComponentImpl
    with BServiceComponentImpl

While in fact the Cake Pattern’s ability to represent arbitrary (including cyclic) dependency graphs is rightly cited by Martin Odersky as one of its strengths, it also provides the rope for the unwary (like us) to hang themselves with. While it’s not to blame for how it may be misused, the ease with which it silently facilitates such misuse is something to be wary of.

When moving dependency declarations from Cake’s self-type annotations to constructor parameters, it quickly becomes apparent that circular dependencies aren’t so innocuous any more. After refactoring our example from above, how would we instantiate an AServiceImpl or BServiceImpl below?

//
// service A
//
trait AService {
  def methodBNeeds: String
  def methodINeedB: String
}

class AServiceImpl(bService: BService) extends AService {
  def methodBNeeds: String = "from A"
  def methodINeedB: String = bService.methodANeeds + "!"
}

//
// service B
//
trait BService {
  def methodANeeds: String
  def methodINeedA: String
}

class BServiceImpl(aService: AService) extends BService {
  def methodANeeds: String = "from B"
  def methodINeedA: String = aService.methodBNeeds + "?"
}

Obviously, it can’t be done, at least directly. Guice suggests selectively introducing dependency Providers (which lazily provide the dependency on demand via a method call) as one workaround, but we too quickly dismissed that option as we didn’t realize that Guice would create them for us automatically. Figuring out where to put them and updating all the dependent code to use them would have been more work, and would have left our code uglier, than the ultimate solution we ended up using.

So, before we discovered other hidden magic in Guice, we thought our only option was to plunge ahead with an even BIGGER refactoring of our app to eliminate the circular dependencies. While that certainly had merits in its own right, given the size of our app it would entail a much larger effort and cost than we’d budgeted for the Play upgrades (to 2.4 and beyond).

We in fact completed a minimal but somewhat artificial refactoring that eliminated the circular dependencies. For this we split each service into a core service that depended only on its repository and, where necessary, an aggregate service that depended on the core service and other core services (but not other aggregate services). We were fortunate in that this approach mostly eliminated the cycles, with the few remaining being unidirectional dependencies between aggregate services.

Around the same time, we discovered the hidden “magic” in Guice: its ability to automatically detect circular dependencies and transparently “fix” them by creating and injecting magical proxy objects strategically into the dependency graph. These proxies automatically forward method calls to the proxied class, and so can be substituted for them without altering functionality — assuming that your injected constructor parameters are traits/interfaces rather than concrete classes. For a Guiceified implementation of the problematic example above, you can see the proxy Guice created for BServiceImpl‘s AService dependency in the right image below (both from IntelliJ’s debug window):

BServiceImpl directly into AServiceImpl
BServiceImpl injected into AServiceImpl

AServiceImpl proxy injected into BServiceImpl
AServiceImpl proxy injected into BServiceImpl

There were a couple reasons we didn’t discover this magic earlier. First, we had some other issues that were breaking the dependency graph creation, but we misunderstood the resulting error messages to mean that circular dependencies were the problem. Second, surprisingly the Guice documentation itself doesn’t mention this on-by-default magic anywhere, even on the page specifically about circular dependencies. But you can find references to it on the web and can find it by looking into the Guice source code.

With the Guice magic discovered and enabled, we shelved the artificial refactoring since it was cumbersome to work with (with the functionality of each former service being artificially split into two new services) and were able to start using Guice-enabled Play 2.4.

There was one more small hurdle to clear when we upgraded to Play 2.5, namely the Guice magic being disabled rather than enabled by default. Fortunately this was easy to enable via a custom application loader:

package util.play

import play.api.ApplicationLoader
import play.api.inject.guice.{GuiceApplicationBuilder, GuiceApplicationLoader}

class CustomApplicationLoader extends GuiceApplicationLoader {
  override protected def builder(context: ApplicationLoader.Context): GuiceApplicationBuilder = {
    super
      .builder(context)
      .disableCircularProxies(false)
  }
}
Looking forward

We’ve now been cake-free for a few months and are very much enjoying the healthier diet and cleaner code, though of course we still have the underlying cyclic dependency graph due to our poorly separated services.

In order to properly address that issue longer term, we first plan to develop an updated domain model decomposed more cleanly into independent, cohesive pieces (or Bounded Contexts). Given the rapid growth of both our application and our development team, most likely we’ll then implement the refactored service boundaries over time as new, independent microservices.

Leave a Reply