Skip to main content
Version: 2.0.x

ZLayer: Constructor as a Value

Before jumping into the next section, which will explain dependency injection in ZIO, let's take a look at the philosophy behind the ZLayer data type.

In the motivation section, we find out that the ordinary Scala constructors are not powerful enough to help us to build the dependency graph easily. So ZLayer was created to overcome scala constructors' limitations.

We can think of ZLayer as an alternative to constructors but with the following powerful features:

  • Composable with a nice ergonomic API
  • Asynchronous so it doesn't block the thread
  • Effectful and resourceful
  • Support for concurrency and parallelism

Let's see the following example written using scala constructors:

class Editor(formatter: Formatter, compiler: Compiler) {
// ...
}

class Compiler() {
// ...
}

class Formatter() {
// ...
}

We can say that each constructor is a function that takes some arguments as dependencies and returns a new instance of the class:

  • () => Formatter
  • () => Compiler
  • (Formatter, Compiler) => Editor

The ZLayer reifies the conceptual idea of scala constructor and turned it into typed value which is equipped with lots of compositional operators and also supporting asynchronous operations.

So in other words, ZLayer is a type-safe data type that describes the asynchronous, effectful and resourceful process of building the dependency graph. We can say that a ZLayer[Input, E, Output] is a recipe that takes some services as input and returns some services as output.

For example, a ZLayer of type ZLayer[Any, Nothing, Formatter] is a constructor that doesn't take any services from the input and returns Formatter as output. Also, a ZLayer of type ZLayer[Formatter with Compiler, Nothing, Editor] is a constructor that takes Formatter and Compiler services from the input and returns Editor as output:

import zio._

object Formatter {
val layer: ZLayer[Any, Nothing, Formatter] =
ZLayer.succeed(new Formatter())
}

object Compiler {
val layer: ZLayer[Any, Nothing, Compiler] =
ZLayer.succeed(new Compiler())
}

object Editor {
val layer: ZLayer[Formatter with Compiler, Nothing, Editor] =
ZLayer {
for {
formatter <- ZIO.service[Formatter]
compiler <- ZIO.service[Compiler]
} yield new Editor(formatter, compiler)
}
}

Composable Constructors

With scala constructors we compose services like the below to create the dependency graph:

val formatter = new Formatter()
val compiler = new Compiler()
val editor = new Editor(formatter, compiler)

While Scala constructors are a type of Scala function. Composable functions in Scala are not as ergonomic as ZLayer for constructing dependency graphs.

With ZLayer we can compose them using operators like ++ and >>>:

val editor: ZLayer[Formatter with Compiler, Nothing, Editor] = 
(Formatter.layer ++ Compiler.layer) >>> Editor.layer

Also, we can compose Formatter and Editor layers to create a new layer that takes the Compiler service and returns the Editor service:

val editor: ZLayer[Compiler, Nothing, Editor] =
Formatter.layer >>> Editor.layer

Effectful Constructors

If we have a dependency that requires an effectful computation to be initialized, we can't model easily such an operation using ordinary Scala constructors.

In the following example, without the help of ZIO#flatMap or ZLayer, we can't easily create an instance of the Editor class:

import zio._

case class Counter(ref: Ref[Int]) {
def inc: UIO[Unit] = ref.update(_ + 1)
def dec: UIO[Unit] = ref.update(_ - 1)
def get: UIO[Int] = ref.get
}

object Counter {
// Effectful constructor
def make: UIO[Counter] = Ref.make(0).map(new Counter(_))
}

class Editor(formatter: Formatter, compiler: Compiler, counter: Counter) {
// ...
}

object Formatter {
def make = new Formatter()
}

object Compiler {
def make = new Compiler()
}

val editor =
new Editor(
Formatter.make,
Compiler.make,
Counter.make // Compiler Error: Type mismatch: expected: Counter, found: UIO[Counter]
)

Let's see how we can use ZIO#flatMap to create Editor:

val editor: ZIO[Any, Nothing, Editor] =
Counter.make.map { counter =>
new Editor(
Formatter.make,
Compiler.make,
counter
)
}

While with ZLayer, we can easily have an effectful constructor. We can create ZLayer from any ZIO effect by using ZLayer.fromZIO/ZLayer.apply constructor:

case class Counter(ref: Ref[Int]) {
def inc: UIO[Unit] = ref.update(_ + 1)
def dec: UIO[Unit] = ref.update(_ - 1)
def get: UIO[Int] = ref.get
}

object Counter {
val layer: ZLayer[Any, Nothing, Counter] =
ZLayer {
Ref.make(0).map(new Counter(_))
}
}

class Formatter {
def format(code: String): UIO[String] = ???
}

object Formatter {
val layer: ZLayer[Any, Nothing, Formatter] =
ZLayer.succeed(new Formatter())
}

class Compiler {
def compile(code: String): UIO[String] = ???
}

object Compiler {
val layer: ZLayer[Any, Nothing, Compiler] =
ZLayer.succeed(new Compiler())
}

class Editor(formatter: Formatter, compiler: Compiler, counter: Counter) {
def formatAndCompile(code: String): UIO[String] = ???
}

object Editor {
val layer: ZLayer[Formatter with Compiler with Counter, Nothing, Editor] =
ZLayer {
for {
formatter <- ZIO.service[Formatter]
compiler <- ZIO.service[Compiler]
counter <- ZIO.service[Counter]
} yield new Editor(formatter, compiler, counter)
}
}

Let's try another example. Assume we have a ZIO effect that reads the application config from a file, we can create a layer from that:

import zio._

case class AppConfig(poolSize: Int)

object AppConfig {
private def loadConfig : Task[AppConfig] =
ZIO.attempt(???) // loading config from a file

val layer: TaskLayer[AppConfig] =
ZLayer(loadConfig) // or ZLayer.fromZIO(loadConfig)
}

Resourceful Constructors

Some components of our applications need to be scoped, meaning they undergo a resource acquisition phase before usage, and a resource release phase after usage (e.g. when the application shuts down). As we stated before, the construction of ZIO layers can be effectful and resourceful, this means they can be acquired and safely released when the services are done being utilized.

The ZLayer relies on the powerful Scope data type and this makes this process extremely simple. We can lift any scoped ZIO to ZLayer by providing a scoped resource to the ZLayer.scoped constructor:

import zio._

case class A(a: Int)
object A {
val layer: ZLayer[Any, Nothing, A] =
ZLayer.scoped {
ZIO.acquireRelease(acquire = ZIO.debug("Initializing A") *> ZIO.succeed(A(5)))(
release = _ => ZIO.debug("Releasing A")
)
}
}

object ZIOApp extends ZIOAppDefault {
val myApp: ZIO[A, Nothing, Int] =
for {
a <- ZIO.serviceWith[A](_.a)
} yield a * a

def run =
myApp
.debug("result")
.provide(A.layer)
}

The output:

Initializing A
result: 25
Releasing A

We can see that the A service is initialized and carefull released when the application is shut down.

Here is another example that uses auto closeable resources:

import zio._
import scala.io.BufferedSource

val fileLayer: ZLayer[Any, Throwable, BufferedSource] =
ZLayer.scoped {
ZIO.fromAutoCloseable(
ZIO.attemptBlocking(scala.io.Source.fromFile("file.txt"))
)
}

Finally, let's see a real-world example of creating a layer from scoped resources. Assume we have the following UserRepository service:

import zio._
import scala.io.Source._
import java.io.{FileInputStream, FileOutputStream, Closeable}

trait DBConfig
trait Transactor
trait User

def dbConfig: Task[DBConfig] = ZIO.attempt(???)
def initializeDb(config: DBConfig): Task[Unit] = ZIO.attempt(???)
def makeTransactor(config: DBConfig): ZIO[Scope, Throwable, Transactor] = ZIO.attempt(???)

trait UserRepository {
def save(user: User): Task[Unit]
}

case class UserRepositoryLive(xa: Transactor) extends UserRepository {
override def save(user: User): Task[Unit] = ZIO.attempt(???)
}

Assume we have written a scoped UserRepository:

def scoped: ZIO[Scope, Throwable, UserRepository] = 
for {
cfg <- dbConfig
_ <- initializeDb(cfg)
xa <- makeTransactor(cfg)
} yield new UserRepositoryLive(xa)

We can convert that to ZLayer with ZLayer.scoped:

object UserRepositoyLive {
val layer : ZLayer[Any, Throwable, UserRepository] =
ZLayer.scoped(scoped)
}

Asynchronous Constructors

We should avoid using blocking operations inside Scala constructors:

class ProducerInput

class KafkaProducer(input: ProducerInput) {
def send(message: String): Unit = ???
}

object KafkaProducer {
def apply() = {
// Blocking operation, we should avoid it inside constructors
val input = doSomeBlockingOperation()
new KafkaProducer(input)
}

private def doSomeBlockingOperation(): ProducerInput = ???
}

While with ZLayer, we can easily use blocking operations:

import zio._

class ProducerInput

class KafkaProducer(input: ProducerInput) {
def send(message: String): Task[Unit] = ???
}

object KafkaProducer {
val layer =
ZLayer {
for {
input <- ZIO.attemptBlocking(doSomeBlockingOperation())
} yield (new KafkaProducer(input))
}

private def doSomeBlockingOperation(): ProducerInput = ???
}

Parallel Constructors

With Zlayer all layers in the dependency graph are executed in parallel:

import zio._

case class A(a: Int)
object A {
val layer: ZLayer[Any, Nothing, A] =
ZLayer.fromZIO {
for {
_ <- ZIO.debug("Initializing A")
_ <- ZIO.sleep(3.seconds)
_ <- ZIO.debug("Initialized A")
} yield A(1)
}
}

case class B(b: Int)
object B {
val layer: ZLayer[Any, Nothing, B] =
ZLayer.fromZIO {
for {
_ <- ZIO.debug("Initializing B")
_ <- ZIO.sleep(2.seconds)
_ <- ZIO.debug("Initialized B")
} yield B(2)
}
}

object ZIOApp extends ZIOAppDefault {
val myApp: ZIO[A with B, Nothing, Int] =
for {
a <- ZIO.serviceWith[A](_.a)
b <- ZIO.serviceWith[B](_.b)
} yield a + b

def run =
myApp
.debug("result")
.provide(A.layer, B.layer)
}

The output:

Initializing A
Initializing B
Initialized B
Initialized A
result: 3