Skip to main content
zio-cats

Dependency Injection

Step 5 of 15

Dependency Injection

ZIO and Cats Effect take fundamentally different approaches to dependency injection.

The Core Difference

Cats Effect
import cats.effect.IO

// Cats Effect: Pass dependencies as parameters
// or use Kleisli/Reader pattern
trait Logger {
  def log(msg: String): IO[Unit]
}

def program(logger: Logger): IO[Unit] =
  logger.log("Hello")

Dependencies as function parameters

ZIO
import zio._

// ZIO: Dependencies in the R type parameter
trait Logger {
  def log(msg: String): UIO[Unit]
}

// Effect requires Logger in environment
val program: ZIO[Logger, Nothing, Unit] =
  ZIO.serviceWithZIO[Logger](_.log("Hello"))

Dependencies encoded in the type system

ZIO Service Pattern

Cats Effect
import cats.effect.IO

// Define trait
trait UserService {
  def getUser(id: Int): IO[Option[User]]
}

// Implementation
class UserServiceLive extends UserService {
  def getUser(id: Int): IO[Option[User]] =
    IO.pure(Some(User(id, "Alice")))
}

// Use directly
val service = new UserServiceLive
service.getUser(1)

Direct instantiation

ZIO
import zio._

// Define trait
trait UserService {
  def getUser(id: Int): UIO[Option[User]]
}

// Companion with accessor
object UserService {
  def getUser(id: Int): ZIO[UserService, Nothing, Option[User]] =
    ZIO.serviceWithZIO[UserService](_.getUser(id))
}

// Implementation as ZLayer
val live: ULayer[UserService] = ZLayer.succeed {
  new UserService {
    def getUser(id: Int) = ZIO.some(User(id, "Alice"))
  }
}

ZLayer for dependency management

Cats Effect: Tagless Final

Cats Effect
import cats.effect.IO
import cats.Monad

// Abstract over effect type F[_]
trait UserService[F[_]] {
  def getUser(id: Int): F[Option[User]]
}

// Companion with summoner
object UserService {
  def apply[F[_]](implicit ev: UserService[F]): UserService[F] = ev
}

// Instance for IO
implicit val ioUserService: UserService[IO] =
  new UserService[IO] {
    def getUser(id: Int) = IO.pure(Some(User(id, "Alice")))
  }

// Use with type class constraint
def program[F[_]: UserService: Monad]: F[String] =
  UserService[F].getUser(1).map(_.fold("Not found")(_.name))

Tagless Final - abstract over F[_]

ZIO
import zio._

// ZIO rarely uses tagless final
// Instead, use the environment pattern

trait UserService {
  def getUser(id: Int): UIO[Option[User]]
}

// The effect type is always ZIO
def program: ZIO[UserService, Nothing, String] =
  ZIO.serviceWithZIO[UserService](_.getUser(1))
    .map(_.fold("Not found")(_.name))

ZIO uses environment (R) instead

Composing Dependencies

Cats Effect
import cats.effect._

trait Database { def query: IO[Int] }
trait Cache { def get: IO[Option[Int]] }

// Pass multiple dependencies
def program(db: Database, cache: Cache): IO[Int] =
  cache.get.flatMap {
    case Some(v) => IO.pure(v)
    case None => db.query
  }

// Or use a case class
case class Deps(db: Database, cache: Cache)

def programWithDeps(deps: Deps): IO[Int] =
  program(deps.db, deps.cache)

Compose via parameters or wrapper

ZIO
import zio._

trait Database { def query: UIO[Int] }
trait Cache { def get: UIO[Option[Int]] }

// Compose with & (intersection type)
val program: ZIO[Database & Cache, Nothing, Int] =
  ZIO.serviceWithZIO[Cache](_.get).flatMap {
    case Some(v) => ZIO.succeed(v)
    case None => ZIO.serviceWithZIO[Database](_.query)
  }

// Layer implementations
val databaseLayer: ULayer[Database] =
  ZLayer.succeed(new Database { def query = ZIO.succeed(42) })
val cacheLayer: ULayer[Cache] =
  ZLayer.succeed(new Cache { def get = ZIO.none })

// Compose layers with ++
val appLayer: ULayer[Database & Cache] =
  databaseLayer ++ cacheLayer

Compose via intersection types and ++

TIP:

ZIO's environment pattern provides compile-time verification that all dependencies are satisfied. Cats Effect relies on the compiler checking function parameters.

Providing Dependencies

Cats Effect
import cats.effect._

trait Config { val port: Int }

def server(config: Config): IO[Unit] =
  IO.println(s"Starting on port ${config.port}")

// Provide at call site
val program: IO[Unit] =
  server(new Config { val port = 8080 })

Pass dependencies at call site

ZIO
import zio._

trait Config { val port: Int }

val server: ZIO[Config, Nothing, Unit] =
  ZIO.serviceWith[Config](c =>
    println(s"Starting on port ${c.port}")
  )

// Provide layer
val program: UIO[Unit] =
  server.provideLayer(
    ZLayer.succeed(new Config { val port = 8080 })
  )

provideLayer eliminates R requirement

Automatic Layer Derivation

ZIO provides ZLayer.derive to automatically generate layers from case class constructors.

Cats Effect
import cats.effect._

trait ServiceA

case class FooService(ref: Ref[IO, Int], a: ServiceA, b: String)

// Cats Effect: No built-in derivation
// Must manually wire dependencies
def makeFooService(
  ref: Ref[IO, Int],
  a: ServiceA,
  b: String
): Resource[IO, FooService] =
  Resource.pure(FooService(ref, a, b))

Manual dependency wiring required

ZIO
import zio._

trait ServiceA

case class FooService(ref: Ref[Int], a: ServiceA, b: String)

object FooService {
  // Automatic derivation - detects all dependencies
  val layer: ZLayer[Ref[Int] & ServiceA & String, Nothing, FooService] =
    ZLayer.derive[FooService]
}

Automatic layer derivation

TIP:

ZLayer.derive automatically detects constructor parameters and their types. It works with ZIO data structures like Ref, Queue, FiberRef, and more.

Error Handling in Layers

Layers can fail during construction. ZIO provides operators to handle these failures.

Cats Effect
import cats.effect._
import scala.concurrent.duration._
import cats.effect.implicits._

// Retry with Resource
val resource =
  RemoteDatabase.resource
    .retry(Schedule.spaced(1.second))

// Fallback pattern
val resilientResource =
  remoteResource.orElse(localResource)

Resource retry and fallback

ZIO
import zio._
import zio.Schedule._

// Retry pattern
val layer = RemoteDatabase.layer.retry(
  Schedule.fibonacci(1.second) && Schedule.recurs(5)
)

// Fallback pattern
val resilientLayer =
  remoteLayer.orElse(localLayer)

ZLayer.retry and orElse

TIP:

Use retry for transient failures (network timeouts) and orElse for fallback implementations (local vs remote).

Next Steps

Dependency injection differs significantly. Now let's look at resource management.

Next: Resource Management →