@zio.dev/zio-blocks 0.0.28 → 0.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1125 @@
1
+ ---
2
+ id: resource
3
+ title: "Resource"
4
+ ---
5
+
6
+ `Resource[A]` is a **lazy recipe for managing resource lifecycles**, encapsulating both acquisition and finalization tied to a `Scope`. Resources describe *what* to do, not *when* — creation only happens when a resource is passed to `scope.allocate()`. They compose naturally with `map`, `flatMap`, and `zip` to build complex dependency graphs with automatic cleanup in LIFO order.
7
+
8
+ ```scala
9
+ sealed trait Resource[+A] {
10
+ def map[B](f: A => B): Resource[B]
11
+ def flatMap[B](f: A => Resource[B]): Resource[B]
12
+ def zip[B](that: Resource[B]): Resource[(A, B)]
13
+ }
14
+ ```
15
+
16
+ Key properties:
17
+ - **Lazy**: Resources don't acquire anything until allocated via `scope.allocate()`
18
+ - **Covariant**: `Resource[Dog]` is a subtype of `Resource[Animal]` when `Dog <: Animal`
19
+ - **Composable**: `map`, `flatMap`, and `zip` combine resources into larger structures
20
+ - **Two strategies**: Shared (memoized with reference counting) and Unique (fresh per allocation)
21
+ - **Auto-cleanup**: Finalizers run automatically in LIFO order when scopes close
22
+
23
+ ## Motivation
24
+
25
+ Without Resources, managing complex initialization and cleanup is tedious and error-prone. Resources eliminate manual bookkeeping by tying value lifecycles to Scopes and registering finalizers automatically.
26
+
27
+ **Benefits:**
28
+ - Automatic cleanup even on exceptions
29
+ - LIFO finalization order (inner resources close before outer ones)
30
+ - Compositional: build complex dependency graphs declaratively
31
+ - Type-safe: compiler ensures you have dependencies available
32
+ - Works seamlessly with `Wire` for constructor-based dependency injection
33
+
34
+ ## Installation
35
+
36
+ Add the ZIO Blocks Scope module to your `build.sbt`:
37
+
38
+ ```scala
39
+ libraryDependencies += "dev.zio" %% "zio-blocks-scope" % "0.0.29"
40
+ ```
41
+
42
+ For cross-platform (Scala.js):
43
+
44
+ ```scala
45
+ libraryDependencies += "dev.zio" %%% "zio-blocks-scope" % "0.0.29"
46
+ ```
47
+
48
+ Supported Scala versions: 2.13.x and 3.x.
49
+
50
+ ## Construction
51
+
52
+ Resources can be created in several ways: from values, from explicit acquire/release pairs, from `AutoCloseable` types, or from custom functions.
53
+
54
+ ### `Resource.apply` — wrap a value
55
+
56
+ Wraps a by-name value as a resource. If the value implements `AutoCloseable`, its `close()` method is automatically registered as a finalizer.
57
+
58
+ ```scala
59
+ import zio.blocks.scope._
60
+
61
+ case class Config(debug: Boolean)
62
+
63
+ class Database(val name: String) extends AutoCloseable {
64
+ def close(): Unit = println(s"Closing database $name")
65
+ }
66
+
67
+ val configResource = Resource(Config(debug = true))
68
+ val dbResource = Resource(new Database("mydb"))
69
+ ```
70
+
71
+ ### `Resource.acquireRelease` — explicit lifecycle
72
+
73
+ Creates a resource with separate acquire and release functions. The acquire thunk runs during allocation; the release function is registered as a finalizer:
74
+
75
+ ```scala
76
+ import zio.blocks.scope._
77
+ import java.io.FileInputStream
78
+
79
+ val fileResource = Resource.acquireRelease {
80
+ new FileInputStream("data.txt")
81
+ } { stream =>
82
+ stream.close()
83
+ }
84
+ ```
85
+
86
+ ### `Resource.fromAutoCloseable` — type-safe wrapping
87
+
88
+ Creates a resource specifically for `AutoCloseable` subtypes. This is a compile-time verified alternative to `Resource(value)` when you know the value is closeable:
89
+
90
+ ```scala
91
+ import zio.blocks.scope._
92
+ import java.io.BufferedInputStream
93
+ import java.io.FileInputStream
94
+
95
+ val streamResource = Resource.fromAutoCloseable {
96
+ new BufferedInputStream(new FileInputStream("data.bin"))
97
+ }
98
+ ```
99
+
100
+ ### `Resource.shared` — memoized with reference counting
101
+
102
+ Creates a shared resource that memoizes its value across multiple allocations. The first call initializes the value; subsequent calls return the same instance with reference counting. Finalizers run only when the last reference is released:
103
+
104
+ ```scala
105
+ import zio.blocks.scope._
106
+
107
+ var initCount = 0
108
+
109
+ val sharedResource = Resource.shared[Int] { _ =>
110
+ initCount += 1
111
+ initCount
112
+ }
113
+ ```
114
+
115
+ ### `Resource.unique` — fresh instances
116
+
117
+ Creates a unique resource that produces a fresh instance each time it's allocated. Use for per-request state or resources that should never be shared:
118
+
119
+ ```scala
120
+ import zio.blocks.scope._
121
+
122
+ var counter = 0
123
+
124
+ val uniqueResource = Resource.unique[Int] { _ =>
125
+ counter += 1
126
+ counter
127
+ }
128
+ ```
129
+
130
+ ## Core Operations
131
+
132
+ Resources support transformation and composition through `map`, `flatMap`, and `zip`.
133
+
134
+ ### `Resource#map` — transform the value
135
+
136
+ Transforms the value produced by a resource without affecting finalization. The transformation function is applied after the resource is acquired.
137
+
138
+ ```scala
139
+ import zio.blocks.scope._
140
+
141
+ val portResource = Resource(8080)
142
+ val urlResource = portResource.map(port => s"http://localhost:$port")
143
+ ```
144
+
145
+ ### `Resource#flatMap` — sequence resources
146
+
147
+ Sequences two resources, using the result of the first to create the second. Both sets of finalizers are registered and run in LIFO order (inner before outer):
148
+
149
+ ```scala
150
+ import zio.blocks.scope._
151
+
152
+ case class DbConfig(url: String)
153
+
154
+ class Database(config: DbConfig) extends AutoCloseable {
155
+ def query(sql: String): String = s"Result from ${config.url}: $sql"
156
+ def close(): Unit = println("Database closed")
157
+ }
158
+
159
+ val configResource = Resource(DbConfig("jdbc:postgres://localhost"))
160
+ val dbResource = configResource.flatMap { config =>
161
+ Resource.fromAutoCloseable(new Database(config))
162
+ }
163
+ ```
164
+
165
+ ### `Resource#zip` — combine resources
166
+
167
+ Combines two resources into a single resource that produces a tuple of both values. Both resources are acquired and both sets of finalizers are registered:
168
+
169
+ ```scala
170
+ import zio.blocks.scope._
171
+
172
+ case class DbConfig(url: String)
173
+
174
+ class Database(config: DbConfig) extends AutoCloseable {
175
+ def close(): Unit = println("Database closed")
176
+ }
177
+
178
+ class Cache extends AutoCloseable {
179
+ def close(): Unit = println("Cache closed")
180
+ }
181
+
182
+ val dbResource = Resource.fromAutoCloseable(new Database(DbConfig("jdbc:postgres://localhost")))
183
+ val cacheResource = Resource.fromAutoCloseable(new Cache())
184
+ val combined = dbResource.zip(cacheResource)
185
+ ```
186
+
187
+ ## Shared vs. Unique
188
+
189
+ The fundamental difference is **reuse semantics**:
190
+
191
+ | Aspect | Shared | Unique |
192
+ |-------------------|-----------------------------------|----------------------------------------------|
193
+ | **Creation** | `Resource.shared(f)` | `Resource.unique(f)` or `Resource(value)` |
194
+ | **Memoization** | Yes, with reference counting | No, fresh per allocation |
195
+ | **When to use** | Expensive resources (DB connections, thread pools) | Per-request state, stateful handlers |
196
+ | **Instance reuse** | Same instance across nested scopes | New instance per allocation |
197
+ | **Finalization** | Runs when last reference released | Runs when scope closes |
198
+
199
+ In a diamond dependency pattern (where `AppService` depends on both `UserService` and `OrderService`, both depending on `Database`), using `Resource.shared[Database]` ensures both services receive the same instance.
200
+
201
+ ## Integration with Wire and Scope
202
+
203
+ `Resource` is the foundation of ZIO Blocks' dependency injection. `Wire` describes how to build a service; `Resource` describes how to manage its lifecycle. When used together with the `Resource.from[T]` macro, they enable compile-safe automatic dependency injection:
204
+
205
+ ```scala
206
+ import zio.blocks.scope._
207
+
208
+ case class Config(debug: Boolean)
209
+
210
+ class Logger(config: Config) {
211
+ def log(msg: String): Unit = println(s"[${config.debug}] $msg")
212
+ }
213
+
214
+ class Service(logger: Logger) extends AutoCloseable {
215
+ def run(): Unit = logger.log("Running")
216
+ def close(): Unit = logger.log("Shutting down")
217
+ }
218
+
219
+ val serviceResource = Resource.from[Service](
220
+ Wire(Config(debug = true))
221
+ )
222
+ ```
223
+
224
+ See [`Wire`](./wire.md) for how to declare dependency recipes and [`Scope`](./scope.md) for scope-based resource management.
225
+
226
+ ## Running the Examples
227
+
228
+ All code from this guide is available as runnable examples in the `scope-examples` module.
229
+
230
+ **1. Clone the repository and navigate to the project:**
231
+
232
+ ```bash
233
+ git clone https://github.com/zio/zio-blocks.git
234
+ cd zio-blocks
235
+ ```
236
+
237
+ **2. Run individual examples with sbt:**
238
+
239
+ **Basic lifecycle management with temporary files**
240
+
241
+ ```scala title="scope-examples/src/main/scala/scope/examples/TempFileHandlingExample.scala"
242
+ /*
243
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
244
+ *
245
+ * Licensed under the Apache License, Version 2.0 (the "License");
246
+ * you may not use this file except in compliance with the License.
247
+ * You may obtain a copy of the License at
248
+ *
249
+ * http://www.apache.org/licenses/LICENSE-2.0
250
+ *
251
+ * Unless required by applicable law or agreed to in writing, software
252
+ * distributed under the License is distributed on an "AS IS" BASIS,
253
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
254
+ * See the License for the specific language governing permissions and
255
+ * limitations under the License.
256
+ */
257
+
258
+ package scope.examples
259
+
260
+ import zio.blocks.scope._
261
+
262
+ /**
263
+ * Demonstrates `scope.defer(...)` for registering manual cleanup actions.
264
+ *
265
+ * This example shows how to create temporary files during processing and ensure
266
+ * they are deleted when the scope exits—even if processing fails. Deferred
267
+ * cleanup actions run in LIFO (last-in-first-out) order.
268
+ */
269
+
270
+ /** Represents a temporary file with basic read/write operations. */
271
+ case class TempFile(path: String) {
272
+ private var content: String = ""
273
+
274
+ def write(data: String): Unit = content = data
275
+ def read(): String = content
276
+ def delete(): Boolean = { println(s" Deleting: $path"); true }
277
+ }
278
+
279
+ /** Result of processing temporary files. */
280
+ case class ProcessingResult(processedCount: Int, totalBytes: Long, errors: List[String])
281
+
282
+ /** Processes a list of temporary files and aggregates results. */
283
+ object FileProcessor {
284
+ def process(files: List[TempFile]): ProcessingResult = {
285
+ val totalBytes = files.map(_.read().length.toLong).sum
286
+ ProcessingResult(processedCount = files.size, totalBytes = totalBytes, errors = Nil)
287
+ }
288
+ }
289
+
290
+ @main def tempFileHandlingExample(): Unit = {
291
+ println("=== Temp File Handling Example ===\n")
292
+ println("Demonstrating scope.defer() for manual cleanup registration.\n")
293
+
294
+ val result = Scope.global.scoped { scope =>
295
+ // Create temp files and register cleanup via defer.
296
+ // Cleanup runs in LIFO order: file3, file2, file1.
297
+
298
+ val file1 = createTempFile(scope, "/tmp/data-001.tmp", "First file content")
299
+ val file2 = createTempFile(scope, "/tmp/data-002.tmp", "Second file - more data here")
300
+ val file3 = createTempFile(scope, "/tmp/data-003.tmp", "Third file with the most content of all")
301
+
302
+ println("\nProcessing files...")
303
+ val processingResult = FileProcessor.process(List(file1, file2, file3))
304
+ println(s"Processed ${processingResult.processedCount} files, ${processingResult.totalBytes} bytes\n")
305
+
306
+ println("Exiting scope - cleanup runs in LIFO order:")
307
+ processingResult
308
+ }
309
+
310
+ println(s"\nFinal result: $result")
311
+ }
312
+
313
+ /**
314
+ * Creates a temporary file and registers its cleanup with the scope.
315
+ *
316
+ * The cleanup action is registered via `defer(...)`, ensuring the file is
317
+ * deleted when the scope closes—regardless of whether processing succeeds.
318
+ *
319
+ * @param s
320
+ * the scope to register cleanup with
321
+ * @param path
322
+ * the file path
323
+ * @param content
324
+ * initial content to write
325
+ * @return
326
+ * the created TempFile
327
+ */
328
+ private def createTempFile(s: Scope, path: String, content: String): TempFile = {
329
+ val file = TempFile(path)
330
+ file.write(content)
331
+ println(s"Created: $path (${content.length} bytes)")
332
+
333
+ // Register cleanup - will run when scope exits, in LIFO order
334
+ s.defer {
335
+ file.delete()
336
+ }
337
+
338
+ file
339
+ }
340
+ ```
341
+
342
+ ([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/TempFileHandlingExample.scala))
343
+
344
+ ```bash
345
+ sbt "scope-examples/runMain scope.examples.TempFileHandlingExample"
346
+ ```
347
+
348
+ **Acquiring and releasing database connections**
349
+
350
+ ```scala title="scope-examples/src/main/scala/scope/examples/DatabaseConnectionExample.scala"
351
+ /*
352
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
353
+ *
354
+ * Licensed under the Apache License, Version 2.0 (the "License");
355
+ * you may not use this file except in compliance with the License.
356
+ * You may obtain a copy of the License at
357
+ *
358
+ * http://www.apache.org/licenses/LICENSE-2.0
359
+ *
360
+ * Unless required by applicable law or agreed to in writing, software
361
+ * distributed under the License is distributed on an "AS IS" BASIS,
362
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
363
+ * See the License for the specific language governing permissions and
364
+ * limitations under the License.
365
+ */
366
+
367
+ package scope.examples
368
+
369
+ import zio.blocks.scope._
370
+
371
+ /**
372
+ * Configuration for database connection.
373
+ *
374
+ * @param host
375
+ * the database server hostname
376
+ * @param port
377
+ * the database server port
378
+ * @param database
379
+ * the database name to connect to
380
+ */
381
+ final case class DbConfig(host: String, port: Int, database: String) {
382
+ def connectionUrl: String = s"jdbc:postgresql://$host:$port/$database"
383
+ }
384
+
385
+ /**
386
+ * Represents the result of a database query.
387
+ *
388
+ * @param rows
389
+ * the result set as a list of row maps
390
+ */
391
+ final case class QueryResult(rows: List[Map[String, String]]) {
392
+ def isEmpty: Boolean = rows.isEmpty
393
+ def size: Int = rows.size
394
+ }
395
+
396
+ /**
397
+ * Simulates a database connection with lifecycle management.
398
+ *
399
+ * This class demonstrates how AutoCloseable resources integrate with ZIO Blocks
400
+ * Scope. When allocated via `allocate(Resource(...))`, the `close()` method is
401
+ * automatically registered as a finalizer.
402
+ *
403
+ * @param config
404
+ * the database configuration
405
+ */
406
+ final class Database(config: DbConfig) extends AutoCloseable {
407
+ private var connected = false
408
+
409
+ def connect(): Unit = {
410
+ println(s"[Database] Connecting to ${config.connectionUrl}...")
411
+ connected = true
412
+ println(s"[Database] Connected successfully")
413
+ }
414
+
415
+ def query(sql: String): QueryResult = {
416
+ require(connected, "Database not connected")
417
+ println(s"[Database] Executing: $sql")
418
+ sql match {
419
+ case s if s.contains("users") =>
420
+ QueryResult(
421
+ List(
422
+ Map("id" -> "1", "name" -> "Alice"),
423
+ Map("id" -> "2", "name" -> "Bob")
424
+ )
425
+ )
426
+ case s if s.contains("orders") =>
427
+ QueryResult(
428
+ List(
429
+ Map("order_id" -> "101", "user_id" -> "1", "total" -> "99.99"),
430
+ Map("order_id" -> "102", "user_id" -> "2", "total" -> "149.50")
431
+ )
432
+ )
433
+ case _ =>
434
+ QueryResult(List(Map("result" -> "OK")))
435
+ }
436
+ }
437
+
438
+ override def close(): Unit = {
439
+ println(s"[Database] Closing connection to ${config.connectionUrl}")
440
+ connected = false
441
+ }
442
+ }
443
+
444
+ /**
445
+ * Demonstrates basic resource lifecycle management with ZIO Blocks Scope.
446
+ *
447
+ * This example shows:
448
+ * - Allocating an AutoCloseable resource with automatic cleanup
449
+ * - Using `$(value)(f)` to access scoped values and execute queries
450
+ * - LIFO finalizer ordering (last allocated = first closed)
451
+ *
452
+ * When the scope exits, all registered finalizers run in reverse order,
453
+ * ensuring proper cleanup even if exceptions occur.
454
+ */
455
+ @main def runDatabaseExample(): Unit = {
456
+ println("=== Database Connection Example ===\n")
457
+
458
+ val config = DbConfig("localhost", 5432, "myapp")
459
+
460
+ Scope.global.scoped { scope =>
461
+ import scope._
462
+ println("[Scope] Entering scoped region\n")
463
+
464
+ // Allocate the database resource. Because Database extends AutoCloseable,
465
+ // its close() method is automatically registered as a finalizer.
466
+ val db: $[Database] = allocate(Resource {
467
+ val database = new Database(config)
468
+ database.connect()
469
+ database
470
+ })
471
+
472
+ // Use $(value)(f) to access the scoped value and execute queries.
473
+ $(db) { database =>
474
+ val users = database.query("SELECT * FROM users")
475
+ println(s"[Result] Found ${users.size} users: ${users.rows.map(_("name")).mkString(", ")}\n")
476
+
477
+ val orders = database.query("SELECT * FROM orders WHERE status = 'pending'")
478
+ println(s"[Result] Found ${orders.size} orders\n")
479
+
480
+ val health = database.query("SELECT 1 AS health_check")
481
+ println(s"[Result] Health check: ${health.rows.head("result")}\n")
482
+ }
483
+
484
+ println("[Scope] Exiting scoped region - finalizers will run in LIFO order")
485
+ }
486
+
487
+ println("\n=== Example Complete ===")
488
+ }
489
+ ```
490
+
491
+ ([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/DatabaseConnectionExample.scala))
492
+
493
+ ```bash
494
+ sbt "scope-examples/runMain scope.examples.DatabaseConnectionExample"
495
+ ```
496
+
497
+ **Shared resources with memoization and reference counting**
498
+
499
+ ```scala title="scope-examples/src/main/scala/scope/examples/CachingSharedLoggerExample.scala"
500
+ /*
501
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
502
+ *
503
+ * Licensed under the Apache License, Version 2.0 (the "License");
504
+ * you may not use this file except in compliance with the License.
505
+ * You may obtain a copy of the License at
506
+ *
507
+ * http://www.apache.org/licenses/LICENSE-2.0
508
+ *
509
+ * Unless required by applicable law or agreed to in writing, software
510
+ * distributed under the License is distributed on an "AS IS" BASIS,
511
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
512
+ * See the License for the specific language governing permissions and
513
+ * limitations under the License.
514
+ */
515
+
516
+ package scope.examples
517
+
518
+ import zio.blocks.scope._
519
+ import java.util.concurrent.atomic.AtomicInteger
520
+
521
+ /**
522
+ * Demonstrates `Wire.shared` vs `Wire.unique` and diamond dependency patterns.
523
+ *
524
+ * Two services (ProductService, OrderService) share one Logger instance
525
+ * (diamond pattern), but each gets its own unique Cache instance. This shows
526
+ * how shared wires provide singleton behavior while unique wires create fresh
527
+ * instances per injection site.
528
+ *
529
+ * Key concepts:
530
+ * - `Wire.shared[T]`: Single instance shared across all dependents (memoized)
531
+ * - `Wire.unique[T]`: Fresh instance created for each dependent
532
+ * - Diamond dependency: Multiple services depend on the same shared resource
533
+ * - Reference counting: Shared resources track usage and clean up when last
534
+ * user closes
535
+ */
536
+ object CachingSharedLoggerExample {
537
+
538
+ /** Tracks instantiation counts for demonstration purposes. */
539
+ val loggerInstances = new AtomicInteger(0)
540
+ val cacheInstances = new AtomicInteger(0)
541
+
542
+ /**
543
+ * A shared logger that tracks instantiations and provides logging methods.
544
+ * Implements AutoCloseable for proper resource cleanup.
545
+ */
546
+ class Logger extends AutoCloseable {
547
+ val instanceId: Int = loggerInstances.incrementAndGet()
548
+ println(s" [Logger#$instanceId] Created")
549
+
550
+ def info(msg: String): Unit = println(s" [Logger#$instanceId] INFO: $msg")
551
+ def debug(msg: String): Unit = println(s" [Logger#$instanceId] DEBUG: $msg")
552
+ def close(): Unit = println(s" [Logger#$instanceId] Closed")
553
+ }
554
+
555
+ /**
556
+ * A unique cache per service. Each service gets its own isolated cache
557
+ * instance. Implements AutoCloseable for proper resource cleanup. Note: No
558
+ * constructor params so it can be auto-wired with Wire.unique.
559
+ */
560
+ class Cache extends AutoCloseable {
561
+ val instanceId: Int = cacheInstances.incrementAndGet()
562
+ private var store: Map[String, String] = Map.empty
563
+ println(s" [Cache#$instanceId] Created")
564
+
565
+ def get(key: String): Option[String] = store.get(key)
566
+ def put(key: String, value: String): Unit = store = store.updated(key, value)
567
+ def close(): Unit = println(s" [Cache#$instanceId] Closed")
568
+ }
569
+
570
+ /** Product service with its own cache but sharing the logger. */
571
+ class ProductService(val logger: Logger, val cache: Cache) {
572
+ println(s" [ProductService] Created with Logger#${logger.instanceId} and Cache#${cache.instanceId}")
573
+
574
+ def findProduct(id: String): String =
575
+ cache.get(id) match {
576
+ case Some(product) =>
577
+ logger.debug(s"Cache hit for product $id")
578
+ product
579
+ case None =>
580
+ logger.info(s"Loading product $id from database")
581
+ val product = s"Product-$id"
582
+ cache.put(id, product)
583
+ product
584
+ }
585
+ }
586
+
587
+ /**
588
+ * Order service with its own cache but sharing the same logger as
589
+ * ProductService.
590
+ */
591
+ class OrderService(val logger: Logger, val cache: Cache) {
592
+ println(s" [OrderService] Created with Logger#${logger.instanceId} and Cache#${cache.instanceId}")
593
+
594
+ def createOrder(productId: String): String = {
595
+ val orderId = s"ORD-${System.currentTimeMillis() % 10000}"
596
+ cache.put(orderId, productId)
597
+ logger.info(s"Created order $orderId for product $productId")
598
+ orderId
599
+ }
600
+ }
601
+
602
+ /** Top-level application combining both services. */
603
+ class CachingApp(val productService: ProductService, val orderService: OrderService) extends AutoCloseable {
604
+ def run(): Unit = {
605
+ productService.logger.info("=== Application Started ===")
606
+ val product = productService.findProduct("P001")
607
+ orderService.createOrder(product)
608
+ productService.findProduct("P001") // cache hit
609
+ }
610
+ def close(): Unit = println(" [CachingApp] Closed")
611
+ }
612
+
613
+ @main def runCachingExample(): Unit = {
614
+ println("\n╔════════════════════════════════════════════════════════════════╗")
615
+ println("║ Wire.shared vs Wire.unique - Diamond Dependency Example ║")
616
+ println("╚════════════════════════════════════════════════════════════════╝\n")
617
+
618
+ println("Creating wires...")
619
+ println(" - Logger: Wire.shared (singleton across all services)")
620
+ println(" - Cache: Wire.unique (fresh instance per service)\n")
621
+
622
+ println("─── Resource Acquisition ───")
623
+ Scope.global.scoped { scope =>
624
+ import scope._
625
+ val app: $[CachingApp] = allocate(
626
+ Resource.from[CachingApp](
627
+ Wire.shared[Logger],
628
+ Wire.unique[Cache]
629
+ )
630
+ )
631
+
632
+ println("\n─── Verification ───")
633
+ println(s" Logger instances created: ${loggerInstances.get()} (expected: 1)")
634
+ println(s" Cache instances created: ${cacheInstances.get()} (expected: 2)")
635
+ $(app) { a =>
636
+ println(s" ProductService.logger eq OrderService.logger: ${a.productService.logger eq a.orderService.logger}")
637
+ println(s" ProductService.cache eq OrderService.cache: ${a.productService.cache eq a.orderService.cache}")
638
+
639
+ println("\n─── Running Application ───")
640
+ a.run()
641
+ }
642
+
643
+ println("\n─── Scope Closing (LIFO cleanup) ───")
644
+ }
645
+
646
+ println("\n─── Summary ───")
647
+ println(s" Final Logger count: ${loggerInstances.get()} (shared = 1 instance)")
648
+ println(s" Final Cache count: ${cacheInstances.get()} (unique = 2 instances)")
649
+ println("\nDiamond pattern verified: both services received the same Logger instance.")
650
+ }
651
+ }
652
+ ```
653
+
654
+ ([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/CachingSharedLoggerExample.scala))
655
+
656
+ ```bash
657
+ sbt "scope-examples/runMain scope.examples.CachingSharedLoggerExample"
658
+ ```
659
+
660
+ **Managing shared expensive resources**
661
+
662
+ ```scala title="scope-examples/src/main/scala/scope/examples/ConnectionPoolExample.scala"
663
+ /*
664
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
665
+ *
666
+ * Licensed under the Apache License, Version 2.0 (the "License");
667
+ * you may not use this file except in compliance with the License.
668
+ * You may obtain a copy of the License at
669
+ *
670
+ * http://www.apache.org/licenses/LICENSE-2.0
671
+ *
672
+ * Unless required by applicable law or agreed to in writing, software
673
+ * distributed under the License is distributed on an "AS IS" BASIS,
674
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
675
+ * See the License for the specific language governing permissions and
676
+ * limitations under the License.
677
+ */
678
+
679
+ package scope.examples
680
+
681
+ import zio.blocks.scope._
682
+ import java.util.concurrent.atomic.AtomicInteger
683
+
684
+ /**
685
+ * Demonstrates `Resource.Shared` with reference counting and nested resource
686
+ * acquisition.
687
+ *
688
+ * This example shows a realistic connection pool pattern where:
689
+ * - The pool itself is a shared resource (created once, ref-counted)
690
+ * - Individual connections are resources that must be allocated in a scope
691
+ * - `pool.acquire` returns `Resource[PooledConnection]`, forcing proper
692
+ * scoping
693
+ *
694
+ * This pattern is common for database pools, HTTP client pools, and thread
695
+ * pools.
696
+ */
697
+
698
+ /** Configuration for the connection pool. */
699
+ final case class PoolConfig(maxConnections: Int, timeout: Long)
700
+
701
+ /**
702
+ * A connection retrieved from the pool.
703
+ *
704
+ * Connections are resources - they must be released back to the pool when done.
705
+ * This is enforced by making `acquire` return a `Resource[PooledConnection]`.
706
+ */
707
+ final class PooledConnection(val id: Int, pool: ConnectionPool) extends AutoCloseable {
708
+ println(s" [Conn#$id] Acquired from pool")
709
+
710
+ def execute(sql: String): String = {
711
+ println(s" [Conn#$id] Executing: $sql")
712
+ s"Result from connection $id"
713
+ }
714
+
715
+ override def close(): Unit =
716
+ pool.release(this)
717
+ }
718
+
719
+ /**
720
+ * A connection pool that manages pooled connections.
721
+ *
722
+ * Key design: `acquire` returns `Resource[PooledConnection]`, not a raw
723
+ * connection. This forces callers to allocate the connection in a scope,
724
+ * ensuring proper release even if exceptions occur.
725
+ */
726
+ final class ConnectionPool(config: PoolConfig) extends AutoCloseable {
727
+ private val nextId = new AtomicInteger(0)
728
+ private val active = new AtomicInteger(0)
729
+ private val _closed = new AtomicInteger(0)
730
+
731
+ println(s" [Pool] Created with max ${config.maxConnections} connections")
732
+
733
+ /**
734
+ * Acquires a connection from the pool.
735
+ *
736
+ * Returns a `Resource[PooledConnection]` that must be allocated in a scope.
737
+ * The connection is automatically released when the scope exits.
738
+ */
739
+ def acquire: Resource[PooledConnection] = Resource.acquireRelease {
740
+ if (_closed.get() > 0) throw new IllegalStateException("Pool is closed")
741
+ if (active.get() >= config.maxConnections)
742
+ throw new IllegalStateException(s"Pool exhausted (max: ${config.maxConnections})")
743
+
744
+ val id = nextId.incrementAndGet()
745
+ val conn = new PooledConnection(id, this)
746
+ active.incrementAndGet()
747
+ println(s" [Pool] Active connections: ${active.get()}/${config.maxConnections}")
748
+ conn
749
+ } { conn =>
750
+ conn.close()
751
+ }
752
+
753
+ private[examples] def release(conn: PooledConnection): Unit = {
754
+ val count = active.decrementAndGet()
755
+ println(s" [Conn#${conn.id}] Released back to pool (active: $count)")
756
+ }
757
+
758
+ def activeConnections: Int = active.get()
759
+
760
+ override def close(): Unit =
761
+ if (_closed.compareAndSet(0, 1)) {
762
+ println(s" [Pool] *** POOL CLOSED *** (served ${nextId.get()} total connections)")
763
+ }
764
+ }
765
+
766
+ @main def connectionPoolExample(): Unit = {
767
+ println("=== Connection Pool with Resource-based Acquire ===\n")
768
+
769
+ val poolConfig = PoolConfig(maxConnections = 3, timeout = 5000L)
770
+
771
+ val poolResource: Resource[ConnectionPool] =
772
+ Resource.fromAutoCloseable(new ConnectionPool(poolConfig))
773
+
774
+ Scope.global.scoped { appScope =>
775
+ import appScope._
776
+ println("[App] Allocating pool\n")
777
+ val pool: $[ConnectionPool] = poolResource.allocate
778
+
779
+ println("--- ServiceA doing work (connection scoped to this block) ---")
780
+ appScope.scoped { workScope =>
781
+ import workScope._
782
+ val p: $[ConnectionPool] = lower(pool)
783
+ val c: $[PooledConnection] = $(p)(_.acquire).allocate
784
+ val result = $(c)(_.execute("SELECT * FROM service_a_table"))
785
+ println(s" [ServiceA] Got: $result")
786
+ }
787
+ println()
788
+
789
+ println("--- ServiceB doing work ---")
790
+ appScope.scoped { workScope =>
791
+ import workScope._
792
+ val p: $[ConnectionPool] = lower(pool)
793
+ val c: $[PooledConnection] = $(p)(_.acquire).allocate
794
+ val result = $(c)(_.execute("SELECT * FROM service_b_table"))
795
+ println(s" [ServiceB] Got: $result")
796
+ }
797
+ println()
798
+
799
+ println("--- Multiple connections in same scope ---")
800
+ appScope.scoped { workScope =>
801
+ import workScope._
802
+ val p: $[ConnectionPool] = lower(pool)
803
+ val a: $[PooledConnection] = $(p)(_.acquire).allocate
804
+ val b: $[PooledConnection] = $(p)(_.acquire).allocate
805
+ val aId = $(a)(_.id)
806
+ val bId = $(b)(_.id)
807
+ println(s" [Parallel] Using connections $aId and $bId")
808
+ $(a)(_.execute("UPDATE table_a SET x = 1"))
809
+ $(b)(_.execute("UPDATE table_b SET y = 2"))
810
+ ()
811
+ }
812
+ println()
813
+
814
+ println("[App] All work complete, exiting app scope...")
815
+ }
816
+
817
+ println("\n=== Example Complete ===")
818
+ println("\nKey insight: pool.acquire returns Resource[PooledConnection],")
819
+ println("forcing proper scoped allocation and automatic release.")
820
+ }
821
+ ```
822
+
823
+ ([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/ConnectionPoolExample.scala))
824
+
825
+ ```bash
826
+ sbt "scope-examples/runMain scope.examples.ConnectionPoolExample"
827
+ ```
828
+
829
+ **Transactional resource management**
830
+
831
+ ```scala title="scope-examples/src/main/scala/scope/examples/TransactionBoundaryExample.scala"
832
+ /*
833
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
834
+ *
835
+ * Licensed under the Apache License, Version 2.0 (the "License");
836
+ * you may not use this file except in compliance with the License.
837
+ * You may obtain a copy of the License at
838
+ *
839
+ * http://www.apache.org/licenses/LICENSE-2.0
840
+ *
841
+ * Unless required by applicable law or agreed to in writing, software
842
+ * distributed under the License is distributed on an "AS IS" BASIS,
843
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
844
+ * See the License for the specific language governing permissions and
845
+ * limitations under the License.
846
+ */
847
+
848
+ package scope.examples
849
+
850
+ import zio.blocks.scope._
851
+
852
+ /**
853
+ * Transaction Boundary Example
854
+ *
855
+ * Demonstrates nested scopes and resource-returning methods for database
856
+ * transaction management.
857
+ *
858
+ * Key patterns shown:
859
+ * - '''Resource-returning methods''': `beginTransaction` returns
860
+ * `Resource[DbTransaction]`
861
+ * - '''Nested scopes''': Transactions live in child scopes of the connection
862
+ * - '''Automatic cleanup''': Uncommitted transactions auto-rollback on scope
863
+ * exit
864
+ * - '''LIFO ordering''': Transaction closes before connection
865
+ */
866
+ object TransactionBoundaryExample {
867
+
868
+ /** Simulates a database connection that can create transactions. */
869
+ class DbConnection(val id: String) extends AutoCloseable {
870
+ println(s" [DbConnection $id] Opened")
871
+
872
+ /**
873
+ * Begins a new transaction.
874
+ *
875
+ * Returns a `Resource[DbTransaction]` that must be allocated in a scope.
876
+ * This ensures the transaction is always properly closed (with rollback if
877
+ * not committed) when the scope exits.
878
+ */
879
+ def beginTransaction(txId: String): Resource[DbTransaction] =
880
+ Resource.acquireRelease {
881
+ new DbTransaction(this, txId)
882
+ } { tx =>
883
+ tx.close()
884
+ }
885
+
886
+ def close(): Unit =
887
+ println(s" [DbConnection $id] Closed")
888
+ }
889
+
890
+ /** Simulates an active database transaction. */
891
+ class DbTransaction(val conn: DbConnection, val id: String) extends AutoCloseable {
892
+ private var committed = false
893
+ private var rolledBack = false
894
+ println(s" [Tx $id] Started on connection ${conn.id}")
895
+
896
+ def execute(sql: String): Int = {
897
+ require(!committed && !rolledBack, s"Transaction $id already completed")
898
+ println(s" [Tx $id] Execute: $sql")
899
+ sql.hashCode.abs % 100 + 1
900
+ }
901
+
902
+ def commit(): Unit = {
903
+ require(!committed && !rolledBack, s"Transaction $id already completed")
904
+ committed = true
905
+ println(s" [Tx $id] Committed")
906
+ }
907
+
908
+ def rollback(): Unit =
909
+ if (!committed && !rolledBack) {
910
+ rolledBack = true
911
+ println(s" [Tx $id] Rolled back")
912
+ }
913
+
914
+ def close(): Unit = {
915
+ if (!committed && !rolledBack) {
916
+ println(s" [Tx $id] Auto-rollback (not committed)")
917
+ rollback()
918
+ }
919
+ println(s" [Tx $id] Closed")
920
+ }
921
+ }
922
+
923
+ /** Result of transaction operations. */
924
+ case class TxResult(success: Boolean, affectedRows: Int) derives Unscoped
925
+
926
+ @main def runTransactionBoundaryExample(): Unit = {
927
+ println("=== Transaction Boundary Example ===\n")
928
+ println("Demonstrating Resource-returning beginTransaction method\n")
929
+
930
+ Scope.global.scoped { connScope =>
931
+ import connScope._
932
+ // Allocate the connection in the outer scope
933
+ val conn: $[DbConnection] = Resource.fromAutoCloseable(new DbConnection("db-001")).allocate
934
+ println()
935
+
936
+ // Transaction 1: Successful insert
937
+ println("--- Transaction 1: Insert user ---")
938
+ val result1: TxResult =
939
+ connScope.scoped { txScope =>
940
+ import txScope._
941
+ val c: $[DbConnection] = lower(conn)
942
+ val tx: $[DbTransaction] = $(c)(_.beginTransaction("tx-001")).allocate
943
+ val rows = $(tx)(_.execute("INSERT INTO users VALUES (1, 'Alice')"))
944
+ $(tx)(_.commit())
945
+ TxResult(success = true, affectedRows = rows)
946
+ }
947
+ println(s" Result: $result1\n")
948
+
949
+ // Transaction 2: Transfer funds (multiple operations)
950
+ println("--- Transaction 2: Transfer funds ---")
951
+ val result2: TxResult =
952
+ connScope.scoped { txScope =>
953
+ import txScope._
954
+ val c: $[DbConnection] = lower(conn)
955
+ val tx: $[DbTransaction] = $(c)(_.beginTransaction("tx-002")).allocate
956
+ val rows1 = $(tx)(_.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1"))
957
+ val rows2 = $(tx)(_.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2"))
958
+ $(tx)(_.commit())
959
+ TxResult(success = true, affectedRows = rows1 + rows2)
960
+ }
961
+ println(s" Result: $result2\n")
962
+
963
+ // Transaction 3: Demonstrates auto-rollback on scope exit without commit
964
+ println("--- Transaction 3: Auto-rollback (no explicit commit) ---")
965
+ val result3: TxResult =
966
+ connScope.scoped { txScope =>
967
+ import txScope._
968
+ val c: $[DbConnection] = lower(conn)
969
+ val tx: $[DbTransaction] = $(c)(_.beginTransaction("tx-003")).allocate
970
+ $(tx)(_.execute("DELETE FROM audit_log"))
971
+ println(" [App] Not committing - scope exit will trigger auto-rollback...")
972
+ TxResult(success = false, affectedRows = 0)
973
+ }
974
+ println(s" Result: $result3\n")
975
+
976
+ println("--- All transactions complete, connection still open ---")
977
+ println("--- Exiting connection scope ---")
978
+ }
979
+
980
+ println("\n=== Example complete ===")
981
+ println("\nKey insight: beginTransaction() returns Resource[DbTransaction],")
982
+ println("forcing proper scoped allocation and automatic cleanup.")
983
+ }
984
+ }
985
+ ```
986
+
987
+ ([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/TransactionBoundaryExample.scala))
988
+
989
+ ```bash
990
+ sbt "scope-examples/runMain scope.examples.TransactionBoundaryExample"
991
+ ```
992
+
993
+ **Multi-layer service construction**
994
+
995
+ ```scala title="scope-examples/src/main/scala/scope/examples/LayeredWebServiceExample.scala"
996
+ /*
997
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
998
+ *
999
+ * Licensed under the Apache License, Version 2.0 (the "License");
1000
+ * you may not use this file except in compliance with the License.
1001
+ * You may obtain a copy of the License at
1002
+ *
1003
+ * http://www.apache.org/licenses/LICENSE-2.0
1004
+ *
1005
+ * Unless required by applicable law or agreed to in writing, software
1006
+ * distributed under the License is distributed on an "AS IS" BASIS,
1007
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1008
+ * See the License for the specific language governing permissions and
1009
+ * limitations under the License.
1010
+ */
1011
+
1012
+ package scope.examples
1013
+
1014
+ import zio.blocks.scope._
1015
+
1016
+ /**
1017
+ * Demonstrates auto-wiring a layered web service using
1018
+ * `Resource.from[T](wires*)`.
1019
+ *
1020
+ * The macro automatically derives wires for concrete classes (Database,
1021
+ * UserRepository, UserController) while requiring only the leaf config value to
1022
+ * be provided explicitly. Resources are cleaned up in LIFO order when the scope
1023
+ * closes.
1024
+ *
1025
+ * Layer hierarchy:
1026
+ * {{{
1027
+ * AppConfig (leaf value via Wire)
1028
+ * ↓
1029
+ * Database (auto-wired, AutoCloseable)
1030
+ * ↓
1031
+ * UserRepository (auto-wired)
1032
+ * ↓
1033
+ * UserController (auto-wired, AutoCloseable)
1034
+ * }}}
1035
+ */
1036
+
1037
+ /** Application configuration - the leaf dependency provided via Wire(value). */
1038
+ case class WebAppConfig(dbUrl: String, serverPort: Int)
1039
+
1040
+ /** Domain model for users. */
1041
+ case class User(id: Long, name: String, email: String)
1042
+
1043
+ /** Database layer - acquires a connection and releases it on close. */
1044
+ class WebDatabase(config: WebAppConfig) extends AutoCloseable {
1045
+ println(s" [WebDatabase] Connecting to ${config.dbUrl}")
1046
+
1047
+ def execute(sql: String): Int = {
1048
+ println(s" [WebDatabase] Executing: $sql")
1049
+ 1
1050
+ }
1051
+
1052
+ def close(): Unit = println(" [WebDatabase] Connection closed")
1053
+ }
1054
+
1055
+ /** Repository layer - provides data access using the database. */
1056
+ class UserRepository(db: WebDatabase) {
1057
+ println(" [UserRepository] Initialized")
1058
+
1059
+ private var nextId = 1L
1060
+
1061
+ def findById(id: Long): Option[User] = {
1062
+ db.execute(s"SELECT * FROM users WHERE id = $id")
1063
+ if (id > 0) Some(User(id, "Alice", "alice@example.com")) else None
1064
+ }
1065
+
1066
+ def save(user: User): Long = {
1067
+ db.execute(s"INSERT INTO users VALUES (${user.id}, '${user.name}', '${user.email}')")
1068
+ val id = nextId
1069
+ nextId += 1
1070
+ id
1071
+ }
1072
+ }
1073
+
1074
+ /** Controller layer - handles HTTP requests using the repository. */
1075
+ class UserController(repo: UserRepository) extends AutoCloseable {
1076
+ println(" [UserController] Ready to serve requests")
1077
+
1078
+ def getUser(id: Long): String =
1079
+ repo.findById(id).map(u => s"User(${u.id}, ${u.name})").getOrElse("Not found")
1080
+
1081
+ def createUser(name: String, email: String): String = {
1082
+ val id = repo.save(User(0, name, email))
1083
+ s"Created user with id=$id"
1084
+ }
1085
+
1086
+ def close(): Unit = println(" [UserController] Shutting down")
1087
+ }
1088
+
1089
+ /**
1090
+ * Entry point demonstrating the auto-wiring feature.
1091
+ *
1092
+ * Only `Wire(config)` is provided; the macro derives wires for Database,
1093
+ * UserRepository, and UserController from their constructors.
1094
+ */
1095
+ @main def layeredWebServiceExample(): Unit = {
1096
+ val config = WebAppConfig(dbUrl = "jdbc:postgresql://localhost:5432/mydb", serverPort = 8080)
1097
+
1098
+ println("=== Constructing layers (order: config → database → repository → controller) ===")
1099
+
1100
+ // Resource.from auto-wires the entire dependency graph
1101
+ val controllerResource: Resource[UserController] = Resource.from[UserController](
1102
+ Wire(config)
1103
+ )
1104
+
1105
+ // Allocate within a scoped block; cleanup runs on scope exit
1106
+ Scope.global.scoped { scope =>
1107
+ import scope._
1108
+ val controller: $[UserController] = allocate(controllerResource)
1109
+
1110
+ println("\n=== Handling requests ===")
1111
+ println(s" GET /users/1 → ${$(controller)(_.getUser(1))}")
1112
+ println(s" POST /users → ${$(controller)(_.createUser("Bob", "bob@example.com"))}")
1113
+
1114
+ println("\n=== Scope closing (LIFO cleanup: controller → database) ===")
1115
+ }
1116
+
1117
+ println("=== Done ===")
1118
+ }
1119
+ ```
1120
+
1121
+ ([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/LayeredWebServiceExample.scala))
1122
+
1123
+ ```bash
1124
+ sbt "scope-examples/runMain scope.examples.LayeredWebServiceExample"
1125
+ ```