Cake antipattern

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

Cake pattern

Let us remind what is a cake pattern. 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 trait UserRepositoryLogging to anything that is 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 was declared type(s) used for 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 self-type, that ensures, that its instantiable implementation will have userRepository method. So by adding the mixin we provide the implementation and, so we ensure that dependency is injected at the compile time. Without runtime reflection, with type-safety, no additional configurations or libraries.

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

Now… why is that a problem?

Adding dependencies

In bigger projects, the number of services, repositories and utilities will be surely bigger than 1-2. 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 the fact I saw once a cake where ComponentImpl went through 2-3 screens. You weren’t injecting there components only-those-needed-at-a-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 self-type here as well. You climb up the tree of mixins till you reach a class or an object. Uff.

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

When cake size and the amount grows further one might want to implement DRY rule 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 so you might remove it. With 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 much more dependencies than you really need.

Compile time

All of that adds up to the compile time. It might not be obvious at the 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 2 traits will be very often compiled together, and 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 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 mean that value you will construct will follow type constraints. But these constraints mean nothing as Nothing (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 creates objects.

With a cake you can forget about it. Values within components will be often lazy vals or objects. So 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 order in which we composed cake - you know, trait linearization. As long as each component will be used exactly once - no problem. But if you DRYed you cake, and some components repeat, when you merge them…

In a perfect world such things as initialization order should not matter, but quite often it does, and we would like to know e.g. when some connection to the DB or subscription to 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 wrapper, only for annotate the dependencies.

Once you go down that road you’ll notice that is is quite difficult to get rid of cake pattern. Each implementation is put inside a trait, and its dependencies are coming from the scope, not via parameters. So for each class, you would have to perform 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. 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 a’la Guice, macro annotations a’la MacWire, implicits, combinations of those 3) allows you to say stop! at any moment, and gradually switch to another solution.

Summary

Cake pattern is a solution to a DI problem, that in a long run does more harm than good. From what I heard it was used in Scala compiler, and they regret it. It was the default DI mechanism for Play Framework, but because of all these issues 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 - have its advantages until your project is maintained by more than yourself, and coworkers start complaining about not knowing, what is happening, slow compilation, 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 abstraction leak by passing injector into the class. Some of them want their config to be flexible - so flexible that class wiring might fail at runtime, bleh!
  • MacWire - halfway through passing arguments manually and using macros for everything - it scans current scope in compile time and injects dependencies for you. If some are missing compilation fails. You meanwhile write just wire[ClassName]
  • Airframe - macro-based framework where you build recipe for DI with a DSL. It has support for object lifecycle management
  • pulp - my shameless auto-promotion. It used implicits and macro annotations in order to generate providers for classes

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