@zio.dev/zio-blocks 0.0.28 → 0.0.30

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.
Files changed (35) hide show
  1. package/guides/compile-time-resource-safety-with-scope.md +997 -0
  2. package/guides/query-dsl-extending.md +1 -1
  3. package/guides/query-dsl-fluent-builder.md +203 -203
  4. package/guides/query-dsl-reified-optics.md +1 -1
  5. package/guides/query-dsl-sql.md +1 -1
  6. package/guides/zio-schema-migration.md +6 -6
  7. package/index.md +68 -16
  8. package/package.json +1 -1
  9. package/path-interpolator.md +70 -9
  10. package/reference/allows.md +96 -0
  11. package/reference/codec.md +8 -8
  12. package/reference/combinators.md +345 -0
  13. package/reference/context.md +639 -67
  14. package/reference/docs.md +1 -1
  15. package/reference/dynamic-schema.md +39 -42
  16. package/reference/http-model.md +1716 -0
  17. package/reference/json-differ.md +320 -0
  18. package/reference/json-patch.md +1 -1
  19. package/reference/media-type.md +2 -2
  20. package/reference/resource-management/defer-handle.md +246 -0
  21. package/reference/resource-management/finalization.md +286 -0
  22. package/reference/resource-management/finalizer.md +167 -0
  23. package/reference/resource-management/index.md +44 -0
  24. package/reference/resource-management/resource.md +1607 -0
  25. package/reference/resource-management/scope.md +3026 -0
  26. package/reference/resource-management/unscoped.md +125 -0
  27. package/reference/resource-management/wire.md +878 -0
  28. package/reference/schema-evolution/as.md +4 -4
  29. package/reference/schema-evolution/into.md +2 -2
  30. package/reference/schema-expr.md +2 -2
  31. package/reference/streams.md +989 -0
  32. package/reference/type-class-derivation.md +31 -31
  33. package/ringbuffer.md +249 -0
  34. package/sidebars.js +19 -2
  35. package/scope.md +0 -1423
