Introduction to Dependency Injection in ZIO
What is a Dependency?
When we implement a service, we might need to use other services. So a dependency is just another service that is required to fulfill its functionality:
class Editor {
val formatter = new Formatter
val compiler = new Compiler
def formatAndCompile(code: String): UIO[String] =
formatter.format(code).flatMap(compiler.compile)
}
What is Dependency Injection?
Dependency injection is a pattern for decoupling the usage of dependencies from their actual creation process. In other words, it is a process of injecting dependencies of service from the outside world. The service itself doesn't know how to create its dependencies.
The following example shows an Editor
service that depends on Formatter
and Compiler
services. It doesn't use dependency injection:
import zio._
class Editor {
private val formatter = new Formatter
private val compiler = new Compiler
def formatAndCompile(code: String): UIO[String] =
formatter.format(code).flatMap(compiler.compile)
}
The Editor
class in the above example is responsible for creating the Formatter
and Compiler
services. The client of the Editor
class doesn't have any control over these services. The client can't use a different implementation for the Formatter
and Compiler
services. So it makes it hard to test the Editor
class.
Let's try to change the above example to use the constructor-based dependency injection pattern:
import zio._
class Editor(formatter: Formatter, compiler: Compiler) {
def formatAndCompile(code: String): UIO[String] = ???
}
In this example, the Editor
service is not responsible for creating its dependencies. Instead, they are expected to be injected from the caller site. The Editor
service does not know how its dependencies are created, they are just injected into its constructor.
So dependency injection is a very simple concept and can be implemented with simple constructs. In a lot of situations, we are not required to use any tools or frameworks.
In the motivation page we explain why applications should use the dependency injection pattern in more detail.
ZIO's Built-in Dependency Injection
ZIO has a full solution to the dependency injection problem. It provides a built-in approach to dependency injection using the following tools in combination together:
ZIO Environment
We use the
ZIO.serviceXYZ
to access services inside the ZIO environment, without having any knowledge of how the services are created or implemented. UsingZIO.serviceXYZ
helps us to decouple our usage of services from the implementation of the services.Consequently, all dependencies will be encoded inside the
R
type parameter of our ZIO application. This specifies which services are required to fulfill the application's functionality.We use the
ZIO.provideXYZ
to provide services to the ZIO environment. This is the opposite operation ofZIO.serviceXYZ
. It allows us to inject all dependencies into the ZIO environment.
ZLayer— We use layers to create the dependency graph that our application depends on.
We can have dependency injection through three simple steps:
- Accessing services from the ZIO environment
- Building the dependency graph
- Providing services to the ZIO environment
We will discuss them in more detail throughout this page.
ZIO's Dependency Injection Features
Dependency injection in ZIO is very powerful, which increases developer productivity. Let's recap some important features of dependency injection in ZIO:
Composable
Composable Environment— Because of the very composable nature of the
ZIO
data type, its environment type parameter is also composable. So when we compose multipleZIO
effects, where each one requires a specific service, we finally get aZIO
effect that requires all the required services that each of the composed effects requires.For example, if we
zip
two effects of typeZIO[A, Nothing, Int]
andZIO[B, Throwable, String]
, the result of this operation will becomeZIO[A with B, Throwable, (Int, String)]
. The result operation requires bothA
andB
services.Composable Dependencies— The
ZLayer
is also composable, as well as ZIO's environment type parameter. So we can compose multiple layers to create a complex dependency graph.
Type-Safe— All the required dependencies should be provided at compile time. If we forget to provide the required services at compile time, we will get a compile error. So if our program compiles successfully, we can be sure that we won't have runtime errors due to missing dependencies.
Effectful— We build dependency graphs using
ZLayer
. SinceZLayer
is effectful, we can create a dependency graph in an effectful way.Resourceful— It also helps us to have resourceful dependencies, where we can manage the creation and release phases of the dependencies.
Parallelism— All dependencies are created in parallel, and will be provided to our application.
Other Frameworks
Using ZLayer
along with the ZIO environment to use dependency injection is optional. While we encourage users to use ZIO's idiomatic dependency injection, it is not mandatory.
We can still use other DI solutions. Here are some other options: