Skip to main content
Version: 2.x

Examples

An Example of a ZIO Application with Multiple Config Layers

In the following example, we have an application that requires AppConfig layer, which itself requires DBConfig and ServerConfig layers:

import zio._

case class ServerConfig(host: String, port: Int)
object ServerConfig {
val layer: ULayer[ServerConfig] =
ZLayer.succeed(ServerConfig("localhost", 8080))
}

case class DBConfig(name: String)
object DBConfig {
val layer: ULayer[DBConfig] =
ZLayer.succeed(DBConfig("my-test-db"))
}

case class AppConfig(db: DBConfig, serverConfig: ServerConfig)
object AppConfig {
val layer: ZLayer[DBConfig with ServerConfig, Nothing, AppConfig] =
ZLayer {
for {
db <- ZIO.service[DBConfig]
server <- ZIO.service[ServerConfig]
} yield AppConfig(db, server)
}
}

object MainApp extends ZIOAppDefault {
val myApp =
for {
c <- ZIO.service[AppConfig]
_ <- ZIO.debug(s"Application started with config: ${c}")
} yield ()

def run = myApp.provide(AppConfig.layer, DBConfig.layer, ServerConfig.layer)
}

An Example of Manually Generating a Dependency Graph

Suppose we have defined the UserRepo, DocumentRepo, Database, BlobStorage, and Cache services and their respective implementations as follows:

import zio._

case class User(email: String, name: String)

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

def get(email: String): Task[User]
}

object UserRepo {
def save(user: User): ZIO[UserRepo, Throwable, Unit] =
ZIO.serviceWithZIO(_.save(user))

def get(email: String): ZIO[UserRepo, Throwable, User] =
ZIO.serviceWithZIO(_.get(email))
}

case class UserRepoLive(cache: Cache, database: Database) extends UserRepo {
override def save(user: User): Task[Unit] = ???

override def get(email: String): Task[User] = ???
}

object UserRepoLive {
val layer: URLayer[Cache & Database, UserRepo] =
ZLayer {
for {
cache <- ZIO.service[Cache]
database <- ZIO.service[Database]
} yield UserRepoLive(cache, database)
}
}

trait Database

case class DatabaseLive() extends Database

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

trait Cache {
def save(key: String, value: Array[Byte]): Task[Unit]

def get(key: String): Task[Array[Byte]]

def remove(key: String): Task[Unit]
}

class InmemeoryCache() extends Cache {
override def save(key: String, value: Array[Byte]): Task[Unit] = ???

override def get(key: String): Task[Array[Byte]] = ???

override def remove(key: String): Task[Unit] = ???
}

object InmemoryCache {
val layer: ZLayer[Any, Throwable, Cache] =
ZLayer(ZIO.attempt(new InmemeoryCache).debug("initialized"))
}

class PersistentCache() extends Cache {
override def save(key: String, value: Array[Byte]): Task[Unit] = ???

override def get(key: String): Task[Array[Byte]] = ???

override def remove(key: String): Task[Unit] = ???
}

object PersistentCache {
val layer: ZLayer[Any, Throwable, Cache] =
ZLayer(ZIO.attempt(new PersistentCache).debug("initialized"))
}

case class Document(title: String, author: String, body: String)

trait DocumentRepo {
def save(document: Document): Task[Unit]

def get(id: String): Task[Document]
}

object DocumentRepo {
def save(document: Document): ZIO[DocumentRepo, Throwable, Unit] =
ZIO.serviceWithZIO(_.save(document))

def get(id: String): ZIO[DocumentRepo, Throwable, Document] =
ZIO.serviceWithZIO(_.get(id))
}

case class DocumentRepoLive(cache: Cache, blobStorage: BlobStorage) extends DocumentRepo {
override def save(document: Document): Task[Unit] = ???

override def get(id: String): Task[Document] = ???
}

object DocumentRepoLive {
val layer: ZLayer[Cache & BlobStorage, Nothing, DocumentRepo] =
ZLayer {
for {
cache <- ZIO.service[Cache]
blobStorage <- ZIO.service[BlobStorage]
} yield DocumentRepoLive(cache, blobStorage)
}
}

trait BlobStorage {
def store(key: String, value: Array[Byte]): Task[Unit]
}

case class BlobStorageLive() extends BlobStorage {
override def store(key: String, value: Array[Byte]): Task[Unit] = ???
}

object BlobStorageLive {
val layer: URLayer[Any, BlobStorage] =
ZLayer.succeed(BlobStorageLive())
}

And then assume we have the following ZIO application:

import zio._

def myApp: ZIO[DocumentRepo & UserRepo, Throwable, Unit] =
for {
_ <- UserRepo.save(User("john@doe", "john"))
_ <- DocumentRepo.save(Document("introduction to zio", "john", ""))
_ <- UserRepo.get("john@doe").debug("retrieved john@doe user")
_ <- DocumentRepo.get("introduction to zio").debug("retrieved article about zio")
} yield ()

