ZLayer
A ZLayer[-RIn, +E, +ROut]
describes a layer of an application: every layer in an application requires some services as input RIn
and produces some services as the output ROut
.
ZLayers are:
-
Recipes for Creating Services — They describe how a given dependencies produces another services. For example, the
ZLayer[Logging with Database, Throwable, UserRepo]
is a recipe for building a service that requiresLogging
andDatabase
service, and it produces aUserRepo
service. -
An Alternative to Constructors — We can think of
ZLayer
as a more powerful version of a constructor, it is an alternative way to represent a constructor. Like a constructor, it allows us to build theROut
service in terms of its dependencies (RIn
). -
Composable — Because of their excellent composition properties, layers are the idiomatic way in ZIO to create services that depend on other services. We can define layers that are relying on each other.
-
Effectful and Resourceful — The construction of ZIO layers can be effectful and resourceful, they can be acquired and safely released when the services are done being utilized.
-
Asynchronous — Unlike class constructors which are blocking, ZLayer is fully asynchronous and non-blocking.
For example, a ZLayer[Blocking with Logging, Throwable, Database]
can be thought of as a function that map Blocking
and Logging
services into Database
service:
(Blocking, Logging) => Database
So we can say that the Database
service has two dependencies: Blocking
and Logging
services.
Let's see how we can create a layer:
Creation​
ZLayer
is an alternative to a class constructor, a recipe to create a service. This recipe may contain the following information:
-
Dependencies — To create a service, we need to indicate what other service we are depending on. For example, a
Database
service might needSocket
andBlocking
services to perform its operations. -
Acquisition/Release Action — It may contain how to initialize a service. For example, if we are creating a recipe for a
Database
service, we should provide how theDatabase
will be initialized, via acquisition action. Also, it may contain how to release a service. For example, how theDatabase
releases its connection pools.
In some cases, a ZLayer
may not have any dependencies or requirements from the environment. In this case, we can specify Any
for the RIn
type parameter. The Layer
type alias provided by ZIO is a convenient way to define a layer without requirements.
There are many ways to create a ZLayer. Here's an incomplete list:
ZLayer.succeed
to create a layer from an existing serviceZLayer.succeedMany
to create a layer from a value that's one or more servicesZLayer.fromFunction
to create a layer from a function from the requirement to the serviceZLayer.fromEffect
to lift aZIO
effect to a layer requiring the effect environmentZLayer.fromAcquireRelease
for a layer based on resource acquisition/release. The idea is the same asZManaged
.ZLayer.fromService
to build a layer from a serviceZLayer.fromServices
to build a layer from a number of required servicesZLayer.identity
to express the requirement for a layerZIO#toLayer
orZManaged#toLayer
to construct a layer from an effect
Where it makes sense, these methods have also variants to build a service effectfully (suffixed by M
), resourcefully (suffixed by Managed
), or to create a combination of services (suffixed by Many
).
Let's review some of the ZLayer
's most useful constructors:
From Simple Values​
With ZLayer.succeed
we can construct a ZLayer
from a value. It returns a ULayer[Has[A]]
value, which represents a layer of application that has a service of type A
:
def succeed[A: Tag](a: A): ULayer[Has[A]]
In the following example, we are going to create a nameLayer
that provides us the name of Adam
.
val nameLayer: ULayer[Has[String]] = ZLayer.succeed("Adam")
In most cases, we use ZLayer.succeed
to provide a layer of service of type A
.
For example, assume we have written the following service:
object terminal {
type Terminal = Has[Terminal.Service]
object Terminal {
trait Service {
def putStrLn(line: String): UIO[Unit]
}
object Service {
val live: Service = new Service {
override def putStrLn(line: String): UIO[Unit] =
ZIO.effectTotal(println(line))
}
}
}
}
Now we can create a ZLayer
from the live
version of this service:
import terminal._
val live: ZLayer[Any, Nothing, Terminal] = ZLayer.succeed(Terminal.Service.live)
From Managed Resources​
Some components of our applications need to be managed, meaning they undergo a resource acquisition phase before usage, and a resource release phase after usage (e.g. when the application shuts down).
Fortunately, 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.
ZLayer
relies on the powerful ZManaged
data type and this makes this process extremely simple.
We can lift any ZManaged
to ZLayer
by providing a managed resource to the ZLayer.fromManaged
constructor:
val managedFile = ZManaged.fromAutoCloseable(
ZIO.effect(scala.io.Source.fromFile("file.txt"))
)
val fileLayer: ZLayer[Any, Throwable, Has[BufferedSource]] =
ZLayer.fromManaged(managedFile)
Also, every ZManaged
can be converted to ZLayer
by calling ZLayer#toLayer
:
val fileLayer: ZLayer[Any, Throwable, Has[BufferedSource]] = managedFile.toLayer
Let's see another real-world example of creating a layer from managed resources. Assume we have written a managed UserRepository
:
def userRepository: ZManaged[Blocking with Console, Throwable, UserRepository] = for {
cfg <- dbConfig.toManaged_
_ <- initializeDb(cfg).toManaged_
xa <- makeTransactor(cfg)
} yield new UserRepository(xa)
We can convert that to ZLayer
with ZLayer.fromManaged
or ZManaged#toLayer
:
val usersLayer = userRepository.toLayer
// usersLayer: ZLayer[Blocking with Console, Throwable, Has[UserRepository]] = Managed(
// self = zio.ZManaged$$anon$2@7b8f02a3
// )
val usersLayer_ = ZLayer.fromManaged(userRepository)
// usersLayer_: ZLayer[Blocking with Console, Throwable, Has[UserRepository]] = Managed(
// self = zio.ZManaged$$anon$2@1ea831da
// )
Also, we can create a ZLayer
directly from acquire
and release
actions of a managed resource:
def acquire = ZIO.effect(new FileInputStream("file.txt"))
def release(resource: Closeable) = ZIO.effectTotal(resource.close())
val inputStreamLayer = ZLayer.fromAcquireRelease(acquire)(release)
// inputStreamLayer: ZLayer[Any, Throwable, Has[FileInputStream]] = Managed(
// self = zio.ZManaged$$anon$2@69e80340
// )
From ZIO Effects​
We can create ZLayer
from any ZIO
effect by using ZLayer.fromEffect
constructor, or calling ZIO#toLayer
method:
val layer = ZLayer.fromEffect(ZIO.succeed("Hello, World!"))
// layer: ZLayer[Any, Nothing, Has[String]] = Managed(
// self = zio.ZManaged$$anon$2@684ca4c9
// )
val layer_ = ZIO.succeed("Hello, World!").toLayer
// layer_: ZLayer[Any, Nothing, Has[String]] = Managed(
// self = zio.ZManaged$$anon$2@6bd53979
// )
Assume we have a ZIO
effect that read the application config from a file, we can create a layer from that:
def loadConfig: Task[AppConfig] = Task.effect(???)
val configLayer = ZLayer.fromEffect(loadConfig)
// configLayer: ZLayer[Any, Throwable, Has[AppConfig]] = Managed(
// self = zio.ZManaged$$anon$2@34ace338
// )
From another Service​
Every ZLayer
describes an application that requires some services as input and produces some services as output. Sometimes when we are writing a new layer, we may need to access and depend on one or several services.
The ZLayer.fromService
construct a layer that purely depends on the specified service:
def fromService[A: Tag, B: Tag](f: A => B): ZLayer[Has[A], Nothing, Has[B]]
Assume we want to write a live
version of the following logging service:
object logging {
type Logging = Has[Logging.Service]
object Logging {
trait Service {
def log(msg: String): UIO[Unit]
}
}
}
We can create that by using ZLayer.fromService
constructor, which depends on Console
service:
val live: ZLayer[Console, Nothing, Logging] = ZLayer.fromService(console =>
new Service {
override def log(msg: String): UIO[Unit] = console.putStrLn(msg).orDie
}
)
Vertical and Horizontal Composition​
We said that we can think of the ZLayer
as a more powerful constructor. Constructors are not composable, because they are not values. While a constructor is not composable, ZLayer
has a nice facility to compose with other ZLayer
s. So we can say that a Zlayer
turns a constructor into values.
ZLayer
s can be composed together horizontally or vertically:
-
Horizontal Composition — They can be composed together horizontally with the
++
operator. When we compose two layers horizontally, the new layer that this layer requires all the services that both of them require, also this layer produces all services that both of them produces. Horizontal composition is a way of composing two layers side-by-side. It is useful when we combine two layers that they don't have any relationship with each other. -
Vertical Composition — If we have a layer that requires
A
and producesB
, we can compose this layer with another layer that requiresB
and producesC
; this composition produces a layer that requiresA
and producesC
. The feed operator,>>>
, stack them on top of each other by using vertical composition. This sort of composition is like function composition, feeding an output of one layer to an input of another.
Let's get into an example, assume we have these services with their implementations:
trait Logging { }
trait Database { }
trait BlobStorage { }
trait UserRepo { }
trait DocRepo { }
case class LoggerImpl(console: Console.Service) extends Logging { }
case class DatabaseImp(blocking: Blocking.Service) extends Database { }
case class UserRepoImpl(logging: Logging, database: Database) extends UserRepo { }
case class BlobStorageImpl(logging: Logging) extends BlobStorage { }
case class DocRepoImpl(logging: Logging, database: Database, blobStorage: BlobStorage) extends DocRepo { }
We can't compose these services together, because their constructors are not value. ZLayer
can convert these services into values, then we can compose them together.
Let's assume we have lifted these services into ZLayer
s:
val logging: URLayer[Has[Console.Service], Has[Logging]] =
(LoggerImpl.apply _).toLayer
val database: URLayer[Has[Blocking.Service], Has[Database]] =
(DatabaseImp(_)).toLayer
val userRepo: URLayer[Has[Logging] with Has[Database], Has[UserRepo]] =
(UserRepoImpl(_, _)).toLayer
val blobStorage: URLayer[Has[Logging], Has[BlobStorage]] =
(BlobStorageImpl(_)).toLayer
val docRepo: URLayer[Has[Logging] with Has[Database] with Has[BlobStorage], Has[DocRepo]] =
(DocRepoImpl(_, _, _)).toLayer
Now, we can compose logging and database horizontally:
val newLayer: ZLayer[Has[Console.Service] with Has[Blocking.Service], Throwable, Has[Logging] with Has[Database]] = logging ++ database
And then we can compose the newLayer
with userRepo
vertically:
val myLayer: ZLayer[Has[Console.Service] with Has[Blocking.Service], Throwable, Has[UserRepo]] = newLayer >>> userRepo
Layer Memoization​
One important feature of ZIO
layers is that they are shared by default, meaning that if the same layer is used twice, the layer will only be allocated a single time.
For every layer in our dependency graph, there is only one instance of it that is shared between all the layers that depend on it.
If we don't want to share a module, we should create a fresh, non-shared version of it through ZLayer#fresh
.
Updating Local Dependencies​
Given a layer, it is possible to update one or more components it provides. We update a dependency in two ways:
- Using the
update
Method — This method allows us to replace one requirement with a different implementation:
val withPostgresService = horizontal.update[UserRepo.Service]{ oldRepo => new UserRepo.Service {
override def getUser(userId: UserId): IO[DBError, Option[User]] = UIO(???)
override def createUser(user: User): IO[DBError, Unit] = UIO(???)
}
}
- Using Horizontal Composition — Another way to update a requirement is to horizontally compose in a layer that provides the updated service. The resulting composition will replace the old layer with the new one:
val dbLayer: Layer[Nothing, UserRepo] = ZLayer.succeed(new UserRepo.Service {
override def getUser(userId: UserId): IO[DBError, Option[User]] = ???
override def createUser(user: User): IO[DBError, Unit] = ???
})
val updatedHorizontal2 = horizontal ++ dbLayer
Hidden Versus Passed Through Dependencies​
One design decision regarding building dependency graphs is whether to hide or pass through the upstream dependencies of a service. ZLayer
defaults to hidden dependencies but makes it easy to pass through dependencies as well.
To illustrate this, consider the Postgres-based repository discussed above:
val connection: ZLayer[Any, Nothing, Has[Connection]] = connectionLayer
val userRepo: ZLayer[Has[Connection], Nothing, UserRepo] = postgresLayer
val layer: ZLayer[Any, Nothing, UserRepo] = connection >>> userRepo
Notice that in layer
, the dependency UserRepo
has on Connection
has been "hidden", and is no longer expressed in the type signature. From the perspective of a caller, layer
just outputs a UserRepo
and requires no inputs. The caller does not need to be concerned with the internal implementation details of how the UserRepo
is constructed.
To provide only some inputs, we need to explicitly define what inputs still need to be provided:
trait Configuration
val userRepoWithConfig: ZLayer[Has[Configuration] with Has[Connection], Nothing, UserRepo] =
ZLayer.succeed(new Configuration{}) ++ postgresLayer
val partialLayer: ZLayer[Has[Configuration], Nothing, UserRepo] =
(ZLayer.identity[Has[Configuration]] ++ connection) >>> userRepoWithConfig
In this example the requirement for a Connection
has been satisfied, but Configuration
is still required by partialLayer
.
This achieves an encapsulation of services and can make it easier to refactor code. For example, say we want to refactor our application to use an in-memory database:
val updatedLayer: ZLayer[Any, Nothing, UserRepo] = dbLayer
No other code will need to be changed, because the previous implementation's dependency upon a Connection
was hidden from users, and so they were not able to rely on it.
However, if an upstream dependency is used by many other services, it can be convenient to "pass through" that dependency, and include it in the output of a layer. This can be done with the >+>
operator, which provides the output of one layer to another layer, returning a new layer that outputs the services of both layers.
val layer: ZLayer[Any, Nothing, Has[Connection] with UserRepo] = connection >+> userRepo
Here, the Connection
dependency has been passed through, and is available to all downstream services. This allows a style of composition where the >+>
operator is used to build a progressively larger set of services, with each new service able to depend on all the services before it.
lazy val baker: ZLayer[Any, Nothing, Baker] = ???
lazy val ingredients: ZLayer[Any, Nothing, Ingredients] = ???
lazy val oven: ZLayer[Any, Nothing, Oven] = ???
lazy val dough: ZLayer[Baker with Ingredients, Nothing, Dough] = ???
lazy val cake: ZLayer[Baker with Oven with Dough, Nothing, Cake] = ???
lazy val all: ZLayer[Any, Nothing, Baker with Ingredients with Oven with Dough with Cake] =
baker >+> // Baker
ingredients >+> // Baker with Ingredients
oven >+> // Baker with Ingredients with Oven
dough >+> // Baker with Ingredients with Oven with Dough
cake // Baker with Ingredients with Oven with Dough with Cake
ZLayer
makes it easy to mix and match these styles. If you pass through dependencies and later want to hide them you can do so through a simple type ascription:
lazy val hidden: ZLayer[Any, Nothing, Cake] = all
And if you do build your dependency graph more explicitly, you can be confident that layers used in multiple parts of the dependency graph will only be created once due to memoization and sharing.
Cyclic Dependencies​
The ZLayer
mechanism makes it impossible to build cyclic dependencies, making the initialization process very linear, by construction.
Asynchronous Service Construction​
Another important note about ZLayer
is that, unlike constructors which are synchronous, ZLayer
is asynchronous. Constructors in classes are always synchronous. This is a drawback for non-blocking applications. Because sometimes we might want to use something that is blocking the inside constructor.
For example, when we are constructing some sort of Kafka streaming service, we might want to connect to the Kafka cluster in the constructor of our service, which takes some time. So that wouldn't be a good idea to blocking inside a constructor. There are some workarounds for fixing this issue, but they are not perfect as the ZIO solution.
Well, with ZIO ZLayer, our constructor could be asynchronous, and they also can block definitely. And that is because ZLayer
has the full power of ZIO. And as a result, we have strictly more power on our constructors with ZLayer.
We can acquire resources asynchronously or in a blocking fashion, and spend some time doing that, and we don't need to worry about it. That is not an anti-pattern. This is the best practice with ZIO.
Examples​
The simplest ZLayer application​
This application demonstrates a ZIO program with a single dependency on a simple string value:
import zio._
object Example extends zio.App {
// Define our simple ZIO program
val zio: ZIO[Has[String], Nothing, Unit] = for {
name <- ZIO.access[Has[String]](_.get)
_ <- UIO(println(s"Hello, $name!"))
} yield ()
// Create a ZLayer that produces a string and can be used to satisfy a string
// dependency that the program has
val nameLayer: ULayer[Has[String]] = ZLayer.succeed("Adam")
// Run the program, providing the `nameLayer`
def run(args: List[String]): URIO[ZEnv, ExitCode] =
zio.provideLayer(nameLayer).as(ExitCode.success)
}
ZLayer application with dependencies​
In the following example, our ZIO application has several dependencies:
zio.clock.Clock
zio.console.Console
ModuleB
ModuleB
in turn depends upon ModuleA
:
import zio._
import zio.clock._
import zio.console._
import zio.duration.Duration._
import java.io.IOException
object moduleA {
type ModuleA = Has[ModuleA.Service]
object ModuleA {
trait Service {
def letsGoA(v: Int): UIO[String]
}
val any: ZLayer[ModuleA, Nothing, ModuleA] =
ZLayer.requires[ModuleA]
val live: Layer[Nothing, Has[Service]] = ZLayer.succeed {
new Service {
def letsGoA(v: Int): UIO[String] = UIO(s"done: v = $v ")
}
}
}
def letsGoA(v: Int): URIO[ModuleA, String] =
ZIO.accessM(_.get.letsGoA(v))
}
import moduleA._
object moduleB {
type ModuleB = Has[ModuleB.Service]
object ModuleB {
trait Service {
def letsGoB(v: Int): UIO[String]
}
val any: ZLayer[ModuleB, Nothing, ModuleB] =
ZLayer.requires[ModuleB]
val live: ZLayer[ModuleA, Nothing, ModuleB] = ZLayer.fromService { (moduleA: ModuleA.Service) =>
new Service {
def letsGoB(v: Int): UIO[String] =
moduleA.letsGoA(v)
}
}
}
def letsGoB(v: Int): URIO[ModuleB, String] =
ZIO.accessM(_.get.letsGoB(v))
}
object ZLayerApp0 extends zio.App {
import moduleB._
val env = Console.live ++ Clock.live ++ (ModuleA.live >>> ModuleB.live)
val program: ZIO[Console with Clock with ModuleB, IOException, Unit] =
for {
_ <- putStrLn(s"Welcome to ZIO!")
_ <- sleep(Finite(1000))
r <- letsGoB(10)
_ <- putStrLn(r)
} yield ()
def run(args: List[String]) =
program.provideLayer(env).exitCode
}
// output:
// [info] running ZLayersApp
// Welcome to ZIO!
// done: v = 10
ZLayer example with complex dependencies​
In this example, we can see that ModuleC
depends upon ModuleA
, ModuleB
, and Clock
. The layer provided to the runnable application shows how dependency layers can be combined using ++
into a single combined layer. The combined layer will then be able to produce both of the outputs of the original layers as a single layer:
import zio._
import zio.clock._
object ZLayerApp1 extends scala.App {
val rt = Runtime.default
type ModuleA = Has[ModuleA.Service]
object ModuleA {
trait Service {}
val any: ZLayer[ModuleA, Nothing, ModuleA] =
ZLayer.requires[ModuleA]
val live: ZLayer[Any, Nothing, ModuleA] =
ZLayer.succeed(new Service {})
}
type ModuleB = Has[ModuleB.Service]
object ModuleB {
trait Service {}
val any: ZLayer[ModuleB, Nothing, ModuleB] =
ZLayer.requires[ModuleB]
val live: ZLayer[Any, Nothing, ModuleB] =
ZLayer.succeed(new Service {})
}
type ModuleC = Has[ModuleC.Service]
object ModuleC {
trait Service {
def foo: UIO[Int]
}
val any: ZLayer[ModuleC, Nothing, ModuleC] =
ZLayer.requires[ModuleC]
val live: ZLayer[ModuleA with ModuleB with Clock, Nothing, ModuleC] =
ZLayer.succeed {
new Service {
val foo: UIO[Int] = UIO.succeed(42)
}
}
val foo: URIO[ModuleC, Int] =
ZIO.accessM(_.get.foo)
}
val env = (ModuleA.live ++ ModuleB.live ++ ZLayer.identity[Clock]) >>> ModuleC.live
val res = ModuleC.foo.provideCustomLayer(env)
val out = rt.unsafeRun(res)
println(out)
// 42
}