Preferable you start a ZIO application with just one runtime.unsafeRun.

But when you’re migrating an old application to ZIO, you likely have multiple places to do runtime.unsafeRun. We will investigate how to deal with the environment (layers).

For these examples, we’ll use 2 layers.

AppConfig contains all configuration:

/** Contains all connection configuration for the app */
case class AppConfig(database: String)

object AppConfig {
  def read: AppConfig = {
    println("reading AppConfig")
    AppConfig("databaseHost")
  }
  def make: Task[AppConfig] = ZIO.attempt(read)
  def layer: ZLayer[Any, Throwable, AppConfig] = ZLayer.apply(make)
}

Database keeps a database connection. And closes it.

class Database(config: String) {
  println(s"opening Database connection")
  def write(domain: String): RIO[Any, Unit] = Console.printLine(s"writing $domain")
  def close(): Unit = println("closing Database connection")
}

object Database {
  def acquire: RIO[AppConfig, Database] = ZIO.serviceWith[AppConfig](ac => new Database(ac.database))
  def release(fr: => Database): ZIO[Any, Nothing, Unit] = ZIO.succeedBlocking(fr.close())
  def scope: RIO[Scope with AppConfig, Database] = ZIO.acquireRelease(acquire)(release(_))
  val layer: ZLayer[AppConfig, Throwable, Database] = ZLayer.scoped(scope)

  def write(domain: String): RIO[Database, Unit] = ZIO.serviceWithZIO[Database](_.write(domain))
}

The normal ZIO way would be to provide the layers in the outermost ZIO statement. If you’re in the process of migrating to ZIO, you may want to keep the same top level framework (API, scheduler). Then one by one switch parts of your application to ZIO. During this migration you’re likely to have multiple calls to runtime.unsafe.run or runToFuture. We’re simulating that in this example App by calling run() twice.

object ProvideLayerApp extends App {
  Unsafe.unsafe { implicit unsafe =>
    Console.println("Example app start")
    val runtime: zio.Runtime[Any] = zio.Runtime.default
    run()
    run()
    Console.println("Example app end")

    def domainAction: RIO[Database, Unit] = Database.write("new domain object")

    def run(): Unit = runtime.unsafe.run(
      domainAction.provideLayer(AppConfig.layer >>> Database.layer)
    ).getOrThrowFiberFailure()
  }
}

This will open and close the database and even reread the config for every run:

Example app start
reading AppConfig
opening Database connection
writing new domain object
closing Database connection
reading AppConfig
opening Database connection
writing new domain object
closing Database connection
Example app end

If we want to have one database connection that’s permanently open, we can create the runtime using a ZLayer:

object RuntimeFromLayerApp extends App {
  Unsafe.unsafe { implicit unsafe =>
    Console.println("Example app start")
    val runtime: Runtime.Scoped[Database] = zio.Runtime.unsafe.fromLayer(AppConfig.layer >>> Database.layer)
    run()
    run()
    runtime.unsafe.shutdown()
    Console.println("Example app end")

    def domainAction: RIO[Database, Unit] = Database.write("new domain object")

    def run(): Unit = runtime.unsafe.run(domainAction).getOrThrowFiberFailure()
  }
}

We can manually do runtime.unsafe.shutdown() to close the connection before application shutdown.

Example app start
reading AppConfig
opening Database connection
writing new domain object
writing new domain object
closing Database connection
Example app end

It is possible to create the runtime using one layer, then provide more later:

object RuntimeFromLayerHybrid extends App {
  Unsafe.unsafe { implicit unsafe =>
    Console.println("Example app start")
    val runtime: Runtime.Scoped[AppConfig] = zio.Runtime.unsafe.fromLayer(AppConfig.layer)
    run()
    run()
    runtime.unsafe.shutdown()
    Console.println("Example app end")

    def domainAction: RIO[Database, Unit] = Database.write("new domain object")

    def run(): Unit = runtime.unsafe.run(
      domainAction.provideLayer(Database.layer)
    ).getOrThrowFiberFailure()
  }
}
Example app start
reading AppConfig
opening Database connection
writing new domain object
closing Database connection
opening Database connection
writing new domain object
closing Database connection
Example app end

object UpdateRuntimeApp extends App {
  Unsafe.unsafe { implicit unsafe =>
    Console.println("Example app start")
    val runtime: Runtime.Scoped[AppConfig] = zio.Runtime.unsafe.fromLayer(AppConfig.layer)
    val updatedRuntime: Runtime.Scoped[AppConfig with Database] = runtime.unsafe.run(
      Database.acquire.map(db => runtime.mapEnvironment(_.add(db)))
    ).getOrThrowFiberFailure()
    run()
    run()
    updatedRuntime.unsafe.shutdown()
    Console.println("Example app end")

    def domainAction: RIO[Database, Unit] = Database.write("new domain object")

    def run(): Unit = {
      updatedRuntime.unsafe.run(domainAction).getOrThrowFiberFailure()
    }
  }
}

We can also update the runtime to add extra things. This method doesn’t work so well together with layers. We need to supply an instantiated Database. And this will not close it for us:

Example app start
reading AppConfig
opening Database connection
writing new domain object
writing new domain object
Example app end

This method might be more useful to add something like a UserId. You can also use provideSome for this purpose. Both methods are used in this example:

object UpdateRuntimeAppWithUserId extends App {
  Unsafe.unsafe { implicit unsafe =>
    Console.println("Example app start")
    val runtime: Runtime.Scoped[AppConfig with Database] = zio.Runtime.unsafe.fromLayer(AppConfig.layer ++ (AppConfig.layer >>> Database.layer))
    case class UserId(id: String)
    case class TrackingId(id: Int)
    val userId = UserId("123")
    val updatedRuntime: Runtime.Scoped[Database with UserId] = runtime.mapEnvironment(_.add(userId))
    run(TrackingId(1))
    run(TrackingId(2))
    updatedRuntime.unsafe.shutdown()
    Console.println("Example app end")

    def domainAction: RIO[Database with UserId with TrackingId, Unit] = for {
      u <- ZIO.service[UserId]
      t <- ZIO.service[TrackingId]
      r <- Database.write(s"new domain object for user ${u.id}, trackingId: ${t.id}")
    } yield r

    def run(t: TrackingId): Unit = {
      updatedRuntime.unsafe.run(domainAction.provideSome[Database with UserId](ZLayer.succeed(t))).getOrThrowFiberFailure()
    }
  }
}
Example app start
reading AppConfig
opening Database connection
writing new domain object for user 123, trackingId: 1
writing new domain object for user 123, trackingId: 2
closing Database connection
Example app end

shadow-left