Using Scala traits as modules, or the “Thin Cake” Pattern

I would like to describe a pure-Scala approach to modularity that we are successfully using in a couple of our Scala projects.

But let’s start with how we do Dependency Injection (see also my other blogs). Each class can have dependencies in the form of constructor parameters, e.g.:

1
2
3
4
5
class WheatField
class Mill(wheatField: wheatField)
class CowPasture
class DiaryFarm(cowPasture: CowPasture)
class Bakery(mill: Mill, dairyFarm: DairyFarm)

At the “end of the world”, there is a main class which runs the application and where the whole object graph is created:

1
2
3
4
5
6
7
8
9
10
11
12
object BakeMeCake extends App {
     // creating the object graph
     lazy val wheatField = new WheatField()
     lazy val mill = new Mill(wheatField)     
     lazy val cowPasture = new CowPasture()
     lazy val diaryFarm = new DiaryFarm(cowPasture)
     lazy val bakery = new Bakery(mill, dairyFarm)
 
     // using the object graph
     val cake = bakery.bakeCake()
     me.eat(cake)
}

The wiring can be done manually, or e.g. using MacWire.

Note that we can do scoping using Scala constructs: a lazy val corresponds to a singleton object (in the constructed object graph), a def to a dependent-scoped object (a new instance will be created for each usage).

Thin Cake pattern

What if the object graph, and at the same time the main class, becomes large? The answer is simple: we have to break it into pieces, which will be the “modules”. Each module is a Scala trait, and contains some part of the object graph.

For example:

1
2
3
4
5
6
7
8
9
trait CropModule {
     lazy val wheatField = new WheatField()
     lazy val mill = new Mill(wheatField)     
} 
 
trait LivestockModule {
     lazy val cowPasture = new CowPasture()
     lazy val diaryFarm = new DiaryFarm(cowPasture)
}

The main object then becomes a composition of traits. This is exactly what also happens in the Cake Pattern. However here we are using only one element of it, hence the “Thin Cake” Pattern name.

1
2
3
4
5
6
object BakeMeCake extends CropModule with LivestockModule {
     lazy val bakery = new Bakery(mill, dairyFarm)
 
     val cake = bakery.bakeCake()
     me.eat(cake) 
}

If you have ever used Google Guice, you may see a similarity: trait-modules directly correspond to Guice modules. However, here we gain the additional type-safety and compile-time checking that dependency requirements for all classes are met.

Of course, the module trait can contain more than just new object instantiations, however you have to be cautious not to put too much logic in there – at some point you probably need to extract a class. Typical code that also goes into modules is e.g. new actor creation code and setting up caches.

Dependencies

What if our trait modules have inter-module dependencies? There are two ways we can deal with that problem.

The first is abstract members. If there’s an instance of a class that is needed in our module, we can simply define it as an abstract member of the trait-module. This abstract member has to be then implemented in some other module with which our module gets composed in the end. Using a consistent naming convention helps here. The fact that all abstract dependencies are defined at some point is checked by the compiler.

The second way is composition via inheritance. If we e.g. want to create a bigger module out of three smaller modules, we can simply extend the other module-traits, and due to the way inheritance works we can use all of the objects defined there.

Putting the two methods together we get for example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// composition via inheritance: bakery depends on crop and livestock modules
trait BakeryModule extends CropModule with LivestockModule {
     lazy val bakery = new Bakery(mill, dairyFarm)
}   
 
// abstract member: we need a bakery
trait CafeModule {
     lazy val espressoMachine = new EspressoMachine()
     lazy val cafe = new Cafe(bakery, espressoMachine)
 
     def bakery: Bakery
}
 
// the abstract bakery member is implemented in another module
object CafeApp extends CafeModule with BakeryModule {
     cafe.orderCoffeeAndCroissant()
}

Multiple implementations

Taking this idea a bit further, in some situations we might have trait-module-interfaces and several trait-module-implementions. The interface would contain only abstract members, and the implementations would wire the appropriate classes. If other modules depend only on the trait-module-interface, when we do the final composition we can use any implementation.