The myApp requires DocumentRepo and UserRepo services to run. So we need to create a ZLayer which requires no services and produces DocumentRepo and UserRepo. We can manually create this layer using vertical and horizontal layer composition:

import zio._

object MainApp extends ZIOAppDefault {

val layers: ZLayer[Any, Any, DocumentRepo with UserRepo] =
(BlobStorageLive.layer ++ InmemoryCache.layer ++ DatabaseLive.layer) >>>
(DocumentRepoLive.layer >+> UserRepoLive.layer)

def run = myApp.provideLayer(layers)
}

An Example of Automatically Generating a Dependency Graph

Instead of creating the required layer manually, we can use the ZIO#provide. ZIO internally creates the dependency graph automatically based on all dependencies provided:

import zio._

object MainApp extends ZIOAppDefault {

def run =
myApp.provide(
InmemoryCache.layer,
DatabaseLive.layer,
UserRepoLive.layer,
BlobStorageLive.layer,
DocumentRepoLive.layer
)

}

An Example of Providing Different Implementations of the Same Service

Let's say we want to provide different versions of the same service to different services. In this example, both UserRepo and DocumentRepo services require the Cache service. However, we want to provide different cache implementations for these two services. Our goal is to provide an InmemoryCache layer for UserRepo and a PersistentCache layer for the DocumentRepo service:

import zio._

object MainApp extends ZIOAppDefault {

val layers: ZLayer[Any, Throwable, UserRepo with DocumentRepo] =
((InmemoryCache.layer ++ DatabaseLive.layer) >>> UserRepoLive.layer) ++
((PersistentCache.layer ++ BlobStorageLive.layer) >>> DocumentRepoLive.layer)

def run = myApp.provideLayer(layers)
}

// Output:
// initialized: zio.examples.PersistentCache@6e899128
// initialized: zio.examples.InmemeoryCache@852e20a

An Example of How to Get Fresh Layers

Having covered the topic of acquiring fresh layers, let's see an example of using the ZLayer#fresh operator.

DocumentRepo and UserRepo services are dependent on an in-memory cache service. On the other hand, let's assume the cache service is quite simple, and we might be prone to cache conflicts between services. While sharing the cache service may cause some problems for our business logic, we should separate the cache service for both DocumentRepo and UserRepo:

import zio._

object MainApp extends ZIOAppDefault {

val layers: ZLayer[Any, Throwable, UserRepo & DocumentRepo] =
((InmemoryCache.layer.fresh ++ DatabaseLive.layer) >>> UserRepoLive.layer) ++
((InmemoryCache.layer.fresh ++ BlobStorageLive.layer) >>> DocumentRepoLive.layer)

def run = myApp.provideLayer(layers)
}

// Output:
// initialized: zio.examples.InmemoryCache@13c9672b
// initialized: zio.examples.InmemoryCache@26d79027

An Example of Pass-through Dependencies

Notice that in the previous examples, both UserRepo and DocuemntRepo have some hidden dependencies, such as Cache, Database, and BlobStorage. So these hidden dependencies are no longer expressed in the type signature of the layers. From the perspective of a caller, layers just outputs a UserRepo and DocuemntRepo and requires no inputs. The caller does not need to be concerned with the internal implementation details of how the UserRepo and DocumentRepo are constructed.

An upstream dependency that is used by many other services can be "passed-through" and included in a layer's output. 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.

The following example shows how to passthrough all dependencies to the final layer:

import zio._

object MainApp extends ZIOAppDefault {

// passthrough all dependencies
val layers: ZLayer[Any, Throwable, Database & BlobStorage & Cache & DocumentRepo & UserRepo] =
DatabaseLive.layer >+>
BlobStorageLive.layer >+>
InmemoryCache.layer >+>
DocumentRepoLive.layer >+>
UserRepoLive.layer

// providing all passthrough dependencies to the ZIO application
def run = myApp.provideLayer(layers)
}

An Example of Updating Hidden Dependencies

One of the use cases of having explicit all dependencies in the final layer is that we can update those hidden layers using ZLayer#update. In the following example, we are replacing the InmemoryCache with another implementation called PersistentCache:

import zio._

object MainApp extends ZIOAppDefault {

def myApp: ZIO[DocumentRepo & UserRepo, Nothing, Unit] =
for {
_ <- ZIO.service[UserRepo]
_ <- ZIO.service[DocumentRepo]
} yield ()

val layers: ZLayer[Any, Throwable, Database & BlobStorage & Cache & DocumentRepo & UserRepo] =
DatabaseLive.layer >+>
BlobStorageLive.layer >+>
InmemoryCache.layer >+>
DocumentRepoLive.layer >+>
UserRepoLive.layer

def run =
myApp.provideLayer(
layers.update[Cache](_ => new PersistentCache)
)
}