@@ -0,0 +1,1607 @@
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.30"
40
+ ```
41
+
42
+ For cross-platform (Scala.js):
43
+
44
+ ```scala
45
+ libraryDependencies += "dev.zio" %%% "zio-blocks-scope" % "0.0.30"
46
+ ```
47
+
48
+ Supported Scala versions: 2.13.x and 3.x.
49
+
50
+ ## Shared and Unique Resources
51
+
52
+ Resource offers two distinct types for managing instance lifecycles: **Shared** for singleton-like resources that are allocated once and reused across multiple scopes, and **Unique** for fresh resources created on each allocation. Understanding when to use each type is critical for building efficient and correct resource-management architectures.
53
+
54
+ ### Comparison
55
+
56
+ | Aspect | Shared | Unique |
57
+ |--------------------|----------------------------------------------------|-------------------------------------------|
58
+ | **Creation** | `Resource.shared(f)` | `Resource.unique(f)` or `Resource(value)` |
59
+ | **Memoization** | Yes, with reference counting | No, fresh per allocation |
60
+ | **When to use** | Expensive resources (DB connections, thread pools) | Per-request state, isolated handlers |
61
+ | **Instance reuse** | Same instance across all allocations | New instance per allocation |
62
+ | **Finalization** | Runs when last reference released | Runs immediately when scope closes |
63
+ | **Thread safety** | Lock-free atomic reference counting | Per-scope, no global coordination needed |
64
+
65
+ ### Shared Resources
66
+
67
+ **Shared resources are memoized**: the first allocation initializes the instance; subsequent allocations return the same reference with automatic reference counting. Finalizers run only when the last reference is released. Use shared resources for **expensive, globally singular components** — database connection pools, thread pools, logging systems, caches, and other heavyweight infrastructure that should exist exactly once for the lifetime of the application.
68
+
69
+ The canonical example is a **database connection pool**. Building a fresh pool for each service layer is wasteful and defeats pooling's purpose. Instead, wrap the pool in a shared resource: both the user service and order service allocate the same pool instance, with the system automatically tracking references and closing the pool only when the last service releases it.
70
+
71
+ Here's what shared acquisition looks like:
72
+
73
+ ```scala
74
+ object Resource {
75
+ def shared[A](f: Scope => A): Resource[A]
76
+ }
77
+ ```
78
+
79
+ When you allocate a shared resource, reference counting ensures cleanup happens exactly once:
80
+
81
+ ```scala
82
+ import zio.blocks.scope._
83
+
84
+ class DatabasePool extends AutoCloseable {
85
+ def close(): Unit = println("Pool closed (after all services released)")
86
+ }
87
+
88
+ val poolResource = Resource.shared { scope =>
89
+ println("Creating database pool (first allocation only)")
90
+ new DatabasePool
91
+ }
92
+
93
+ // Allocate in ServiceA
94
+ Scope.global.scoped { scopeA =>
95
+ import scopeA._
96
+ val poolA: $[DatabasePool] = poolResource.allocate
97
+ println("ServiceA allocated pool")
98
+
99
+ // Allocate in ServiceB within a nested scope
100
+ scopeA.scoped { scopeB =>
101
+ import scopeB._
102
+ val poolB: $[DatabasePool] = poolResource.allocate
103
+ println("ServiceB allocated same pool instance (reference count incremented)")
104
+
105
+ // Both services share the same underlying pool instance
106
+ println("Both ServiceA and ServiceB are using the same pool instance")
107
+ }
108
+ println("ServiceB released, but pool stays open for ServiceA (ref count -= 1)")
109
+ }
110
+ println("All services released, pool closed (ref count == 0)")
111
+ ```
112
+
113
+ ### Unique Resources
114
+
115
+ **Unique resources create fresh instances each time**: every allocation produces a new value. Finalizers run when their owning scope closes, independent of other allocations. Use unique resources for **per-request state, isolated services, and stateful handlers** — anything that should never be shared because it encapsulates request-specific or scope-specific data.
116
+
117
+ A typical scenario is **per-request caches**: each API request gets its own cache instance to avoid one request polluting another's cached data. Similarly, stateful handlers (parser state machines, transaction contexts, event buffers) need isolation to prevent cross-contamination.
118
+
119
+ Here's what unique acquisition looks like:
120
+
121
+ ```scala
122
+ object Resource {
123
+ def unique[A](f: Scope => A): Resource[A]
124
+ }
125
+ ```
126
+
127
+ When you allocate a unique resource, each allocation is independent:
128
+
129
+ ```scala
130
+ import zio.blocks.scope._
131
+
132
+ class RequestCache extends AutoCloseable {
133
+ def close(): Unit = println("Request cache closed")
134
+ }
135
+
136
+ val cacheResource = Resource.unique { scope =>
137
+ println("Creating new request cache")
138
+ new RequestCache
139
+ }
140
+
141
+ // Two allocations in the same scope produce different instances
142
+ Scope.global.scoped { scope =>
143
+ import scope._
144
+ println("Creating first cache...")
145
+ val cache1: $[RequestCache] = cacheResource.allocate
146
+
147
+ println("Creating second cache...")
148
+ val cache2: $[RequestCache] = cacheResource.allocate
149
+
150
+ println("Both caches are independent instances")
151
+ }
152
+ ```
153
+
154
+ ### Diamond Dependency Pattern
155
+
156
+ A classic architecture uses shared resources to solve **diamond dependencies**, where multiple services depend on the same expensive component. Consider an e-commerce application where both `ProductService` and `OrderService` depend on `Logger`:
157
+
158
+ ```
159
+ CachingApp
160
+ / \
161
+ / \
162
+ ProductService OrderService
163
+ \ /
164
+ \ /
165
+ Logger
166
+ ```
167
+
168
+ Without shared resources, each service would receive a different `Logger` instance. With `Resource.shared`, both services automatically receive the same singleton instance:
169
+
170
+ ```scala
171
+ import zio.blocks.scope._
172
+
173
+ class Logger extends AutoCloseable {
174
+ def log(msg: String): Unit = println(s"LOG: $msg")
175
+ def close(): Unit = println("Logger closed")
176
+ }
177
+
178
+ class ProductService(val logger: Logger) {
179
+ def findProduct(id: String): String = {
180
+ logger.log(s"Finding product $id")
181
+ s"Product-$id"
182
+ }
183
+ }
184
+
185
+ class OrderService(val logger: Logger) {
186
+ def createOrder(productId: String): String = {
187
+ logger.log(s"Creating order for $productId")
188
+ s"ORD-123"
189
+ }
190
+ }
191
+
192
+ class CachingApp(val productService: ProductService, val orderService: OrderService) extends AutoCloseable {
193
+ def close(): Unit = println("App closed")
194
+ }
195
+
196
+ Scope.global.scoped { scope =>
197
+ import scope._
198
+
199
+ // Use Wire.shared[Logger] to ensure only one instance is created
200
+ val app: $[CachingApp] = allocate(
201
+ Resource.from[CachingApp](
202
+ Wire.shared[Logger] // Both services get the same Logger instance
203
+ )
204
+ )
205
+
206
+ $(app) { a =>
207
+ println(s"ProductService and OrderService share Logger? ${a.productService.logger eq a.orderService.logger}")
208
+ a.productService.findProduct("P001")
209
+ a.orderService.createOrder("P001")
210
+ }
211
+ }
212
+ ```
213
+
214
+ In this pattern, `Resource.shared` (via `Wire.shared`) guarantees a single `Logger` instance across all services, eliminating duplication while maintaining proper cleanup semantics.
215
+
216
+ ### Multiple Shared Dependencies
217
+
218
+ Most applications need more than one shared resource. Here's a realistic example where `CachingApp` depends on both a shared `Logger` and a shared `MetricsCollector`. Both resources are singletons that all services share:
219
+
220
+ ```
221
+ CachingApp
222
+ / \
223
+ / \
224
+ / \
225
+ ProductService OrderService
226
+ / \ / \
227
+ / \ / \
228
+ Logger Metrics Logger Metrics
229
+ \ / \ /
230
+ \ / \ /
231
+ \/ \/
232
+ (shared singleton instances)
233
+ ```
234
+
235
+ Here's the implementation:
236
+
237
+ ```scala
238
+ import zio.blocks.scope._
239
+
240
+ class Logger extends AutoCloseable {
241
+ def log(msg: String): Unit = println(s"[LOG] $msg")
242
+ def close(): Unit = println("[Logger] Closed")
243
+ }
244
+
245
+ class MetricsCollector extends AutoCloseable {
246
+ private var eventCount = 0
247
+ def recordEvent(name: String): Unit = {
248
+ eventCount += 1
249
+ println(s"[METRICS] Event: $name (total: $eventCount)")
250
+ }
251
+ def close(): Unit = println(s"[MetricsCollector] Closed after $eventCount events")
252
+ }
253
+
254
+ class ProductService(val logger: Logger, val metrics: MetricsCollector) {
255
+ def findProduct(id: String): String = {
256
+ logger.log(s"Finding product $id")
257
+ metrics.recordEvent("product.find")
258
+ s"Product-$id"
259
+ }
260
+ }
261
+
262
+ class OrderService(val logger: Logger, val metrics: MetricsCollector) {
263
+ def createOrder(productId: String): String = {
264
+ logger.log(s"Creating order for $productId")
265
+ metrics.recordEvent("order.create")
266
+ s"ORD-123"
267
+ }
268
+ }
269
+
270
+ class CachingApp(
271
+ val productService: ProductService,
272
+ val orderService: OrderService
273
+ ) extends AutoCloseable {
274
+ def close(): Unit = println("[CachingApp] Closed")
275
+ }
276
+
277
+ Scope.global.scoped { scope =>
278
+ import scope._
279
+
280
+ // Both Logger and MetricsCollector are shared (singleton instances)
281
+ val app: $[CachingApp] = allocate(
282
+ Resource.from[CachingApp](
283
+ Wire.shared[Logger], // One Logger for all services
284
+ Wire.shared[MetricsCollector] // One MetricsCollector for all services
285
+ )
286
+ )
287
+
288
+ $(app) { a =>
289
+ println(s"ProductService and OrderService share Logger? ${a.productService.logger eq a.orderService.logger}")
290
+ println(s"ProductService and OrderService share Metrics? ${a.productService.metrics eq a.orderService.metrics}")
291
+
292
+ a.productService.findProduct("P001")
293
+ a.orderService.createOrder("P001")
294
+ }
295
+ }
296
+ ```
297
+
298
+ When this scope closes, both the `Logger` and `MetricsCollector` are finalized exactly once, regardless of how many services reference them. The macro automatically builds a dependency graph, verifies all wires, and ensures LIFO cleanup order.
299
+
300
+ ## Construction
301
+
302
+ Resources can be created in several ways: from values, from explicit acquire/release pairs, from `AutoCloseable` types, from custom functions, or derived automatically from a type's constructor using the `Resource.from` macros.
303
+
304
+ ### `Resource.apply` — Wrap a Value
305
+
306
+ Wraps a by-name value as a resource. If the value implements `AutoCloseable`, its `close()` method is automatically registered as a finalizer:
307
+
308
+ ```scala
309
+ object Resource {
310
+ def apply[A](value: => A): Resource[A]
311
+ }
312
+ ```
313
+
314
+ Here's how to use it:
315
+
316
+ ```scala
317
+ import zio.blocks.scope._
318
+
319
+ case class Config(debug: Boolean)
320
+
321
+ class Database(val name: String) extends AutoCloseable {
322
+ def close(): Unit = println(s"Closing database $name")
323
+ }
324
+
325
+ val configResource = Resource(Config(debug = true))
326
+ val dbResource = Resource(new Database("mydb"))
327
+ ```
328
+
329
+ ### `Resource.acquireRelease` — Explicit Lifecycle
330
+
331
+ Creates a resource with separate acquire and release functions. The acquire thunk runs during allocation; the release function is registered as a finalizer:
332
+
333
+ ```scala
334
+ object Resource {
335
+ def acquireRelease[A](acquire: => A)(release: A => Unit): Resource[A]
336
+ }
337
+ ```
338
+
339
+ Here's an example with file handling:
340
+
341
+ ```scala
342
+ import zio.blocks.scope._
343
+ import java.io.FileInputStream
344
+
345
+ val fileResource = Resource.acquireRelease {
346
+ new FileInputStream("data.txt")
347
+ } { stream =>
348
+ stream.close()
349
+ }
350
+ ```
351
+
352
+ ### `Resource.fromAutoCloseable` — Type-Safe Wrapping
353
+
354
+ Creates a resource specifically for `AutoCloseable` subtypes. This is a compile-time verified alternative to `Resource(value)` when you know the value is closeable:
355
+
356
+ ```scala
357
+ object Resource {
358
+ def fromAutoCloseable[A <: AutoCloseable](value: => A): Resource[A]
359
+ }
360
+ ```
361
+
362
+ Here's an example with a stream:
363
+
364
+ ```scala
365
+ import zio.blocks.scope._
366
+ import java.io.BufferedInputStream
367
+ import java.io.FileInputStream
368
+
369
+ val streamResource = Resource.fromAutoCloseable {
370
+ new BufferedInputStream(new FileInputStream("data.bin"))
371
+ }
372
+ ```
373
+
374
+ ### `Resource.shared` — Memoized with Reference Counting
375
+
376
+ Creates a shared resource that memoizes its value across multiple allocations using reference counting. The first allocation initializes the value using an `OpenScope` parented to `Scope.global`; subsequent allocations increment the reference count and return the same instance. Each scope that receives the shared value registers a finalizer that decrements the count. When the count reaches zero, the shared scope closes automatically. This mechanism is **thread-safe and lock-free**, implemented using `AtomicReference` with atomic compare-and-swap operations to avoid contention.
377
+
378
+ Under the hood, a shared resource progresses through four states: (1) **Uninitialized** — the resource has never been allocated and the first call triggers initialization; (2) **Pending** — initialization is in progress and other threads wait via spin-yield for the value to become available; (3) **Created** — the value is fully initialized and ready, each allocation increments the reference count, and each scope registers a finalizer to decrement it; (4) **Destroyed** — the reference count reached zero, the resource was cleaned up, and further allocations will fail. This state machine ensures that no matter how many threads try to allocate simultaneously, the underlying value initializes exactly once, and cleanup happens exactly once when the last reference is released.
379
+
380
+ Use shared resources for **expensive, singleton-like components** (database connection pools, thread pools, logging systems, caches) that should exist exactly once for the lifetime of the application, even when multiple services depend on them.
381
+
382
+ Here's the signature:
383
+
384
+ ```scala
385
+ object Resource {
386
+ def shared[A](f: Scope => A): Resource[A]
387
+ }
388
+ ```
389
+
390
+ Here's a realistic example showing reference counting and automatic cleanup:
391
+
392
+ ```scala
393
+ import zio.blocks.scope._
394
+
395
+ class ExpensiveComponent extends AutoCloseable {
396
+ println("ExpensiveComponent initialized (expensive operation)")
397
+ def close(): Unit = println("ExpensiveComponent cleaned up (last reference released)")
398
+ }
399
+
400
+ val sharedResource = Resource.shared { scope =>
401
+ new ExpensiveComponent()
402
+ }
403
+
404
+ // Multiple services allocating the same shared resource
405
+ Scope.global.scoped { scope =>
406
+ import scope._
407
+
408
+ // First allocation initializes the component
409
+ println("ServiceA allocating...")
410
+ val componentA: $[ExpensiveComponent] = sharedResource.allocate
411
+
412
+ // ServiceB in a nested scope receives the same instance
413
+ scope.scoped { innerScope =>
414
+ import innerScope._
415
+ println("ServiceB allocating (same instance, ref count += 1)...")
416
+ val componentB: $[ExpensiveComponent] = sharedResource.allocate
417
+ println("Both services have the same instance")
418
+ }
419
+
420
+ println("ServiceB released, but component stays alive (ref count -= 1)")
421
+ }
422
+
423
+ println("ServiceA released, component cleaned up (ref count == 0)")
424
+ ```
425
+
426
+ ### `Resource.unique` — Fresh Instances
427
+
428
+ 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:
429
+
430
+ ```scala
431
+ object Resource {
432
+ def unique[A](f: Scope => A): Resource[A]
433
+ }
434
+ ```
435
+
436
+ Here's an example showing fresh instances:
437
+
438
+ ```scala
439
+ import zio.blocks.scope._
440
+
441
+ var counter = 0
442
+
443
+ val uniqueResource = Resource.unique[Int] { _ =>
444
+ counter += 1
445
+ counter
446
+ }
447
+ ```
448
+
449
+ ### `Resource.from[T]` — Derive from Constructor
450
+
451
+ Derives a `Resource[T]` from `T`'s primary constructor, requiring no external dependencies.
452
+ If `T` extends `AutoCloseable`, `Resource.from` automatically registers its `close()` method
453
+ as a finalizer:
454
+
455
+ ```scala
456
+ object Resource {
457
+ def from[T]: Resource[T]
458
+ }
459
+ ```
460
+
461
+ To derive a resource for a type that has no constructor dependencies:
462
+
463
+ ```scala
464
+ import zio.blocks.scope._
465
+
466
+ class MetricsCollector extends AutoCloseable {
467
+ def record(event: String): Unit = println(s"Recording: $event")
468
+ def close(): Unit = println("MetricsCollector closed")
469
+ }
470
+
471
+ val metricsResource = Resource.from[MetricsCollector]
472
+ ```
473
+
474
+ Internally, the macro inspects `T`'s constructor at compile time, verifies that no external
475
+ dependencies are needed, and synthesizes a `Resource.shared` that instantiates `T` and —
476
+ when `T` extends `AutoCloseable` — registers `close()` as a scope finalizer. Any attempt
477
+ to use `Resource.from[T]` on a type with unsatisfied constructor parameters results in a
478
+ compile-time error.
479
+
480
+ ### `Resource.from[T](wires: Wire[?, ?]*)` — Derive with Dependency Overrides
481
+
482
+ Derives a `Resource[T]` from `T`'s constructor, with `Wire` values provided as dependency
483
+ overrides. Any dependency not covered by an explicit wire is auto-derived if the macro can
484
+ construct it; otherwise a compile-time error is produced:
485
+
486
+ ```scala
487
+ object Resource {
488
+ def from[T](wires: Wire[?, ?]*): Resource[T]
489
+ }
490
+ ```
491
+
492
+ To derive a resource for a type whose dependencies are provided via wires:
493
+
494
+ ```scala
495
+ import zio.blocks.scope._
496
+
497
+ case class Config(host: String, port: Int)
498
+
499
+ class Logger(config: Config) extends AutoCloseable {
500
+ def log(msg: String): Unit = println(s"[$msg] ${config.host}:${config.port}")
501
+ def close(): Unit = log("Logger shutting down")
502
+ }
503
+
504
+ class Service(logger: Logger) extends AutoCloseable {
505
+ def run(): Unit = logger.log("Service running")
506
+ def close(): Unit = logger.log("Service shutting down")
507
+ }
508
+
509
+ val serviceResource = Resource.from[Service](
510
+ Wire(Config("localhost", 8080))
511
+ )
512
+ ```
513
+
514
+ Internally, the macro builds a complete dependency graph at compile time: it inspects each
515
+ provided wire's input and output types, identifies all constructor parameters of `T`, and
516
+ auto-derives wires for any remaining dependencies. It then topologically sorts all wires to
517
+ determine acquisition order and composes them into a single `Resource` whose finalizers run
518
+ in LIFO order — inner dependencies close before outer ones.
519
+
520
+ ## Core Operations
521
+
522
+ Resources support transformation and composition through `map`, `flatMap`, and `zip`.
523
+
524
+ ### `Resource#map` — Transform the Value
525
+
526
+ Transforms the value produced by a resource without affecting finalization. The transformation function is applied after the resource is acquired:
527
+
528
+ ```scala
529
+ trait Resource[+A] {
530
+ def map[B](f: A => B): Resource[B]
531
+ }
532
+ ```
533
+
534
+ Here's a usage example:
535
+
536
+ ```scala
537
+ import zio.blocks.scope._
538
+
539
+ val portResource = Resource(8080)
540
+ val urlResource = portResource.map(port => s"http://localhost:$port")
541
+ ```
542
+
543
+ ### `Resource#flatMap` — Sequence Resources
544
+
545
+ 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):
546
+
547
+ ```scala
548
+ trait Resource[+A] {
549
+ def flatMap[B](f: A => Resource[B]): Resource[B]
550
+ }
551
+ ```
552
+
553
+ Here's an example with dependent resources:
554
+
555
+ ```scala
556
+ import zio.blocks.scope._
557
+
558
+ case class DbConfig(url: String)
559
+
560
+ class Database(config: DbConfig) extends AutoCloseable {
561
+ def query(sql: String): String = s"Result from ${config.url}: $sql"
562
+ def close(): Unit = println("Database closed")
563
+ }
564
+
565
+ val configResource = Resource(DbConfig("jdbc:postgres://localhost"))
566
+ val dbResource = configResource.flatMap { config =>
567
+ Resource.fromAutoCloseable(new Database(config))
568
+ }
569
+ ```
570
+
571
+ ### `Resource#zip` — Combine Resources
572
+
573
+ 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:
574
+
575
+ ```scala
576
+ trait Resource[+A] {
577
+ def zip[B](that: Resource[B]): Resource[(A, B)]
578
+ }
579
+ ```
580
+
581
+ Here's an example combining multiple resources:
582
+
583
+ ```scala
584
+ import zio.blocks.scope._
585
+
586
+ case class DbConfig(url: String)
587
+
588
+ class Database(config: DbConfig) extends AutoCloseable {
589
+ def close(): Unit = println("Database closed")
590
+ }
591
+
592
+ class Cache extends AutoCloseable {
593
+ def close(): Unit = println("Cache closed")
594
+ }
595
+
596
+ val dbResource = Resource.fromAutoCloseable(new Database(DbConfig("jdbc:postgres://localhost")))
597
+ val cacheResource = Resource.fromAutoCloseable(new Cache())
598
+ val combined = dbResource.zip(cacheResource)
599
+ ```
600
+
601
+ ### `Resource#allocate` — Acquire Within a Scope
602
+
603
+ Allocates a `Resource[A]` within the current `Scope`, returning a scoped `$[A]`. This is syntax sugar for `scope.allocate(resource)`, available via `import scope._` inside a `scoped` block:
604
+
605
+ ```scala
606
+ implicit class ResourceOps[A](private val r: Resource[A]) {
607
+ def allocate: $[A]
608
+ }
609
+ ```
610
+
611
+ To allocate a resource and use its value inside a scope:
612
+
613
+ ```scala
614
+ import zio.blocks.scope._
615
+
616
+ class Database extends AutoCloseable {
617
+ def query(sql: String): String = s"Result: $sql"
618
+ def close(): Unit = println("Database closed")
619
+ }
620
+
621
+ Scope.global.scoped { implicit scope =>
622
+ import scope._
623
+ val db: $[Database] = Resource.fromAutoCloseable(new Database).allocate
624
+ $(db)(_.query("SELECT 1"))
625
+ }
626
+ ```
627
+
628
+ ### `$[Resource[A]]#allocate` — Allocate a Scoped Resource
629
+
630
+ Allocates a `$[Resource[A]]` — a Resource that is itself a scoped value — returning `$[A]`. Use this when a method on a scoped object returns a Resource and you need to immediately acquire it while keeping the result scoped.
631
+
632
+ **What is a scoped resource?** A scoped value `$[A]` represents an `A` that is valid only while a scope is alive. A **scoped resource** `$[Resource[A]]` is a Resource object that exists *inside* that scope. When you call a method like `$(pool)(_.lease())` that returns a Resource, the result is typed as `$[Resource[Connection]]` — the Resource itself is scoped. The `.allocate` extension method unwraps this scoped resource and acquires it, returning the acquired value as a new scoped value `$[A]`.
633
+
634
+ **Motivation:** This pattern appears frequently in resource factories. A scoped object (like a database pool) has methods that produce new Resources. Without this extension, you'd need to unwrap the `$[Resource[A]]` from the `$` context, allocate it separately, and re-wrap the result. The `.allocate` method chains naturally, letting you write `$(pool)(_.lease()).allocate` instead of dealing with intermediate unwrapping.
635
+
636
+ The implicit class:
637
+
638
+ ```scala
639
+ implicit class ScopedResourceOps[A](private val sr: $[Resource[A]]) {
640
+ def allocate: $[A]
641
+ }
642
+ ```
643
+
644
+ Here's a realistic example where a database pool (a scoped object) has a method that returns a Resource for individual connections:
645
+
646
+ ```scala
647
+ import zio.blocks.scope._
648
+
649
+ class Connection extends AutoCloseable {
650
+ def query(sql: String): String = s"Result: $sql"
651
+ def close(): Unit = println("Connection closed")
652
+ }
653
+
654
+ class Pool extends AutoCloseable {
655
+ def lease(): Resource[Connection] = Resource.fromAutoCloseable(new Connection)
656
+ def close(): Unit = println("Pool closed")
657
+ }
658
+
659
+ Scope.global.scoped { implicit scope =>
660
+ import scope._
661
+ val pool: $[Pool] = Resource.fromAutoCloseable(new Pool).allocate
662
+ val conn: $[Connection] = $(pool)(_.lease()).allocate
663
+ $(conn)(_.query("SELECT 1"))
664
+ }
665
+ ```
666
+
667
+
668
+ ## Integration
669
+
670
+ Resource is a core abstraction in ZIO Blocks' resource management ecosystem:
671
+
672
+ - **[`Scope`](./scope.md)** — Resources require a `Scope` for allocation. The `Scope` manages the lifetime of acquired resources and automatically runs finalizers when the scope closes.
673
+ - **[`Wire`](./wire.md)** — The `Resource.from[T](wires)` macro builds dependency graphs using `Wire` values, enabling constructor-based dependency injection with automatic resource management.
674
+ - **[`Finalizer`](./finalizer.md)** — Resources register their cleanup logic with `Finalizer` objects, which execute in LIFO order when scopes close.
675
+
676
+ For example, `Resource.from[T]` uses `Wire` to construct instances with their dependencies, automatically registering any `AutoCloseable` cleanup:
677
+
678
+ ```scala
679
+ val serviceResource = Resource.from[Service](
680
+ Wire(Config("localhost", 8080))
681
+ )
682
+ ```
683
+
684
+ This builds a complete dependency graph: `Config` → `Logger` → `Service`, with all finalizers managed by the containing `Scope`.
685
+
686
+ ## Running the Examples
687
+
688
+ All code from this guide is available as runnable examples in the `scope-examples` module.
689
+
690
+ **1. Clone the repository and navigate to the project:**
691
+
692
+ ```bash
693
+ git clone https://github.com/zio/zio-blocks.git
694
+ cd zio-blocks
695
+ ```
696
+
697
+ **2. Run individual examples with sbt:**
698
+
699
+ ### Basic Lifecycle Management with Temporary Files
700
+
701
+ This example demonstrates creating and automatically cleaning up temporary files using Resource's lifecycle management. It shows how Resource ensures files are closed even if exceptions occur:
702
+
703
+ ```scala title="scope-examples/src/main/scala/scope/examples/TempFileHandlingExample.scala"
704
+ /*
705
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
706
+ *
707
+ * Licensed under the Apache License, Version 2.0 (the "License");
708
+ * you may not use this file except in compliance with the License.
709
+ * You may obtain a copy of the License at
710
+ *
711
+ * http://www.apache.org/licenses/LICENSE-2.0
712
+ *
713
+ * Unless required by applicable law or agreed to in writing, software
714
+ * distributed under the License is distributed on an "AS IS" BASIS,
715
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
716
+ * See the License for the specific language governing permissions and
717
+ * limitations under the License.
718
+ */
719
+
720
+ package scope.examples
721
+
722
+ import zio.blocks.scope._
723
+
724
+ /**
725
+ * Demonstrates `scope.defer(...)` for registering manual cleanup actions.
726
+ *
727
+ * This example shows how to create temporary files during processing and ensure
728
+ * they are deleted when the scope exits—even if processing fails. Deferred
729
+ * cleanup actions run in LIFO (last-in-first-out) order.
730
+ */
731
+
732
+ /** Represents a temporary file with basic read/write operations. */
733
+ case class TempFile(path: String) {
734
+ private var content: String = ""
735
+
736
+ def write(data: String): Unit = content = data
737
+ def read(): String = content
738
+ def delete(): Boolean = { println(s" Deleting: $path"); true }
739
+ }
740
+
741
+ /** Result of processing temporary files. */
742
+ case class ProcessingResult(processedCount: Int, totalBytes: Long, errors: List[String])
743
+
744
+ /** Processes a list of temporary files and aggregates results. */
745
+ object FileProcessor {
746
+ def process(files: List[TempFile]): ProcessingResult = {
747
+ val totalBytes = files.map(_.read().length.toLong).sum
748
+ ProcessingResult(processedCount = files.size, totalBytes = totalBytes, errors = Nil)
749
+ }
750
+ }
751
+
752
+ @main def tempFileHandlingExample(): Unit = {
753
+ println("=== Temp File Handling Example ===\n")
754
+ println("Demonstrating scope.defer() for manual cleanup registration.\n")
755
+
756
+ val result = Scope.global.scoped { scope =>
757
+ // Create temp files and register cleanup via defer.
758
+ // Cleanup runs in LIFO order: file3, file2, file1.
759
+
760
+ val file1 = createTempFile(scope, "/tmp/data-001.tmp", "First file content")
761
+ val file2 = createTempFile(scope, "/tmp/data-002.tmp", "Second file - more data here")
762
+ val file3 = createTempFile(scope, "/tmp/data-003.tmp", "Third file with the most content of all")
763
+
764
+ println("\nProcessing files...")
765
+ val processingResult = FileProcessor.process(List(file1, file2, file3))
766
+ println(s"Processed ${processingResult.processedCount} files, ${processingResult.totalBytes} bytes\n")
767
+
768
+ println("Exiting scope - cleanup runs in LIFO order:")
769
+ processingResult
770
+ }
771
+
772
+ println(s"\nFinal result: $result")
773
+ }
774
+
775
+ /**
776
+ * Creates a temporary file and registers its cleanup with the scope.
777
+ *
778
+ * The cleanup action is registered via `defer(...)`, ensuring the file is
779
+ * deleted when the scope closes—regardless of whether processing succeeds.
780
+ *
781
+ * @param s
782
+ * the scope to register cleanup with
783
+ * @param path
784
+ * the file path
785
+ * @param content
786
+ * initial content to write
787
+ * @return
788
+ * the created TempFile
789
+ */
790
+ private def createTempFile(s: Scope, path: String, content: String): TempFile = {
791
+ val file = TempFile(path)
792
+ file.write(content)
793
+ println(s"Created: $path (${content.length} bytes)")
794
+
795
+ // Register cleanup - will run when scope exits, in LIFO order
796
+ s.defer {
797
+ file.delete()
798
+ }
799
+
800
+ file
801
+ }
802
+ ```
803
+
804
+ ([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/TempFileHandlingExample.scala))
805
+
806
+ Run this example with:
807
+
808
+ ```bash
809
+ sbt "scope-examples/runMain scope.examples.tempFileHandlingExample"
810
+ ```
811
+
812
+ ### Acquiring and Releasing Database Connections
813
+
814
+ This example demonstrates the acquire-release pattern using Resource to manage database connections. It shows proper connection initialization and guaranteed cleanup:
815
+
816
+ ```scala title="scope-examples/src/main/scala/scope/examples/DatabaseConnectionExample.scala"
817
+ /*
818
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
819
+ *
820
+ * Licensed under the Apache License, Version 2.0 (the "License");
821
+ * you may not use this file except in compliance with the License.
822
+ * You may obtain a copy of the License at
823
+ *
824
+ * http://www.apache.org/licenses/LICENSE-2.0
825
+ *
826
+ * Unless required by applicable law or agreed to in writing, software
827
+ * distributed under the License is distributed on an "AS IS" BASIS,
828
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
829
+ * See the License for the specific language governing permissions and
830
+ * limitations under the License.
831
+ */
832
+
833
+ package scope.examples
834
+
835
+ import zio.blocks.scope._
836
+
837
+ /**
838
+ * Configuration for database connection.
839
+ *
840
+ * @param host
841
+ * the database server hostname
842
+ * @param port
843
+ * the database server port
844
+ * @param database
845
+ * the database name to connect to
846
+ */
847
+ final case class DbConfig(host: String, port: Int, database: String) {
848
+ def connectionUrl: String = s"jdbc:postgresql://$host:$port/$database"
849
+ }
850
+
851
+ /**
852
+ * Represents the result of a database query.
853
+ *
854
+ * @param rows
855
+ * the result set as a list of row maps
856
+ */
857
+ final case class QueryResult(rows: List[Map[String, String]]) {
858
+ def isEmpty: Boolean = rows.isEmpty
859
+ def size: Int = rows.size
860
+ }
861
+
862
+ /**
863
+ * Simulates a database connection with lifecycle management.
864
+ *
865
+ * This class demonstrates how AutoCloseable resources integrate with ZIO Blocks
866
+ * Scope. When allocated via `allocate(Resource(...))`, the `close()` method is
867
+ * automatically registered as a finalizer.
868
+ *
869
+ * @param config
870
+ * the database configuration
871
+ */
872
+ final class Database(config: DbConfig) extends AutoCloseable {
873
+ private var connected = false
874
+
875
+ def connect(): Unit = {
876
+ println(s"[Database] Connecting to ${config.connectionUrl}...")
877
+ connected = true
878
+ println(s"[Database] Connected successfully")
879
+ }
880
+
881
+ def query(sql: String): QueryResult = {
882
+ require(connected, "Database not connected")
883
+ println(s"[Database] Executing: $sql")
884
+ sql match {
885
+ case s if s.contains("users") =>
886
+ QueryResult(
887
+ List(
888
+ Map("id" -> "1", "name" -> "Alice"),
889
+ Map("id" -> "2", "name" -> "Bob")
890
+ )
891
+ )
892
+ case s if s.contains("orders") =>
893
+ QueryResult(
894
+ List(
895
+ Map("order_id" -> "101", "user_id" -> "1", "total" -> "99.99"),
896
+ Map("order_id" -> "102", "user_id" -> "2", "total" -> "149.50")
897
+ )
898
+ )
899
+ case _ =>
900
+ QueryResult(List(Map("result" -> "OK")))
901
+ }
902
+ }
903
+
904
+ override def close(): Unit = {
905
+ println(s"[Database] Closing connection to ${config.connectionUrl}")
906
+ connected = false
907
+ }
908
+ }
909
+
910
+ /**
911
+ * Demonstrates basic resource lifecycle management with ZIO Blocks Scope.
912
+ *
913
+ * This example shows:
914
+ * - Allocating an AutoCloseable resource with automatic cleanup
915
+ * - Using `$(value)(f)` to access scoped values and execute queries
916
+ * - LIFO finalizer ordering (last allocated = first closed)
917
+ *
918
+ * When the scope exits, all registered finalizers run in reverse order,
919
+ * ensuring proper cleanup even if exceptions occur.
920
+ */
921
+ @main def runDatabaseExample(): Unit = {
922
+ println("=== Database Connection Example ===\n")
923
+
924
+ val config = DbConfig("localhost", 5432, "myapp")
925
+
926
+ Scope.global.scoped { scope =>
927
+ import scope._
928
+ println("[Scope] Entering scoped region\n")
929
+
930
+ // Allocate the database resource. Because Database extends AutoCloseable,
931
+ // its close() method is automatically registered as a finalizer.
932
+ val db: $[Database] = allocate(Resource {
933
+ val database = new Database(config)
934
+ database.connect()
935
+ database
936
+ })
937
+
938
+ // Use $(value)(f) to access the scoped value and execute queries.
939
+ $(db) { database =>
940
+ val users = database.query("SELECT * FROM users")
941
+ println(s"[Result] Found ${users.size} users: ${users.rows.map(_("name")).mkString(", ")}\n")
942
+
943
+ val orders = database.query("SELECT * FROM orders WHERE status = 'pending'")
944
+ println(s"[Result] Found ${orders.size} orders\n")
945
+
946
+ val health = database.query("SELECT 1 AS health_check")
947
+ println(s"[Result] Health check: ${health.rows.head("result")}\n")
948
+ }
949
+
950
+ println("[Scope] Exiting scoped region - finalizers will run in LIFO order")
951
+ }
952
+
953
+ println("\n=== Example Complete ===")
954
+ }
955
+ ```
956
+
957
+ ([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/DatabaseConnectionExample.scala))
958
+
959
+ Run this example with:
960
+
961
+ ```bash
962
+ sbt "scope-examples/runMain scope.examples.runDatabaseExample"
963
+ ```
964
+
965
+ ### Shared Resources with Memoization and Reference Counting
966
+
967
+ This example demonstrates Resource.shared to create a singleton logger instance that is automatically cleaned up only when the last service releases it. Shows reference counting in action:
968
+
969
+ ```scala title="scope-examples/src/main/scala/scope/examples/CachingSharedLoggerExample.scala"
970
+ /*
971
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
972
+ *
973
+ * Licensed under the Apache License, Version 2.0 (the "License");
974
+ * you may not use this file except in compliance with the License.
975
+ * You may obtain a copy of the License at
976
+ *
977
+ * http://www.apache.org/licenses/LICENSE-2.0
978
+ *
979
+ * Unless required by applicable law or agreed to in writing, software
980
+ * distributed under the License is distributed on an "AS IS" BASIS,
981
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
982
+ * See the License for the specific language governing permissions and
983
+ * limitations under the License.
984
+ */
985
+
986
+ package scope.examples
987
+
988
+ import zio.blocks.scope._
989
+ import java.util.concurrent.atomic.AtomicInteger
990
+
991
+ /**
992
+ * Demonstrates `Wire.shared` vs `Wire.unique` and diamond dependency patterns.
993
+ *
994
+ * Two services (ProductService, OrderService) share one Logger instance
995
+ * (diamond pattern), but each gets its own unique Cache instance. This shows
996
+ * how shared wires provide singleton behavior while unique wires create fresh
997
+ * instances per injection site.
998
+ *
999
+ * Key concepts:
1000
+ * - `Wire.shared[T]`: Single instance shared across all dependents (memoized)
1001
+ * - `Wire.unique[T]`: Fresh instance created for each dependent
1002
+ * - Diamond dependency: Multiple services depend on the same shared resource
1003
+ * - Reference counting: Shared resources track usage and clean up when last
1004
+ * user closes
1005
+ */
1006
+ object CachingSharedLoggerExample {
1007
+
1008
+ /** Tracks instantiation counts for demonstration purposes. */
1009
+ val loggerInstances = new AtomicInteger(0)
1010
+ val cacheInstances = new AtomicInteger(0)
1011
+
1012
+ /**
1013
+ * A shared logger that tracks instantiations and provides logging methods.
1014
+ * Implements AutoCloseable for proper resource cleanup.
1015
+ */
1016
+ class Logger extends AutoCloseable {
1017
+ val instanceId: Int = loggerInstances.incrementAndGet()
1018
+ println(s" [Logger#$instanceId] Created")
1019
+
1020
+ def info(msg: String): Unit = println(s" [Logger#$instanceId] INFO: $msg")
1021
+ def debug(msg: String): Unit = println(s" [Logger#$instanceId] DEBUG: $msg")
1022
+ def close(): Unit = println(s" [Logger#$instanceId] Closed")
1023
+ }
1024
+
1025
+ /**
1026
+ * A unique cache per service. Each service gets its own isolated cache
1027
+ * instance. Implements AutoCloseable for proper resource cleanup. Note: No
1028
+ * constructor params so it can be auto-wired with Wire.unique.
1029
+ */
1030
+ class Cache extends AutoCloseable {
1031
+ val instanceId: Int = cacheInstances.incrementAndGet()
1032
+ private var store: Map[String, String] = Map.empty
1033
+ println(s" [Cache#$instanceId] Created")
1034
+
1035
+ def get(key: String): Option[String] = store.get(key)
1036
+ def put(key: String, value: String): Unit = store = store.updated(key, value)
1037
+ def close(): Unit = println(s" [Cache#$instanceId] Closed")
1038
+ }
1039
+
1040
+ /** Product service with its own cache but sharing the logger. */
1041
+ class ProductService(val logger: Logger, val cache: Cache) {
1042
+ println(s" [ProductService] Created with Logger#${logger.instanceId} and Cache#${cache.instanceId}")
1043
+
1044
+ def findProduct(id: String): String =
1045
+ cache.get(id) match {
1046
+ case Some(product) =>
1047
+ logger.debug(s"Cache hit for product $id")
1048
+ product
1049
+ case None =>
1050
+ logger.info(s"Loading product $id from database")
1051
+ val product = s"Product-$id"
1052
+ cache.put(id, product)
1053
+ product
1054
+ }
1055
+ }
1056
+
1057
+ /**
1058
+ * Order service with its own cache but sharing the same logger as
1059
+ * ProductService.
1060
+ */
1061
+ class OrderService(val logger: Logger, val cache: Cache) {
1062
+ println(s" [OrderService] Created with Logger#${logger.instanceId} and Cache#${cache.instanceId}")
1063
+
1064
+ def createOrder(productId: String): String = {
1065
+ val orderId = s"ORD-${System.currentTimeMillis() % 10000}"
1066
+ cache.put(orderId, productId)
1067
+ logger.info(s"Created order $orderId for product $productId")
1068
+ orderId
1069
+ }
1070
+ }
1071
+
1072
+ /** Top-level application combining both services. */
1073
+ class CachingApp(val productService: ProductService, val orderService: OrderService) extends AutoCloseable {
1074
+ def run(): Unit = {
1075
+ productService.logger.info("=== Application Started ===")
1076
+ val product = productService.findProduct("P001")
1077
+ orderService.createOrder(product)
1078
+ productService.findProduct("P001") // cache hit
1079
+ }
1080
+ def close(): Unit = println(" [CachingApp] Closed")
1081
+ }
1082
+
1083
+ @main def runCachingExample(): Unit = {
1084
+ println("\n╔════════════════════════════════════════════════════════════════╗")
1085
+ println("║ Wire.shared vs Wire.unique - Diamond Dependency Example ║")
1086
+ println("╚════════════════════════════════════════════════════════════════╝\n")
1087
+
1088
+ println("Creating wires...")
1089
+ println(" - Logger: Wire.shared (singleton across all services)")
1090
+ println(" - Cache: Wire.unique (fresh instance per service)\n")
1091
+
1092
+ println("─── Resource Acquisition ───")
1093
+ Scope.global.scoped { scope =>
1094
+ import scope._
1095
+ val app: $[CachingApp] = allocate(
1096
+ Resource.from[CachingApp](
1097
+ Wire.shared[Logger],
1098
+ Wire.unique[Cache]
1099
+ )
1100
+ )
1101
+
1102
+ println("\n─── Verification ───")
1103
+ println(s" Logger instances created: ${loggerInstances.get()} (expected: 1)")
1104
+ println(s" Cache instances created: ${cacheInstances.get()} (expected: 2)")
1105
+ $(app) { a =>
1106
+ println(s" ProductService.logger eq OrderService.logger: ${a.productService.logger eq a.orderService.logger}")
1107
+ println(s" ProductService.cache eq OrderService.cache: ${a.productService.cache eq a.orderService.cache}")
1108
+
1109
+ println("\n─── Running Application ───")
1110
+ a.run()
1111
+ }
1112
+
1113
+ println("\n─── Scope Closing (LIFO cleanup) ───")
1114
+ }
1115
+
1116
+ println("\n─── Summary ───")
1117
+ println(s" Final Logger count: ${loggerInstances.get()} (shared = 1 instance)")
1118
+ println(s" Final Cache count: ${cacheInstances.get()} (unique = 2 instances)")
1119
+ println("\nDiamond pattern verified: both services received the same Logger instance.")
1120
+ }
1121
+ }
1122
+ ```
1123
+
1124
+ ([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/CachingSharedLoggerExample.scala))
1125
+
1126
+ Run this example with:
1127
+
1128
+ ```bash
1129
+ sbt "scope-examples/runMain scope.examples.runCachingExample"
1130
+ ```
1131
+
1132
+ ### Managing Shared Expensive Resources
1133
+
1134
+ This example demonstrates using Resource.shared for a database connection pool—an expensive resource that should exist exactly once. Shows how multiple services safely share the same pool instance with automatic cleanup:
1135
+
1136
+ ```scala title="scope-examples/src/main/scala/scope/examples/ConnectionPoolExample.scala"
1137
+ /*
1138
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
1139
+ *
1140
+ * Licensed under the Apache License, Version 2.0 (the "License");
1141
+ * you may not use this file except in compliance with the License.
1142
+ * You may obtain a copy of the License at
1143
+ *
1144
+ * http://www.apache.org/licenses/LICENSE-2.0
1145
+ *
1146
+ * Unless required by applicable law or agreed to in writing, software
1147
+ * distributed under the License is distributed on an "AS IS" BASIS,
1148
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1149
+ * See the License for the specific language governing permissions and
1150
+ * limitations under the License.
1151
+ */
1152
+
1153
+ package scope.examples
1154
+
1155
+ import zio.blocks.scope._
1156
+ import java.util.concurrent.atomic.AtomicInteger
1157
+
1158
+ /**
1159
+ * Demonstrates `Resource.Shared` with reference counting and nested resource
1160
+ * acquisition.
1161
+ *
1162
+ * This example shows a realistic connection pool pattern where:
1163
+ * - The pool itself is a shared resource (created once, ref-counted)
1164
+ * - Individual connections are resources that must be allocated in a scope
1165
+ * - `pool.acquire` returns `Resource[PooledConnection]`, forcing proper
1166
+ * scoping
1167
+ *
1168
+ * This pattern is common for database pools, HTTP client pools, and thread
1169
+ * pools.
1170
+ */
1171
+
1172
+ /** Configuration for the connection pool. */
1173
+ final case class PoolConfig(maxConnections: Int, timeout: Long)
1174
+
1175
+ /**
1176
+ * A connection retrieved from the pool.
1177
+ *
1178
+ * Connections are resources - they must be released back to the pool when done.
1179
+ * This is enforced by making `acquire` return a `Resource[PooledConnection]`.
1180
+ */
1181
+ final class PooledConnection(val id: Int, pool: ConnectionPool) extends AutoCloseable {
1182
+ println(s" [Conn#$id] Acquired from pool")
1183
+
1184
+ def execute(sql: String): String = {
1185
+ println(s" [Conn#$id] Executing: $sql")
1186
+ s"Result from connection $id"
1187
+ }
1188
+
1189
+ override def close(): Unit =
1190
+ pool.release(this)
1191
+ }
1192
+
1193
+ /**
1194
+ * A connection pool that manages pooled connections.
1195
+ *
1196
+ * Key design: `acquire` returns `Resource[PooledConnection]`, not a raw
1197
+ * connection. This forces callers to allocate the connection in a scope,
1198
+ * ensuring proper release even if exceptions occur.
1199
+ */
1200
+ final class ConnectionPool(config: PoolConfig) extends AutoCloseable {
1201
+ private val nextId = new AtomicInteger(0)
1202
+ private val active = new AtomicInteger(0)
1203
+ private val _closed = new AtomicInteger(0)
1204
+
1205
+ println(s" [Pool] Created with max ${config.maxConnections} connections")
1206
+
1207
+ /**
1208
+ * Acquires a connection from the pool.
1209
+ *
1210
+ * Returns a `Resource[PooledConnection]` that must be allocated in a scope.
1211
+ * The connection is automatically released when the scope exits.
1212
+ */
1213
+ def acquire: Resource[PooledConnection] = Resource.acquireRelease {
1214
+ if (_closed.get() > 0) throw new IllegalStateException("Pool is closed")
1215
+ if (active.get() >= config.maxConnections)
1216
+ throw new IllegalStateException(s"Pool exhausted (max: ${config.maxConnections})")
1217
+
1218
+ val id = nextId.incrementAndGet()
1219
+ val conn = new PooledConnection(id, this)
1220
+ active.incrementAndGet()
1221
+ println(s" [Pool] Active connections: ${active.get()}/${config.maxConnections}")
1222
+ conn
1223
+ } { conn =>
1224
+ conn.close()
1225
+ }
1226
+
1227
+ private[examples] def release(conn: PooledConnection): Unit = {
1228
+ val count = active.decrementAndGet()
1229
+ println(s" [Conn#${conn.id}] Released back to pool (active: $count)")
1230
+ }
1231
+
1232
+ def activeConnections: Int = active.get()
1233
+
1234
+ override def close(): Unit =
1235
+ if (_closed.compareAndSet(0, 1)) {
1236
+ println(s" [Pool] *** POOL CLOSED *** (served ${nextId.get()} total connections)")
1237
+ }
1238
+ }
1239
+
1240
+ @main def connectionPoolExample(): Unit = {
1241
+ println("=== Connection Pool with Resource-based Acquire ===\n")
1242
+
1243
+ val poolConfig = PoolConfig(maxConnections = 3, timeout = 5000L)
1244
+
1245
+ val poolResource: Resource[ConnectionPool] =
1246
+ Resource.fromAutoCloseable(new ConnectionPool(poolConfig))
1247
+
1248
+ Scope.global.scoped { appScope =>
1249
+ import appScope._
1250
+ println("[App] Allocating pool\n")
1251
+ val pool: $[ConnectionPool] = poolResource.allocate
1252
+
1253
+ println("--- ServiceA doing work (connection scoped to this block) ---")
1254
+ appScope.scoped { workScope =>
1255
+ import workScope._
1256
+ val p: $[ConnectionPool] = lower(pool)
1257
+ val c: $[PooledConnection] = $(p)(_.acquire).allocate
1258
+ val result = $(c)(_.execute("SELECT * FROM service_a_table"))
1259
+ println(s" [ServiceA] Got: $result")
1260
+ }
1261
+ println()
1262
+
1263
+ println("--- ServiceB doing work ---")
1264
+ appScope.scoped { workScope =>
1265
+ import workScope._
1266
+ val p: $[ConnectionPool] = lower(pool)
1267
+ val c: $[PooledConnection] = $(p)(_.acquire).allocate
1268
+ val result = $(c)(_.execute("SELECT * FROM service_b_table"))
1269
+ println(s" [ServiceB] Got: $result")
1270
+ }
1271
+ println()
1272
+
1273
+ println("--- Multiple connections in same scope ---")
1274
+ appScope.scoped { workScope =>
1275
+ import workScope._
1276
+ val p: $[ConnectionPool] = lower(pool)
1277
+ val a: $[PooledConnection] = $(p)(_.acquire).allocate
1278
+ val b: $[PooledConnection] = $(p)(_.acquire).allocate
1279
+ val aId = $(a)(_.id)
1280
+ val bId = $(b)(_.id)
1281
+ println(s" [Parallel] Using connections $aId and $bId")
1282
+ $(a)(_.execute("UPDATE table_a SET x = 1"))
1283
+ $(b)(_.execute("UPDATE table_b SET y = 2"))
1284
+ ()
1285
+ }
1286
+ println()
1287
+
1288
+ println("[App] All work complete, exiting app scope...")
1289
+ }
1290
+
1291
+ println("\n=== Example Complete ===")
1292
+ println("\nKey insight: pool.acquire returns Resource[PooledConnection],")
1293
+ println("forcing proper scoped allocation and automatic release.")
1294
+ }
1295
+ ```
1296
+
1297
+ ([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/ConnectionPoolExample.scala))
1298
+
1299
+ Run this example with:
1300
+
1301
+ ```bash
1302
+ sbt "scope-examples/runMain scope.examples.connectionPoolExample"
1303
+ ```
1304
+
1305
+ ### Transactional Resource Management
1306
+
1307
+ This example demonstrates combining Resource with transaction boundaries. Shows how to manage resources (connections, transactions) that must be coordinated across scopes with proper rollback on failure:
1308
+
1309
+ ```scala title="scope-examples/src/main/scala/scope/examples/TransactionBoundaryExample.scala"
1310
+ /*
1311
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
1312
+ *
1313
+ * Licensed under the Apache License, Version 2.0 (the "License");
1314
+ * you may not use this file except in compliance with the License.
1315
+ * You may obtain a copy of the License at
1316
+ *
1317
+ * http://www.apache.org/licenses/LICENSE-2.0
1318
+ *
1319
+ * Unless required by applicable law or agreed to in writing, software
1320
+ * distributed under the License is distributed on an "AS IS" BASIS,
1321
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1322
+ * See the License for the specific language governing permissions and
1323
+ * limitations under the License.
1324
+ */
1325
+
1326
+ package scope.examples
1327
+
1328
+ import zio.blocks.scope._
1329
+
1330
+ /**
1331
+ * Transaction Boundary Example
1332
+ *
1333
+ * Demonstrates nested scopes and resource-returning methods for database
1334
+ * transaction management.
1335
+ *
1336
+ * Key patterns shown:
1337
+ * - '''Resource-returning methods''': `beginTransaction` returns
1338
+ * `Resource[DbTransaction]`
1339
+ * - '''Nested scopes''': Transactions live in child scopes of the connection
1340
+ * - '''Automatic cleanup''': Uncommitted transactions auto-rollback on scope
1341
+ * exit
1342
+ * - '''LIFO ordering''': Transaction closes before connection
1343
+ */
1344
+ object TransactionBoundaryExample {
1345
+
1346
+ /** Simulates a database connection that can create transactions. */
1347
+ class DbConnection(val id: String) extends AutoCloseable {
1348
+ println(s" [DbConnection $id] Opened")
1349
+
1350
+ /**
1351
+ * Begins a new transaction.
1352
+ *
1353
+ * Returns a `Resource[DbTransaction]` that must be allocated in a scope.
1354
+ * This ensures the transaction is always properly closed (with rollback if
1355
+ * not committed) when the scope exits.
1356
+ */
1357
+ def beginTransaction(txId: String): Resource[DbTransaction] =
1358
+ Resource.acquireRelease {
1359
+ new DbTransaction(this, txId)
1360
+ } { tx =>
1361
+ tx.close()
1362
+ }
1363
+
1364
+ def close(): Unit =
1365
+ println(s" [DbConnection $id] Closed")
1366
+ }
1367
+
1368
+ /** Simulates an active database transaction. */
1369
+ class DbTransaction(val conn: DbConnection, val id: String) extends AutoCloseable {
1370
+ private var committed = false
1371
+ private var rolledBack = false
1372
+ println(s" [Tx $id] Started on connection ${conn.id}")
1373
+
1374
+ def execute(sql: String): Int = {
1375
+ require(!committed && !rolledBack, s"Transaction $id already completed")
1376
+ println(s" [Tx $id] Execute: $sql")
1377
+ sql.hashCode.abs % 100 + 1
1378
+ }
1379
+
1380
+ def commit(): Unit = {
1381
+ require(!committed && !rolledBack, s"Transaction $id already completed")
1382
+ committed = true
1383
+ println(s" [Tx $id] Committed")
1384
+ }
1385
+
1386
+ def rollback(): Unit =
1387
+ if (!committed && !rolledBack) {
1388
+ rolledBack = true
1389
+ println(s" [Tx $id] Rolled back")
1390
+ }
1391
+
1392
+ def close(): Unit = {
1393
+ if (!committed && !rolledBack) {
1394
+ println(s" [Tx $id] Auto-rollback (not committed)")
1395
+ rollback()
1396
+ }
1397
+ println(s" [Tx $id] Closed")
1398
+ }
1399
+ }
1400
+
1401
+ /** Result of transaction operations. */
1402
+ case class TxResult(success: Boolean, affectedRows: Int) derives Unscoped
1403
+
1404
+ @main def runTransactionBoundaryExample(): Unit = {
1405
+ println("=== Transaction Boundary Example ===\n")
1406
+ println("Demonstrating Resource-returning beginTransaction method\n")
1407
+
1408
+ Scope.global.scoped { connScope =>
1409
+ import connScope._
1410
+ // Allocate the connection in the outer scope
1411
+ val conn: $[DbConnection] = Resource.fromAutoCloseable(new DbConnection("db-001")).allocate
1412
+ println()
1413
+
1414
+ // Transaction 1: Successful insert
1415
+ println("--- Transaction 1: Insert user ---")
1416
+ val result1: TxResult =
1417
+ connScope.scoped { txScope =>
1418
+ import txScope._
1419
+ val c: $[DbConnection] = lower(conn)
1420
+ val tx: $[DbTransaction] = $(c)(_.beginTransaction("tx-001")).allocate
1421
+ val rows = $(tx)(_.execute("INSERT INTO users VALUES (1, 'Alice')"))
1422
+ $(tx)(_.commit())
1423
+ TxResult(success = true, affectedRows = rows)
1424
+ }
1425
+ println(s" Result: $result1\n")
1426
+
1427
+ // Transaction 2: Transfer funds (multiple operations)
1428
+ println("--- Transaction 2: Transfer funds ---")
1429
+ val result2: TxResult =
1430
+ connScope.scoped { txScope =>
1431
+ import txScope._
1432
+ val c: $[DbConnection] = lower(conn)
1433
+ val tx: $[DbTransaction] = $(c)(_.beginTransaction("tx-002")).allocate
1434
+ val rows1 = $(tx)(_.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1"))
1435
+ val rows2 = $(tx)(_.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2"))
1436
+ $(tx)(_.commit())
1437
+ TxResult(success = true, affectedRows = rows1 + rows2)
1438
+ }
1439
+ println(s" Result: $result2\n")
1440
+
1441
+ // Transaction 3: Demonstrates auto-rollback on scope exit without commit
1442
+ println("--- Transaction 3: Auto-rollback (no explicit commit) ---")
1443
+ val result3: TxResult =
1444
+ connScope.scoped { txScope =>
1445
+ import txScope._
1446
+ val c: $[DbConnection] = lower(conn)
1447
+ val tx: $[DbTransaction] = $(c)(_.beginTransaction("tx-003")).allocate
1448
+ $(tx)(_.execute("DELETE FROM audit_log"))
1449
+ println(" [App] Not committing - scope exit will trigger auto-rollback...")
1450
+ TxResult(success = false, affectedRows = 0)
1451
+ }
1452
+ println(s" Result: $result3\n")
1453
+
1454
+ println("--- All transactions complete, connection still open ---")
1455
+ println("--- Exiting connection scope ---")
1456
+ }
1457
+
1458
+ println("\n=== Example complete ===")
1459
+ println("\nKey insight: beginTransaction() returns Resource[DbTransaction],")
1460
+ println("forcing proper scoped allocation and automatic cleanup.")
1461
+ }
1462
+ }
1463
+ ```
1464
+
1465
+ ([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/TransactionBoundaryExample.scala))
1466
+
1467
+ Run this example with:
1468
+
1469
+ ```bash
1470
+ sbt "scope-examples/runMain scope.examples.runTransactionBoundaryExample"
1471
+ ```
1472
+
1473
+ ### Multi-Layer Service Construction
1474
+
1475
+ This example demonstrates Resource.from macro to automatically build a complex dependency graph with multiple services. Shows automatic wiring of constructor dependencies and cleanup in correct LIFO order:
1476
+
1477
+ ```scala title="scope-examples/src/main/scala/scope/examples/LayeredWebServiceExample.scala"
1478
+ /*
1479
+ * Copyright 2024-2026 John A. De Goes and the ZIO Contributors
1480
+ *
1481
+ * Licensed under the Apache License, Version 2.0 (the "License");
1482
+ * you may not use this file except in compliance with the License.
1483
+ * You may obtain a copy of the License at
1484
+ *
1485
+ * http://www.apache.org/licenses/LICENSE-2.0
1486
+ *
1487
+ * Unless required by applicable law or agreed to in writing, software
1488
+ * distributed under the License is distributed on an "AS IS" BASIS,
1489
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1490
+ * See the License for the specific language governing permissions and
1491
+ * limitations under the License.
1492
+ */
1493
+
1494
+ package scope.examples
1495
+
1496
+ import zio.blocks.scope._
1497
+
1498
+ /**
1499
+ * Demonstrates auto-wiring a layered web service using
1500
+ * `Resource.from[T](wires*)`.
1501
+ *
1502
+ * The macro automatically derives wires for concrete classes (Database,
1503
+ * UserRepository, UserController) while requiring only the leaf config value to
1504
+ * be provided explicitly. Resources are cleaned up in LIFO order when the scope
1505
+ * closes.
1506
+ *
1507
+ * Layer hierarchy:
1508
+ * {{{
1509
+ * AppConfig (leaf value via Wire)
1510
+ * ↓
1511
+ * Database (auto-wired, AutoCloseable)
1512
+ * ↓
1513
+ * UserRepository (auto-wired)
1514
+ * ↓
1515
+ * UserController (auto-wired, AutoCloseable)
1516
+ * }}}
1517
+ */
1518
+
1519
+ /** Application configuration - the leaf dependency provided via Wire(value). */
1520
+ case class WebAppConfig(dbUrl: String, serverPort: Int)
1521
+
1522
+ /** Domain model for users. */
1523
+ case class User(id: Long, name: String, email: String)
1524
+
1525
+ /** Database layer - acquires a connection and releases it on close. */
1526
+ class WebDatabase(config: WebAppConfig) extends AutoCloseable {
1527
+ println(s" [WebDatabase] Connecting to ${config.dbUrl}")
1528
+
1529
+ def execute(sql: String): Int = {
1530
+ println(s" [WebDatabase] Executing: $sql")
1531
+ 1
1532
+ }
1533
+
1534
+ def close(): Unit = println(" [WebDatabase] Connection closed")
1535
+ }
1536
+
1537
+ /** Repository layer - provides data access using the database. */
1538
+ class UserRepository(db: WebDatabase) {
1539
+ println(" [UserRepository] Initialized")
1540
+
1541
+ private var nextId = 1L
1542
+
1543
+ def findById(id: Long): Option[User] = {
1544
+ db.execute(s"SELECT * FROM users WHERE id = $id")
1545
+ if (id > 0) Some(User(id, "Alice", "alice@example.com")) else None
1546
+ }
1547
+
1548
+ def save(user: User): Long = {
1549
+ db.execute(s"INSERT INTO users VALUES (${user.id}, '${user.name}', '${user.email}')")
1550
+ val id = nextId
1551
+ nextId += 1
1552
+ id
1553
+ }
1554
+ }
1555
+
1556
+ /** Controller layer - handles HTTP requests using the repository. */
1557
+ class UserController(repo: UserRepository) extends AutoCloseable {
1558
+ println(" [UserController] Ready to serve requests")
1559
+
1560
+ def getUser(id: Long): String =
1561
+ repo.findById(id).map(u => s"User(${u.id}, ${u.name})").getOrElse("Not found")
1562
+
1563
+ def createUser(name: String, email: String): String = {
1564
+ val id = repo.save(User(0, name, email))
1565
+ s"Created user with id=$id"
1566
+ }
1567
+
1568
+ def close(): Unit = println(" [UserController] Shutting down")
1569
+ }
1570
+
1571
+ /**
1572
+ * Entry point demonstrating the auto-wiring feature.
1573
+ *
1574
+ * Only `Wire(config)` is provided; the macro derives wires for Database,
1575
+ * UserRepository, and UserController from their constructors.
1576
+ */
1577
+ @main def layeredWebServiceExample(): Unit = {
1578
+ val config = WebAppConfig(dbUrl = "jdbc:postgresql://localhost:5432/mydb", serverPort = 8080)
1579
+
1580
+ println("=== Constructing layers (order: config → database → repository → controller) ===")
1581
+
1582
+ // Resource.from auto-wires the entire dependency graph
1583
+ val controllerResource: Resource[UserController] = Resource.from[UserController](
1584
+ Wire(config)
1585
+ )
1586
+
1587
+ // Allocate within a scoped block; cleanup runs on scope exit
1588
+ Scope.global.scoped { scope =>
1589
+ import scope._
1590
+ val controller: $[UserController] = allocate(controllerResource)
1591
+
1592
+ println("\n=== Handling requests ===")
1593
+ println(s" GET /users/1 → ${$(controller)(_.getUser(1))}")
1594
+ println(s" POST /users → ${$(controller)(_.createUser("Bob", "bob@example.com"))}")
1595
+
1596
+ println("\n=== Scope closing (LIFO cleanup: controller → database) ===")
1597
+ }
1598
+
1599
+ println("=== Done ===")
1600
+ }
1601
+ ```
1602
+
1603
+ ([source](https://github.com/zio/zio-blocks/blob/main/scope-examples/src/main/scala/scope/examples/LayeredWebServiceExample.scala))
1604
+
1605
+ ```bash
1606
+ sbt "scope-examples/runMain scope.examples.layeredWebServiceExample"
1607
+ ```