This isn’t perfect, however. The implementation must be known statically, when writing the code – we cannot dynamically decide which implementations we want to use. If we want to dynamically choose an implementation for only one trait-interface, that’s not a problem – we can use a simple “if”. But every additional combination causes an exponential increase in the cases we have to cover. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
trait MillModule {
     def mill: Mill
}
 
trait CornMillModule extends MillModule { 
     lazy val cornField = new CornField()
     lazy val mill = new CornMill(cornField)
}
 
trait WheatMillModule extends MillModule { 
     lazy val wheatField = new WheatField()
     lazy val mill = new WheatMill(wheatField)
}
 
val modules = if (config.cornPreferred) {
     new BakeryModule with CornMillModule
} else {
     new BakeryModule with WheatMillModule
}

Can it be any better?

Sure! There’s always something to improve :). One of the problems was already mentioned – you cannot choose which trait-module to use dynamically (run-time configuration).

Another area that could get improved is the relation between trait-modules and packages. A good approach is to have a single trait-module per package (or per package tree). That way you logically group code that implements some functionality in a single package, and specify how the classes that form the implementations should be used in the trait-module. But why then do you have to define both the package and trait-module? Maybe they can be merged together somehow? Increasing the role of packages is also an idea I’ve been exploring in the Veripacks project.

It may also be good to restrict the visibility of some of the defined objects. Following the “one public class per package” rule, here we might have “one public object per trait-module”. However, if we are creating bigger trait-modules out of smaller ones, the bigger module has no way to restrict the visibility of the objects in the module it composes of. In fact, the smaller modules would have to know the maximum scope of their visibility and use an appropriate private[package name] modifier (supposing the bigger module is in a parent package).

Summing up

Overall, we found this solution to be a simple, clear way to structure our code and create the object graph. It uses only native Scala constructs, does not depend on any frameworks or libraries, and provides compile-time checking that everything is defined properly.

Bon Appetit!

  • Rafael de Paula Souza

    Hey! What a nice post! I’m working in a scala + play project and a month ago we found the same solution!!!

  • http://fehmicansaglam.net/ Fehmi Can Sağlam

    First of all, thanks for the article.

    Correct me if I am wrong but isn’t your suggestion for Multiple Implementations same as Cake Pattern?

    And I think the usage below is not composition but inheritance.

    // composition: bakery depends on crop and livestock modules
    trait BakeryModule extends CropModule with LivestockModule {
    lazy val bakery = new Bakery(mill, dairyFarm)
    }

  • http://www.warski.org/ Adam Warski

    Even without multiple inheritance, this is similar to the cake pattern. However, in the cake pattern, the modules contain the classes themselves. And you would rather have a single trait-module per class. Here, you have one more per group of classes (e.g. in a package), and the classes never live inside the traits.

    As for composition/inheritance what I mean is “composition by inheritance” :) I’ll amend the post, thanks. With classic single-inheritance you couldn’t compose two classes, with traits its possible.

  • Cédric Brancourt

    “Think Cake” Typo ?

  • http://www.warski.org/ Adam Warski

    Heh, true, fixed, thanks :)

  • parth_patil

    I wonder what are the benefits of DI using abstract members or doing it via self types. Isn’t DI using constructor params both simpler and sufficient ?

  • http://www.warski.org/ Adam Warski

    In the thin cake pattern, self types/abstract members are used to compose modules, that is traits which define parts of the wired object graph.

    In the full cake pattern, self types/abstract members are used implement DI and wire the object graph. It has some advantages, such as easier type abstraction, and disadvantages, such as a large amount of boilerplate.

  • parth_patil

    Hi Adam thanks for your reply but I was not asking why the thin cake pattern is better than the full cake pattern. Instead I was arguing that the DI method you described in the very beginning of the article above, using plain old constructor parameters was a simpler solution and sufficient solution for doing DI. I don’t fully understand what the cake pattern is bringing to the table.

  • http://www.warski.org/ Adam Warski

    It is useful for partitioning the object graph creation into multiple small modules, without creating a huge monolithic class/method at “the end of the world”.

    Once you start to split though, you need some way to express inter-module dependencies. Hence the cake pattern fragments.

  • parth_patil

    Thanks for your reply. I think I am beginning to understand the benefits. I need to use it in my projects to fully grasp its potential. Again thanks for the clarifications and the article.