Scala ZIO Agent Rules
Project Context
You are building Scala 3 applications with ZIO 2. ZIO provides typed errors, structured concurrency, and dependency injection via ZLayer. Every effectful operation returns a `ZIO[R, E, A]` — never throw exceptions or perform side effects outside of ZIO's runtime.
Code Style
- Use Scala 3 syntax: significant indentation, `enum` for ADTs, `given`/`using` for contextual abstractions.
- Use `opaque type UserId = String` for lightweight domain wrappers — they have zero runtime cost.
- Use `for`-comprehensions for sequential effect composition; use `flatMap` chains only when more readable.
- Avoid `null`, `throw`, `Option.get`, and `Either.right.get` — use ZIO error channel and `ZIO.fail` instead.
- Name effects to communicate intent: `val fetchUser: ZIO[UserRepo, UserError, User]` — the types tell the story.
ZIO Effects
- Use `ZIO.succeed(value)` to lift pure values; `ZIO.attempt(sideEffect)` to safely wrap exception-throwing code.
- Use `ZIO.fail(error)` for typed domain errors; use `ZIO.die(cause)` only for unrecoverable programmer bugs.
- Use `ZIO.foreachPar(list)(item => processItem(item))` for parallel collection processing with backpressure.
- Use `ZIO.collectAllPar` for running a fixed set of independent effects concurrently.
- Wrap blocking I/O with `ZIO.attemptBlocking` to offload it to Blocking thread pool, never calling it on the main fiber pool.
- Use `ZIO.acquireRelease(acquire)(release)` for resource management — this guarantees cleanup even on interruption.
ZLayer & Dependency Injection
- Define services as traits with methods returning `ZIO` effects: `trait OrderRepo { def findById(id: OrderId): IO[RepoError, Option[Order]] }`.
- Provide implementations as companion object `ZLayer`s: `object OrderRepo { val live: ZLayer[Database, Nothing, OrderRepo] = ZLayer.fromFunction(LiveOrderRepo(_)) }`.
- Compose layers horizontally with `++` and vertically with `>>>`: `val appLayer = DatabaseLayer.live >>> OrderRepo.live ++ UserRepo.live`.
- Call `ZIO.provide(appLayer)` at the application edge in `ZIOAppDefault.run`. Never pass layers deep into business logic.
- Use `ZLayer.scoped` for services that need `Scope` for lifecycle management (connection pools, HTTP clients).
Concurrency
- Use `ZIO.forkScoped` to launch background fibers — they are automatically cancelled when the scope closes.
- Use `Ref[A]` for shared mutable state: `Ref.make(initial).flatMap(ref => ref.update(f))`.
- Use `Queue.bounded[A](capacity)` for producer-consumer patterns; bounded queues apply back-pressure automatically.
- Use `Semaphore(permits)` to limit concurrency for rate-limited external resources.
- Use `Hub[A]` for fan-out broadcasting to multiple fiber subscribers.
- Use `Schedule.exponential(100.millis) && Schedule.recurs(5)` for retry policies with jitter.
Error Handling
- Define domain errors as sealed enums: `enum AppError { case NotFound(id: String); case Unauthorized; case DbError(cause: Throwable) }`.
- Use the typed error channel: `ZIO[R, AppError, A]` — never `ZIO[R, Throwable, A]` for recoverable errors.
- Use `mapError`, `catchSome`, `catchAll`, and `orElseFail` to transform and recover from errors.
- Use `.orDie` only for errors that represent programmer bugs (configuration errors at startup) — it converts errors to defects.
- Use `ZIO.logError("message", cause)` with annotations for structured error logging: `ZIO.logAnnotate("orderId", id.toString)`.
HTTP with zio-http
- Define routes as `Routes` values and compose them with the `++` operator.
- Use `Handler.fromFunctionZIO` for handlers that perform effects; use `Handler.ok` for trivial responses.
- Apply middleware for authentication, CORS, logging, and metrics; define them as `Middleware` values.
- Return `Response.json(body.toJsonAST.getOrElse("{}"))` for JSON responses; set appropriate `Content-Type` headers.
- Stream large responses with `Body.fromStream(zioStream)` — never accumulate large payloads in memory.
Testing with zio-test
- Extend `ZIOSpecDefault` for all test suites; tests are `ZIO` effects and can access any ZIO functionality.
- Use `assertTrue(condition)` and `assertZIO(effect)(succeeds(equalTo(expected)))` for assertions.
- Provide test layers in `spec` with `.provide(TestOrderRepo.layer, ...)` to supply in-memory fakes.
- Use `TestClock.adjust(1.hour)` to control time in tests without `Thread.sleep`.
- Use `TestRandom` and `TestConsole` for deterministic testing of random and console-dependent code.
- Use `Gen` and `check` for property-based tests: `check(Gen.int(1, 1000)) { n => assertTrue(factorial(n) > 0) }`.
Performance
- Use `ZStream` for streaming large data sets; `ZStream.fromIterable` → `ZStream.mapZIOPar(n)` for parallel processing.
- Configure fiber pool sizes in `Runtime.addLogger` and `ZLayer` bootstrap — defaults work for most use cases.
- Use `Chunk[A]` for high-performance immutable sequences instead of `List` in hot paths.