Revisit Dependency Injection in Scala

Naoki Takezoe
Jul 20, 2019 · 5 min read
Image for post
Image for post

Dependency Injection (DI) is a common pattern in modern software development. In short, DI is wiring objects. It allows us to separate interface and implementation, but more important thing is we can replace objects depending on the situation. The most obvious use case is replacing objects by mock for testing.

There are various DI containers in any programming languages. For example, Spring and Guice are really common in Java. Of course, we have some options in Scala as well. Scala works on JVM and it has high interoperability with Java, so we can use Spring and Guice even in Scala. Actually, Play Framework which is a full-stack web application framework for Scala adopts Guice as DI container. However, since they are designed for Java, they can’t use all of the power of Scala.

Benefits of compile-time DI

As you know, we have more functional approaches for DI in Scala like Reader Monad and more advanced ways. Anyway, the characteristics of them like using only language features and resolving everything at compile-time (also some boilerplates are necessary :-P) are similar to CakePattern. Of course, making it using only language functionality is a great advantage of these approaches and it also shows the power of Scala as a programming language.

We also have Scala native DI library like MacWire.

MacWire is macro that generates instance creation code rather than DI container. So it resolves object dependency at compile-time as same as CakePattern. We call this type of DI as compile-time DI. Compile-time DI is well fit to Scala’s statically typing culture. It resolves everything at compile-time as much as possible to reduce the risk of errors at runtime.

import com.softwaremill.macwire._trait SampleModule {
lazy val userRepository = wire[UserRepository]
lazy val userService = wire[UserService]
}

Compile-time DI is great, but one problem in compile-time DI is that it’s difficult to manage the lifecycle of objects. In the real world applications, initialization and shutting down processes for each object can be necessary for resource allocation and release. Certainly, compile-time DI solves wiring objects, but it doesn’t care about lifecycle management.

Airframe: Another DI container for Scala

Airframe supports constructor injection. It’s very similar to Guice but any annotation is not necessary.

class UserService(userRepository: UserRepository) {
def get(userId: Long): Option[User] = userRepository.get(userId)
}

We can get an instance of this class as follows:

val d: Design = newDesignd.withSession { s: Session =>
val userService = s.build[UserService]
val user = userService.get(1)
}

Airframe creates instances of dependent objects automatically as much as possible. This means that you don’t need to register objects to Airframe explicitly normally. Only if objects can’t be created automatically or if you want to replace these instances with other implementation, you have to register objects explicitly to Design as follows:

val d = newDesign
.bind[UserRepository].to[MockUserRepository]

Airframe also supports in-trait DI. In this case, we have to use a special notation to declare dependent objects in traits. This makes traits depend on Airframe, but we can put together the necessary traits as we need, instead of lining up all necessary objects as constructor parameters.

trait UserService {
val userRepository = bind[UserRepository]
def get(userId: Long): Option[User] = userRepository.get(userId)
}

One advantage of runtime DI is lifecycle management. With constructor injection, we can use annotations to specify lifecycle management methods. These methods are called when an object is created and destroyed. This kind of lifecycle management can be made by managing objects by the container.

trait UserRepository {
@PostConstruct
def init {
...
}

@PreDestroy
def stop {
...
}
}

With in-trait injection, we can do the same things as follows:

trait UserService {
val userRepository = bind[UserRepository]
.onStart { x =>
...
}
.onShutdown { x =>
...
}
)
}

Initialization processes are run in the order of object dependencies, and shutting down processes are run in the opposite. We don’t need to care about the execution order of them.

In my opinion, less configuration and lifecycle management are the main advantages of Airframe although Airframe isn’t compile-time DI. On the other hand, runtime DI may cause some problems like a runtime binding error, unsafe to remove components and runtime overhead.

Mitigate runtime DI problems

[MISSING_DEPENDENCY] Binding for String at AirframeInTraitSample.scala:12 is not found: String <- UserRepository

Also, Airframe provides statistics of components usage so that we can find unused components easily.

[coverage]
design coverage: 50.0%
[unused types]
ItemService
[access stats]
[UserService] init:1, inject:1
[UserRepository] init:1, inject:1

Another interesting feature for the debug is component lifecycle tracing. Airframe has ChromeTracer as a default implementation of Tracer. It generates a JSON file and we can visualize it using Chrome browser so that we can know component dependencies and which component takes a long time to initialize.

Image for post
Image for post

We have many options for Dependency Injection in Scala other than introduced in this article. I don’t say what is the best solution because it highly depends on the various factors like the type of application, the strategy of development and knowledge of the team. However, knowing various options would be a help to make the right decision in your situation.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store