ZIO's Scope-based resource lifecycle management provides automatic cleanup and finalizer execution for leak-free applications with guaranteed resource cleanup even in the presence of failures and interruptions.
The foundational abstraction for managing resource lifecycles with automatic cleanup guarantees.
/**
* A scope represents a resource lifecycle boundary where resources can be safely allocated
* and automatically cleaned up when the scope closes
*/
sealed trait Scope {
/** Add a finalizer that runs when the scope closes */
def addFinalizer(finalizer: => UIO[Any]): UIO[Unit]
/** Add a finalizer that receives the exit status */
def addFinalizerExit(finalizer: Exit[Any, Any] => UIO[Any]): UIO[Unit]
/** Create a child scope */
def fork: UIO[Scope.Closeable]
/** Create a child scope with custom execution strategy */
def forkWith(executionStrategy: => ExecutionStrategy): UIO[Scope.Closeable]
/** Extend the scope to cover a larger region */
def extend[R]: Scope.ExtendPartiallyApplied[R]
}
/**
* A closeable scope that can be explicitly closed
*/
abstract class Scope.Closeable extends Scope {
/** Close the scope, running all finalizers */
def close(exit: => Exit[Any, Any]): UIO[Unit]
/** Number of finalizers registered */
def size: Int
/** Use this scope for an effect */
def use[R]: Scope.UsePartiallyApplied[R]
}Create and manage scopes for various resource management patterns.
/**
* Create a new closeable scope
*/
def make: UIO[Scope.Closeable]
/**
* Create a scope with custom execution strategy
*/
def makeWith(executionStrategy: => ExecutionStrategy): UIO[Scope.Closeable]
/**
* Create a scope that runs finalizers in parallel
*/
def parallel: UIO[Scope.Closeable]
/**
* Add a finalizer to the current scope
*/
def addFinalizer(finalizer: => UIO[Any]): ZIO[Scope, Nothing, Unit]
/**
* Add an exit-aware finalizer to the current scope
*/
def addFinalizerExit(finalizer: Exit[Any, Any] => UIO[Any]): ZIO[Scope, Nothing, Unit]
/**
* Access to global scope (never closes)
*/
val global: Scope.Closeable
/**
* Default scope layer for dependency injection
*/
val default: ZLayer[Any, Nothing, Scope]
/**
* Execute an effect with a fresh scope
*/
def scoped[R, E, A](zio: ZIO[R with Scope, E, A]): ZIO[R, E, A]
/**
* Execute an effect that requires a scope
*/
def scopedWith[R, E, A](f: Scope => ZIO[R, E, A]): ZIO[R, E, A]Usage Examples:
import zio._
// Basic resource management with scope
val resourceProgram = ZIO.scoped {
for {
_ <- Scope.addFinalizer(Console.printLine("Cleaning up resources"))
resource <- ZIO.attempt(new FileInputStream("data.txt"))
_ <- Scope.addFinalizer(ZIO.succeed(resource.close()))
data <- ZIO.attempt(resource.read())
} yield data
}
// Manual scope control
val manualScopeProgram = for {
scope <- Scope.make
_ <- scope.addFinalizer(Console.printLine("Manual cleanup"))
result <- scope.use { implicit s =>
for {
resource <- acquireResource
data <- processResource(resource)
} yield data
}
_ <- scope.close(Exit.succeed(()))
} yield result
// Nested scopes with different lifecycles
val nestedScopes = ZIO.scoped {
for {
_ <- Scope.addFinalizer(Console.printLine("Outer scope cleanup"))
outer <- acquireOuterResource
result <- ZIO.scoped {
for {
_ <- Scope.addFinalizer(Console.printLine("Inner scope cleanup"))
inner <- acquireInnerResource
data <- processResources(outer, inner)
} yield data
}
} yield result
}Common patterns for safe resource acquisition and management.
/**
* Acquire-release pattern for resource management
*/
def acquireRelease[R, E, A](
acquire: => ZIO[R, E, A]
)(release: A => URIO[R, Any]): ZIO[R with Scope, E, A]
/**
* Acquire-release with exit-aware cleanup
*/
def acquireReleaseExit[R, E, A](
acquire: => ZIO[R, E, A]
)(release: (A, Exit[Any, Any]) => URIO[R, Any]): ZIO[R with Scope, E, A]
/**
* Acquire multiple resources atomically
*/
def acquireReleaseAttempt[R, E, A](
acquire: ZIO[R, E, A]
)(release: A => URIO[R, Any]): ZIO[R with Scope, E, A]
/**
* Auto-closeable resource pattern (for Java interop)
*/
def fromAutoCloseable[R, E, A <: AutoCloseable](
fa: ZIO[R, E, A]
): ZIO[R with Scope, E, A]
/**
* Ensuring finalizer runs regardless of how effect completes
*/
def ensuring[R1 <: R](finalizer: => URIO[R1, Any]): ZIO[R1, E, A]
/**
* Add finalizer that only runs on specific exit cases
*/
def onExit[R1 <: R](cleanup: Exit[E, A] => URIO[R1, Any]): ZIO[R1, E, A]Usage Examples:
// File processing with automatic cleanup
val fileProcessing = ZIO.scoped {
for {
file <- ZIO.acquireRelease(
ZIO.attempt(new BufferedReader(new FileReader("input.txt")))
)(reader => ZIO.succeed(reader.close()).ignore)
output <- ZIO.acquireRelease(
ZIO.attempt(new PrintWriter("output.txt"))
)(writer => ZIO.succeed(writer.close()).ignore)
_ <- processFile(file, output)
} yield ()
}
// Database connection management
val databaseWork = ZIO.scoped {
for {
connection <- ZIO.acquireRelease(
ZIO.attempt(DriverManager.getConnection(jdbcUrl))
)(conn => ZIO.succeed(conn.close()).ignore)
statement <- ZIO.acquireRelease(
ZIO.attempt(connection.prepareStatement(sql))
)(stmt => ZIO.succeed(stmt.close()).ignore)
results <- ZIO.attempt(statement.executeQuery())
data <- processResults(results)
} yield data
}
// Network resource management
val networkOperation = ZIO.scoped {
for {
socket <- ZIO.acquireReleaseExit(
ZIO.attempt(new Socket("localhost", 8080))
) { (socket, exit) =>
for {
_ <- Console.printLine(s"Closing socket. Exit: $exit")
_ <- ZIO.succeed(socket.close()).ignore
} yield ()
}
stream <- ZIO.fromAutoCloseable(
ZIO.attempt(socket.getInputStream())
)
data <- readFromStream(stream)
} yield data
}Complex resource management patterns for sophisticated applications.
// Resource pool pattern
class ResourcePool[R, E, A](
factory: ZIO[R, E, A],
cleanup: A => URIO[R, Any],
maxSize: Int
) {
private val available = Queue.bounded[A](maxSize)
private val inUse = Ref.make(Set.empty[A])
def acquire: ZIO[R with Scope, E, A] = ZIO.scoped {
for {
resource <- available.take.orElse(factory)
_ <- inUse.update(_ + resource)
_ <- Scope.addFinalizer(release(resource))
} yield resource
}
def release(resource: A): URIO[R, Unit] = {
for {
_ <- inUse.update(_ - resource)
_ <- available.offer(resource).ignore
} yield ()
}
def shutdown: URIO[R, Unit] = {
for {
resources <- available.takeAll
_ <- ZIO.foreachDiscard(resources)(cleanup)
} yield ()
}
}
// Cached resource pattern
class CachedResource[R, E, A](
factory: ZIO[R, E, A],
cleanup: A => URIO[R, Any],
ttl: Duration
) {
private val cache = Ref.make(Option.empty[(A, Long)])
def get: ZIO[R with Scope, E, A] = ZIO.scoped {
for {
now <- Clock.currentTime(TimeUnit.MILLISECONDS)
cached <- cache.get
resource <- cached match {
case Some((resource, timestamp)) if (now - timestamp) < ttl.toMillis =>
ZIO.succeed(resource)
case _ =>
for {
newResource <- factory
_ <- cache.set(Some((newResource, now)))
_ <- Scope.addFinalizer(cleanup(newResource))
} yield newResource
}
} yield resource
}
def invalidate: UIO[Unit] = cache.set(None)
}
// Scoped reference pattern
class ScopedRef[A](initialValue: A) {
private val ref = Ref.make(initialValue)
def scoped: ZIO[Scope, Nothing, Ref[A]] = {
for {
currentValue <- ref.get
scopedRef <- Ref.make(currentValue)
_ <- Scope.addFinalizerExit { exit =>
for {
finalValue <- scopedRef.get
_ <- ref.set(finalValue)
} yield ()
}
} yield scopedRef
}
def get: UIO[A] = ref.get
def set(a: A): UIO[Unit] = ref.set(a)
}Usage Examples:
// HTTP client with connection pooling
val httpClientProgram = for {
pool <- ZIO.succeed(new ResourcePool(
factory = ZIO.attempt(HttpClientBuilder.create().build()),
cleanup = client => ZIO.succeed(client.close()).ignore,
maxSize = 10
))
results <- ZIO.foreachPar(urls) { url =>
ZIO.scoped {
for {
client <- pool.acquire
request <- ZIO.succeed(HttpGet(url))
response <- ZIO.attempt(client.execute(request))
body <- ZIO.attempt(EntityUtils.toString(response.getEntity))
} yield body
}
}
_ <- pool.shutdown
} yield results
// Cached database connection
val cachedDbProgram = for {
connectionCache <- ZIO.succeed(new CachedResource(
factory = ZIO.attempt(DriverManager.getConnection(url)),
cleanup = conn => ZIO.succeed(conn.close()).ignore,
ttl = 5.minutes
))
results <- ZIO.foreachPar(queries) { query =>
ZIO.scoped {
for {
conn <- connectionCache.get
stmt <- ZIO.attempt(conn.prepareStatement(query))
result <- ZIO.attempt(stmt.executeQuery())
data <- extractData(result)
} yield data
}
}
} yield results
// Hierarchical resource management
val hierarchicalResources = ZIO.scoped {
for {
// Top-level resources
_ <- Scope.addFinalizer(Console.printLine("Shutting down application"))
config <- loadConfiguration
// Mid-level resources
database <- ZIO.scoped {
for {
_ <- Scope.addFinalizer(Console.printLine("Closing database"))
db <- initializeDatabase(config.dbConfig)
} yield db
}
// Low-level resources
result <- ZIO.scoped {
for {
_ <- Scope.addFinalizer(Console.printLine("Cleaning up transaction"))
session <- database.createSession()
_ <- Scope.addFinalizer(ZIO.succeed(session.close()).ignore)
data <- session.query("SELECT * FROM users")
} yield data
}
} yield result
}