Dependency Injection with Cats-effect Resource Monad

Ihor Vovk
5 min readJun 16, 2023

--

Upd: published the library on Github https://github.com/igor-vovk/cats-effect-simple-di

Cats-effect Resource Monad provides a great monadic abstraction over try-with-resource pattern, e.g. it allows to manage dependency lifecycle including resource closing/finalization when it is not needed anymore (closing connection to a database, freeing cache on shutdown). Together with composability of monads it became a very popular approach for dependencies management, to the extent that Scala libraries like http4s provide their dependencies wrapped in Resource Monad.

Off-topic: one of the big advantages of Resource Monad is that it allows to free resources in the opposite way to how they were instantiated, feature which even monsters like Google Guice doesn’t have

Now every Scala application developer, when creating their application, performs dependency management in some way. There are libraries that help with dependency management, like macwire, but usually using pure Resource monad is enough to perform dependency injection, like in the following simple example of http service:

import cats.effect.*

// repository, two services, and api:
class Repository(conn: ConnectionFactory) {}

class ServiceA(repo: Repository) {}
class ServiceB(repo: Repository) {}

class HttpServerTask(serviceA: ServiceA, serviceB: ServiceB) {
def run: IO[Unit] = ???
}

// Dependency Injection part:
object Dependencies {
private val conn: Resource[IO, ConnectionFactory] = ???

private val repo: Resource[IO, Repository] = for {
conn <- this.conn
} yield new Repository(conn)

private val serviceA: Resource[IO, ServiceA] = for {
repo <- this.repo
} yield new ServiceA(repo)

private val serviceB: Resource[IO, ServiceB] = for {
repo <- this.repo
} yield new ServiceB(repo)

val server: Resource[IO, HttpServerTask] = for {
serviceA <- this.serviceA
serviceB <- this.serviceB
} yield new HttpServerTask(serviceA, serviceB)
}

// Application entry point
object Main extends IOApp.Simple {
def run = Dependencies.server.use(_.run())
}

Now let’s name things. We can say that our Depencies object is a Dependency Injection container, and server is an exit dependency – e.g. dependency that will be used outside of the container. All other dependencies are then an internal dependencies.

Now, this code has a major problem: because of lazy nature of cats-effect, while it is temping to think that conn is a val, in reality it will be executed as many times as it is referenced, twice in this example – which we don't want to, cause then we have 2 connection pools to the same database in a single application. The usual pattern of Dependency Injection is to have only one instance of every dependency, if not specified otherwise for specific cases.

So usually this problem is solved by thinking in a functional way, and first approach that comes into head is to convert all the wiring to a function with big for-comprehension which returns exit dependency, like here:

...

// Here we specify our exit dependencies, wrapping in a case class to add new exit dependendies in the future
case class Dependencies(server: HttpServerTask)

object Dependencies {
val conn: Resource[IO, ConnectionFactory] = ???

def apply: Resource[IO, Dependencies] = for {
conn <- this.conn
repo <- new Repository(conn)
serviceA <- new ServiceA(repo)
serviceB <- new ServiceB(repo)
server <- new HttpServerTask(serviceA, serviceB)
} yield Dependencies(server)

}

This approach is widely used for small applications (example) because of it’s simplicity. It also have some downsides:

  • having one big for-comprehension is not extensible and starts to look badly when it grows with new dependencies included;
  • adding more exit dependencies requires to instantiate all of them, including their graph of dependencies. Suppose we have 2 APIs and 2 entry points (that will be deployed as 2 separate services), using this approach we either need to write 2 separate for-comprehensions or instantiate all of them together and use only 1, which is not optimal to say at least;
  • suppose we want to have a freedom of each dependency to be an exit dependency, e.g. to be able to use each dependency outside of a DI container, something that java libraries like Google Guice easily allow one to do, than this approach doesn’t work at all

So another approach is to go one step back to having separate dependencies, and just try to initialize them but keep track of initialized dependencies shutdown functions, to be able to gracefully shutdown them on the stop of the application.

To accomplish this, we need some helper class called Allocator, that will be responsible for dependency initialization, keeping track over initialized dependencies, and finalizing them:

import cats.effect.*
import cats.effect.unsafe.IORuntime

class Allocator(implicit runtime: IORuntime) {
// Ref that will keep track of finalizers
private val shutdown: Ref[IO, IO[Unit]] = Ref.unsafe(IO.unit)

// Method to allocate dependencies
def allocate[A](resource: Resource[IO, A]): A =
resource.allocated.flatMap { case (a, release) =>
// Shutdown this resource, and after shutdown all previous
shutdown.update(release *> _).map(_ => a)
}.unsafeRunSync()

// Shutdown dependencies
def shutdownAll: IO[Unit] = {
shutdown.getAndSet(IO.unit).flatten
}

}

What this class does is it allows us to perform some non-idiomatic unwrapping of the Resources and keep track of finalizers to shutdown them in the right order. Non-idiomacity will be fixed later, now check how the code looks like after using this simple class:

class Dependencies(val allocator: Allocator) {
lazy val conn: ConnectionFactory = allocator.allocate {
???
}

lazy val repo = new Repository(conn)

lazy val serviceA = new ServiceA(repo)

lazy val serviceB = new ServiceB(repo)

lazy val server = HttpServerTask(serviceA, serviceB)
}

As you can see, only one dependency needed Allocator as it had something to finalize (ConnectionFactory). The code is much simplified and allows us to achieve:

  • The dependency instation is still lazy, but now lazyness is achieved by lazy val Scala mechanism instead of Resource laziness.
  • Now all dependencies can be exposed as exit dependencies. Each dependency can be freely used outside.
  • Again the code is simple and extensible. We can group similar dependencies in separate Dependencies classes, only reuse single Instantantiator between them.

Now let’s track the problem that we had broken idiomacy of Resource pattern inside of the DI container. If you take a look on the Allocator class, it is by itself has shutdownAll method, which allows us now to wrap our entire DI container in a single Resource monad:

object Dependencies {
// Safe method to create dependencies:
def apply(runtime: IORuntime): Resource[IO, Dependencies] =
Resource.make {
IO(unsafeCreate(runtime))
} {
_.allocator.shutdownAll
}

// Unsafe method, use it carefully as no shutdown is executed:
def unsafeCreate(runtime: IORuntime): Dependencies =
new Dependencies(new Allocator()(runtime))
}

class Dependencies(val allocator: Allocator) {
...
}

// Runner class
object Main extends IOApp.Simple {
// Now we can use any dependency and not just `server` as all of them are exposed:
private val dependencies = Dependencies(IORuntime.global)

override def run = dependencies.use(_.server.run)
}

That’s it! This method is my chosed method of doing Dependency Injection in Scala with Cats-Effect right now. While breaking Resource idiomatic boundary inside of the container, it allows to achieve reusing of the dependencies in a simple and extensible way, and we still go back to the Resource pattern for external users who will call DI container's dependencies.

Check how this pattern also allows breaking dependencies into multiple containers easily, like so:

class AwsDependencies(val allocator: Allocator, config: Config) {
val s3: AwsS3Client[IO] = allocator.allocate {
Resource.fromAutoCloseable(IO.blocking {
S3AsyncClient.builder()
.region(config.region)
.build()
}).map(new AwsS3Client(_))
}
}

class MainDependencies(val allocator: Allocator) {

lazy val config = ???

lazy val aws: AwsDependencies = new AwsDependencies(allocator, config.as[Config]("aws"))

lazy val httpRoutes: Routes = new Routes(aws.s3)

}

Here is a full example of the final code:

--

--

Ihor Vovk

Software Engineer/Team Lead. Interested in functional programming, psychology, photography.