Cake antipattern

A long time ago in the land of Scala emerged a new type-safe way of dependency injection. In the long run, it brings more trouble than it is worth.

Cake pattern

Let us recall what the cake pattern is. To understand it, we need to know self-types. They were intended to be used as mixins and look more or less like that:

trait UserRepository {
  def fetchUsers(userIds: Seq[Long]): Seq[User]
}

class UserRepositoryDbImpl(database: Database)
    extends UserRepository {
  def fetchUsers(userIds: Seq[Long]): Seq[User] = ...
}

trait UserRepositoryLogging { self: UserRepository =>
  
  private lazy val logger: Logger = ...
  
  override def fetchUsers(userIds: Seq[Long]): Seq[User] = {
    val users = super.fetchUsers(usersIds)
    logger.debug(s"""Fetched users by ids: ${users.mkString(", ")}""")
    users
  }
}

val userRepository: UserRepository =
  new UserRepositoryDbImpl(database) with UserRepositoryLogging

Here we can add the trait UserRepositoryLogging to anything that is a UserRepository implementation - it would not compile otherwise. Additionally, inside UserRepositoryLogging we are assuming that it is the implementation of UserRepository. So everything accessible within UserRepository is also accessible there.

Now, since we can access whatever members are declared by the type(s) used as the self-type, we are allowed to do this:

trait UserRepository {
  def fetchUsers(userIds: Seq[Long]): Seq[User]
}

trait UserRepositoryComponent {
  def userRepository: UserRepository
}

trait UserRepositoryComponentImpl
    extends UserRepositoryComponent {
  lazy val userRepository: UserRepository = new UserRepository {
    // ...
  }
}

trait SomeController { self: UserRepositoryComponent =>
  
  def handleRequest(userIds: Seq[Long]): Response = {
    // ...
    userRepository.fetchUsers(userIds)
    // ...
  }
}

object SomeController
  extends SomeController
  with UserRepositoryComponentImpl

Within SomeController we declare a self-type that ensures its instantiable implementation will have the userRepository method. By adding the mixin, we provide the implementation and thus ensure that the dependency is injected at compile time. This works without runtime reflection, with type safety, and requires no additional configuration or libraries.

Each such component could be a layer of our application (business logic layer, the infrastructure layer, etc), which someone compared to the layers of a cake. So, by creating your application in such a way you are effectively baking a cake. Thus, the cake pattern.

Now… why is that a problem?

Adding dependencies

In bigger projects, the number of services, repositories, and utilities will surely be greater than one or two. So your cake will start looking like:

object PaymentTransactionApiController
  extends TransactionApiController
  with ConfigComponentImpl
  with DatabaseComponentImpl
  with UserRepositoryComponentImpl
  with SessionRepositoryComponentImpl
  with SecurityServicesComponentImpl
  with ExternalPaymentApiServicesComponentImpl
  with PaymentServicesComponentImpl
  with TransactionServicesComponentImpl

As a matter of fact, I once saw a cake where ComponentImpl went through two or three screens. You weren’t injecting only the components needed at the time; you were combining everything together at once: infrastructure, domain services, persistence, business logic…

Now, imagine what happens when you need to add a dependency. You start by adding another with Dependency to your self-type. Then, you check where the updated trait is used. You possibly have to add the self-type here as well. You climb up the tree of mixins until you reach a class or an object. Uff.

Except that this time you added this yourself, so you kind of know what needs to be added. But when you are doing a rebase or resolving a conflict, you might end up in a situation where a new dependency appears that you do not have. The compiler error only says that self-type X does not conform to Y. With 50+ components, you might as well guess which one is failing. (Or start doing a binary search by removing components until the error disappears).

As the cake’s size and complexity grow further one might want to follow the DRY principle and split it into smaller cakes that will be put together later on. This uncovers new depths of hell.

Removing dependencies

With normal DI, when you stop using some object, your IDE/compiler can tell you that it is no longer needed, so you might remove it. With the cake pattern, you will not be informed about such things.

As a result, cleanup is much more difficult and it is quite possible that at some moment you will end up with many more dependencies than you really need.

Compile time

