As a brief note today, here is some source code for a ZIO ZLayer
application using Scala 3. In this code I use the ZLayer
framework to handle some dependency injection for a small application. (Note that I don’t like to use the word “simple” when writing about computer programming, but I have tried to make this as simple as I can.)
I’ve commented the code below as multiple “parts” so you can see the thought process of creating an application that uses ZLayer
. Basically the idea is that your application needs some sort of service — which might be like a database connection pool, HTTP framework, etc. — and then you make that service available to your application with ZLayer
’s provideLayer
function (or one of its other functions).
The ZLayer example
Given that small introduction, here’s my ZIO ZLayer
example, with many notes shown in the comments inside the code:
//> using scala "3"
//> using lib "dev.zio::zio::2.0.22"
import zio.*
import java.io.IOException
// -------------
// PART 1: MODEL
// -------------
case class User(id: String, name: String)
// -------------
// PART 2: TRAIT
// -------------
trait UserService:
def getUser(id: String): Task[User] //Task[User] == ZIO[Any, Throwable, User]
// --------------------------------------------------------
// PART 3a: "LIVE" SERVICE (an implementation of the trait)
// --------------------------------------------------------
class LiveUserService extends UserService:
def getUser(id: String): Task[User] =
// this could be getting a user from a Production database
// like MySQL, Postgres, etc.
ZIO.succeed(User(id, s"Name for $id (LIVE)"))
object LiveUserService:
// ULayer[+ROut] == ZLayer[Any, Nothing, ROut]
val layer: ULayer[UserService] = ZLayer.succeed(LiveUserService())
// -------------------------------------------------------------
// PART 3b: "TEST" SERVICE (another implementation of the trait)
// -------------------------------------------------------------
class TestUserService extends UserService:
def getUser(id: String): ZIO[Any, Throwable, User] =
// getting a user from a Test data store
ZIO.succeed(User(id, s"Name for $id (TEST)"))
object TestUserService:
val layer: ULayer[UserService] = ZLayer.succeed(TestUserService())
// --------------------------------
// PART 4: THE APPLICATION & WIRING
// --------------------------------
object MainApp extends ZIOAppDefault:
// IOException is here because `printLine` has this return type: `IO[IOException, Unit]`
// note: IO[IOException, A] == ZIO[Any, IOException, A])
// Console: https://zio.dev/api/zio/console$ (returns IOException)
// Console: https://zio.dev/reference/services/console
val program: ZIO[UserService, Throwable, Unit] = for
// ---------------------------------------------
// PART 4a: CREATING & USING THE DESIRED SERVICE
// ---------------------------------------------
user <- ZIO.service[UserService].flatMap(svc => svc.getUser("123"))
_ <- Console.printLine(s"User found: ${user.name}")
yield
()
// ------------------------
// PART 4b: THE WIRING PART
// ------------------------
// can be `provideLayer` or `provide` here
override def run: ZIO[Scope, Any, Any] =
program.provideLayer(LiveUserService.layer) // TestUserService in TEST
// DevUserService in DEV
Discussion
An important part about this ZLayer
solution is that not only can you have a LiveUserService
for your PRODUCTION environment, but you also have a TestUserService
for your TEST environment, and a DevUserService
for your DEVELOPMENT environment. As I show in the final comments, with the ZLayer
framework you can easily plug in the environment you need.
A key to this sort of solution is the use of a base trait that defines the API for your service, and in this case that trait is named UserService
. Starting with a base trait and then having concrete classes and objects that implement the trait is part of Scala’s modular or module programming approach.
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |
More resources
Here are a few resources that are related to this ZIO ZLayer tutorial: