scalaadvanced

Cats Effect Resource Management

Manage resources safely with Cats Effect Resource: acquire-release, composition, and lifecycle.

scala
import cats.effect.*
import cats.syntax.all.*

// Simulated resources
class DatabaseConnection(url: String):
  def query(sql: String): IO[List[String]] =
    IO.println(s"  Query: $sql") *> IO.pure(List("row1", "row2"))
  def close(): IO[Unit] = IO.println(s"  Closed DB: $url")

class HttpClient(name: String):
  def get(url: String): IO[String] =
    IO.pure(s"Response from $url")
  def close(): IO[Unit] = IO.println(s"  Closed HTTP: $name")

class Cache(name: String):
  private val store = scala.collection.mutable.Map.empty[String, String]
  def get(key: String): IO[Option[String]] = IO(store.get(key))
  def put(key: String, value: String): IO[Unit] = IO(store(key) = value)
  def close(): IO[Unit] = IO.println(s"  Closed Cache: $name")

// Resource definitions
def mkDatabase(url: String): Resource[IO, DatabaseConnection] =
  Resource.make(
    IO.println(s"  Opening DB: $url") *> IO(new DatabaseConnection(url))
  )(_.close())

def mkHttpClient(name: String): Resource[IO, HttpClient] =
  Resource.make(
    IO.println(s"  Creating HTTP: $name") *> IO(new HttpClient(name))
  )(_.close())

def mkCache(name: String): Resource[IO, Cache] =
  Resource.make(
    IO.println(s"  Init Cache: $name") *> IO(new Cache(name))
  )(_.close())

// Composed resources
case class AppResources(
  db: DatabaseConnection,
  http: HttpClient,
  cache: Cache
)

def mkAppResources: Resource[IO, AppResources] =
  for
    db    <- mkDatabase("postgres://localhost/myapp")
    http  <- mkHttpClient("main")
    cache <- mkCache("app")
  yield AppResources(db, http, cache)

// Parallel resource acquisition
def mkAppResourcesPar: Resource[IO, AppResources] =
  (
    mkDatabase("postgres://localhost/myapp"),
    mkHttpClient("main"),
    mkCache("app")
  ).parMapN(AppResources.apply)

// Resource with finalizer ordering
def orderedResources: Resource[IO, Unit] =
  for
    _ <- Resource.make(IO.println("  Acquire A"))(_ => IO.println("  Release A"))
    _ <- Resource.make(IO.println("  Acquire B"))(_ => IO.println("  Release B"))
    _ <- Resource.make(IO.println("  Acquire C"))(_ => IO.println("  Release C"))
  yield ()  // Released in reverse: C, B, A

object ResourceApp extends IOApp.Simple:
  def run: IO[Unit] = for
    // Single resource
    _ <- IO.println("=== Single Resource ===")
    _ <- mkDatabase("test").use { db =>
      db.query("SELECT 1").flatMap(r => IO.println(s"  Result: $r"))
    }

    // Composed resources
    _ <- IO.println("\n=== Composed Resources ===")
    _ <- mkAppResources.use { app =>
      for
        rows <- app.db.query("SELECT * FROM users")
        resp <- app.http.get("https://api.example.com")
        _    <- app.cache.put("key", resp)
        v    <- app.cache.get("key")
        _    <- IO.println(s"  DB: $rows, Cache: $v")
      yield ()
    }

    // Ordering demo
    _ <- IO.println("\n=== Release Order ===")
    _ <- orderedResources.use(_ => IO.println("  Using all resources"))

    // Error safety
    _ <- IO.println("\n=== Error Safety ===")
    _ <- mkDatabase("error-test").use { db =>
      IO.raiseError(RuntimeException("Boom!"))
    }.handleError(e => IO.println(s"  Caught: ${e.getMessage}"))
  yield ()

Use Cases

  • Safe resource lifecycle management
  • Composed service initialization
  • Error-safe cleanup guarantees

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.