All of that adds up to compile time. It might not be obvious at first glance, but you can end up with:

trait ModuleAComponentImpl { self: ModuleBComponent => }
trait ModuleBComponentImpl { self: ModuleAComponent => }

which is a cyclic dependency. (We all know they are bad, but you might not even notice that you just created one. Especially if you initialize everything lazily).

So those two traits will be very often compiled together, and the Zinc incremental compiler will not help with that. Additionally, changes to any of the components in a cake will trigger recompilation of stuff above it in the dependency graph.

Testing

Cyclic dependencies create another issue. You might very well end up with something like:

trait AComponentImpl { self: BComponent =>
  lazy val a: A = new A {
    b.someMethod() // use b
  }
}

trait BComponentImpl { self: AComponent =>
  lazy val b: B = new B {
    a.someMethod() // use a
  }
}

object Cake extends AComponentImpl with BComponentImpl
Cake.a // NPE!
Cake.b // NPE!

Each component will be in its own file, so you won’t notice this dependency. Tests with a mock won’t help you with that:

trait BComponentMock { val b: B = mock[B] }

val aComponent = new AComponentImpl with BComponentMock {}
aComponent.a // works!

so it will be all fine and dandy until you actually run the code. You know - the code is type-safe, which means that the value you construct will follow type constraints. But these constraints mean nothing as Nothing (i.e., a thrown exception) is a perfectly valid subtype of your required type.

Initialization order

The previous problem is related to this one: what is the order of the component initialization?

With normal DI, it is obvious - before you put one thing into the other you need to create it. So you just look at the order in which you create objects.

With a cake you can forget about it. Values within components will often be lazy vals or objects. So the first accessed attribute will try to instantiate its dependencies, which will try to instantiate its dependencies, etc.

Even if we go with vals, it’s a matter of the order in which we composed the cake - you know, trait linearization. As long as each component is used exactly once - no problem. But if you DRYed your cake and some components repeat when you merge them…

In a perfect world, such things as initialization order should not matter, but quite often they do, and we would like to know e.g., when some connection to the DB or subscription to a Kafka topic started, and log things in order to debug potential failures.

Boilerplate and lock-in

As you probably noticed, this pattern generates a lot of boilerplate - basically each repository or service would have to have a wrapper, only to annotate the dependencies.

Once you go down that road, you’ll notice that it is quite difficult to get rid of the cake pattern. Each implementation is put inside a trait, and its dependencies come from the scope, not via parameters. So for each class, you would have to perform a refactor first in order to be able to instantiate it without the wrapper.

But then the dependencies would also have to be refactored and so on. The cake pattern is an ugly commitment that aches and opposes you when you try to withdraw from it gradually.

Literally, any other form of DI in Scala that I know (runtime reflection à la Guice, macro annotations à la MacWire, implicits, combinations of those three) allows you to say stop! at any moment, and gradually switch to another solution.

Summary

The cake pattern is a solution to a DI problem that, in the long run, does more harm than good. From what I heard it was used in the Scala compiler, and its authors regret it. It was the default DI mechanism for the Play Framework, but because of all these issues the authors decided to switch to Guice - yes, functional programmers preferred runtime reflection to type-safe cake, so that tells a lot.

So, what are the alternatives?

  • plain old manual argument passing - no need to explain that
  • implicits - has its advantages until your project is maintained by more than just yourself, and coworkers start complaining about not knowing what is happening, slow compilation, and not understanding implicits…
  • runtime reflection - Guice is a mature framework doing DI in exactly the way that I hate. It was used in many projects, Java programmers love it - just like they love Spring. Some of them let the abstraction leak by passing the injector into the class. Some of them want their configuration to be flexible - so flexible that the class wiring might fail at runtime, bleh!
  • MacWire - halfway between passing arguments manually and using macros for everything - it scans the current scope at compile time and injects dependencies for you. If some are missing, compilation fails. Meanwhile, you just write wire[ClassName]
  • Airframe - macro-based framework where you build a recipe for DI with a DSL. It has support for object lifecycle management.
  • pulp - my shameless auto-promotion. It uses implicits and macro annotations in order to generate providers for classes.

IMHO each of them should work out better than the cake pattern.