ZIO FAQ: How do I create a very simple ZLayer
in a ZIO 2 application?
Solution
As a wee bit of background, the ZIO Zlayer
approach provides several important purposes, including:
- Dependency injection
- Modularity and composability
- Resource management
- Testability
- Separation of concerns
- Type safety
At its most basic, the ZIO 2 ZLayer
gives you a way to provide configuration information to your application, similar to dependency injection approaches with other languages and tools.
A simple ZLayer example
For instance, imagine that your application requires configuration information such as (a) SQLite database configuration information and (b) an email address. One way to start solving this problem is to define a Scala case class for each piece of configuration information:
// case classes for our properties
case class EmailConfig(address: String)
case class SQLiteConfig(url: String)
After that, create ZLayer
values for these two pieces of information:
val sqliteLive: ULayer[SQLiteConfig] =
ZLayer.succeed(SQLiteConfig("jdbc:sqlite:./stocks.sqlite"))
val emailLive: ULayer[EmailConfig] =
ZLayer.succeed(EmailConfig("coyote@acme.com"))
(Note that I create these as val
values, but they can also be functions, as you’ll see later in this article.)
Then all you need to do is to “provide” your ZLayer
values to your application. Here’s a small but complete ZIO 2 application named app
that uses these two pieces of configuration information that are provided to it in the run
value:
object ZioZLayerSimplest101 extends ZIOAppDefault:
val app: ZIO[EmailConfig & SQLiteConfig, Throwable, Unit] = for
sqLiteConfig <- ZIO.service[SQLiteConfig] // receive this value
emailConfig <- ZIO.service[EmailConfig] // receive this one, too
_ <- Console.printLine(s"SQLite URL: ${sqLiteConfig.url}")
_ <- Console.printLine(s"EMAIL: ${emailConfig.address}")
yield
()
// this is where the layers are “provided” to the application
// with the `ZIO::provide` method:
val run =
app.provide(sqliteLive ++ emailLive)
.exitCode
The complete application
Here’s all that same information in one complete application:
package _zlayer_101
import zio.*
// case classes for our properties
case class EmailConfig(address: String)
case class SQLiteConfig(url: String) // "jdbc:sqlite:./stocks.sqlite"
// the configuration values our app needs
val sqliteLive: ULayer[SQLiteConfig] =
ZLayer.succeed(SQLiteConfig("jdbc:sqlite:./stocks.sqlite"))
val emailLive: ULayer[EmailConfig] =
ZLayer.succeed(EmailConfig("coyote@acme.com"))
/**
* https://zio.dev/reference/contextual/zlayer
*/
object ZioZLayerSimplest101 extends ZIOAppDefault:
val app: ZIO[EmailConfig & SQLiteConfig, Throwable, Unit] = for
sqLiteConfig <- ZIO.service[SQLiteConfig]
emailConfig <- ZIO.service[EmailConfig]
_ <- Console.printLine(s"SQLite URL: ${sqLiteConfig.url}")
_ <- Console.printLine(s"EMAIL: ${emailConfig.address}")
yield
()
val run =
app.provide(sqliteLive ++ emailLive)
.exitCode
In this application, notice how the sqliteLive
and emailLive
configuration values are “provided” to the app
application from the run
value. Here I assume that you know how ZIO works and only want to know about ZLayer
, but a key point to know is that every ZIO application starts with the run
value (where run
can be a value or a function).
Another approach that’s closer to the real world
Next, here’s a very similar example that’s a little closer to what you’ll be doing in the real world. In this application I don’t define the actual ZLayer
values within the application, and instead define them in the run
value:
package _zlayer_101
import zio.*
// case classes for our properties
case class EmailConfig(address: String)
case class SQLiteConfig(url: String) // "jdbc:sqlite:./stocks.sqlite"
def sqliteLive(url: String): ULayer[SQLiteConfig] =
ZLayer.succeed(SQLiteConfig(url))
def emailLive(address: String): ULayer[EmailConfig] =
ZLayer.succeed(EmailConfig(address))
/**
* https://zio.dev/reference/contextual/zlayer
*/
object ZioZLayerSimplest101 extends ZIOAppDefault:
val app: ZIO[EmailConfig & SQLiteConfig, Throwable, Unit] = for
sqLiteConfig <- ZIO.service[SQLiteConfig]
emailConfig <- ZIO.service[EmailConfig]
_ <- Console.printLine(s"SQLite URL: ${sqLiteConfig.url}")
_ <- Console.printLine(s"EMAIL: ${emailConfig.address}")
yield
()
// i now provide the data for the ZLayer values here,
// rather than in the variables inside the application
val run =
app.provide(
sqliteLive("jdbc:sqlite:./stocks.sqlite") ++ emailLive("coyote@acme.com")
).exitCode
This still isn’t a complete “real world” application, because (a) you’ll typically handle the ZLayer
values a little differently inside your application, and (b) you’ll typically get your configuration information from configuration files or from your runtime environment, but this example does take you on the next step of your ZLayer
journey.
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |
For information on how to use configuration files with ZLayer
, see my other tutorials:
- ZIO/ZLayer FAQ: How to use a Java Properties files with ZIO
- ZIO/ZLayer FAQ: How to use a Typesafe Config HOCON properties file with ZIO
- Video: How to create a small example ZIO 2 application
In summary, I hope these ZIO 2 and ZLayer
tutorials are helpful!