ZIO ZLayer: A simple “Hello, world” example (dependency injection, services)

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 software, 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.

More resources

Here are a few resources that are related to this ZIO ZLayer tutorial: