# Request & RequestResolver

A **Request** describes a single piece of data you want to fetch — its inputs, its success
type, and its failure type. A **RequestResolver** is the one place that actually talks to
the external system: it receives a whole batch of pending requests at once and completes
each of them. Because requests are plain, comparable values, Effect can collect requests
issued across many concurrent fibers, deduplicate the identical ones, and run the resolver a
single time per batch.

The pattern below wraps a resolver inside a service, so callers see an ordinary
`getUserById` method and never deal with batching mechanics directly.

```ts
import { Context, Effect, Exit, Layer, Request, RequestResolver, Schema } from "effect"

// The domain types the resolver returns.
class User extends Schema.Class<User>("User")({
  id: Schema.Number,
  name: Schema.String,
  email: Schema.String
}) {}

class UserNotFound extends Schema.TaggedErrorClass<UserNotFound>()("UserNotFound", {
  id: Schema.Number
}) {}

class Users extends Context.Service<Users, {
  getUserById(id: number): Effect.Effect<User, UserNotFound>
}>()("app/Users") {
  static readonly layer = Layer.effect(
    Users,
    Effect.gen(function*() {
      // A Request models one external lookup. The four type arguments are:
      // inputs, success, error, and required services (here `never`).
      class GetUserById extends Request.Class<
        { readonly id: number },
        User,
        UserNotFound,
        never
      > {}

      // Stand-in for a data source that supports bulk lookup (e.g. WHERE id IN (...)).
      const usersTable = new Map<number, User>([
        [1, new User({ id: 1, name: "Ada Lovelace", email: "ada@acme.dev" })],
        [2, new User({ id: 2, name: "Alan Turing", email: "alan@acme.dev" })],
        [3, new User({ id: 3, name: "Grace Hopper", email: "grace@acme.dev" })]
      ])

      // The resolver receives the *whole batch* of unique requests and must
      // complete every entry. `withCache` returns an Effect, so we `yield*`.
      const resolver = yield* RequestResolver.make<GetUserById>(
        Effect.fnUntraced(function*(entries) {
          // In a real resolver you would issue ONE bulk query for all the ids
          // here, then route each row back to its entry below.
          for (const entry of entries) {
            const user = usersTable.get(entry.request.id)

            // Complete an entry by handing it an Exit: success or typed failure.
            entry.completeUnsafe(
              user
                ? Exit.succeed(user)
                : Exit.fail(new UserNotFound({ id: entry.request.id }))
            )
          }
        })
      ).pipe(
        // Wait this long before running, so more requests can join the batch.
        // Larger delay = bigger batches but more latency on the first request.
        RequestResolver.setDelay("10 millis"),
        // Wrap each batch in a span (with a `batchSize` attribute) and link it
        // to the span of every request that contributed to the batch.
        RequestResolver.withSpan("Users.getUserById.resolver"),
        // Add an in-memory LRU cache keyed by request value, so a repeated id
        // skips the resolver entirely on later calls.
        RequestResolver.withCache({ capacity: 1024 })
      )

      // Callers just see a normal effectful method; batching is invisible.
      const getUserById = (id: number) =>
        Effect.request(new GetUserById({ id }), resolver).pipe(
          Effect.withSpan("Users.getUserById", { attributes: { userId: id } })
        )

      return { getUserById } as const
    })
  )
}

// Run several lookups concurrently. The resolver is invoked ONCE with the
// unique ids [1, 2, 3] — the duplicate 1 and 2 are deduplicated for you.
const program = Effect.gen(function*() {
  const users = yield* Users
  return yield* Effect.forEach([1, 2, 1, 3, 2], users.getUserById, {
    concurrency: "unbounded"
  })
}).pipe(Effect.provide(Users.layer))
```

## Anatomy of the pattern

1. **Define a Request.** `Request.Class<Inputs, Success, Error, Services>` produces a class
   whose instances carry the inputs and declare the success/error types the resolver must
   complete with. Each instance is a comparable value: `new GetUserById({ id: 1 })` equals
   another `new GetUserById({ id: 1 })`, which is what makes deduplication possible.

2. **Build a RequestResolver.** `RequestResolver.make` takes a function from a *non-empty*
   array of [entries](#completing-entries) to an `Effect`. This callback is the only code
   that touches the external system — write your bulk fetch here, then complete every entry.

3. **Issue requests.** `Effect.request(request, resolver)` returns an `Effect<Success, Error,
   Services>`. To the caller it behaves like any other effect; under the hood it enqueues the
   request into the resolver's current batch.

4. **Run concurrently.** Requests issued within the delay window — typically via
   `Effect.forEach(..., { concurrency })` or concurrent fibers — are gathered into one batch,
   deduplicated, and resolved together.

## Completing entries

The resolver callback receives `Request.Entry` values, not raw requests. Each entry exposes:

- `entry.request` — the request value, so you can read its inputs (e.g. `entry.request.id`).
- `entry.completeUnsafe(exit)` — completes the entry with an `Exit`: `Exit.succeed(value)` or
  `Exit.fail(error)` for a typed failure. You **must** complete every entry, or the fibers
  waiting on those requests will hang.
- `entry.context` — the services in scope where the request was made. Use
  `Context.getOption(entry.context, SomeTag)` to read them inside the resolver.
**Caution:** `completeUnsafe` is "unsafe" only in that it bypasses Effect's normal control flow — it is
the intended way to complete an entry from inside a resolver. The contract is simply: complete
*each* entry exactly once. Prefer `Effect.fnUntraced` (not `Effect.fn`) for the resolver
callback so its internal iteration does not appear as separate spans in your traces.

## Tuning the batch

A resolver is a value you refine with combinators in a `pipe`:

| Combinator | Effect |
| --- | --- |
| `RequestResolver.setDelay("10 millis")` | Wait before running so more requests can join the batch. Trades first-request latency for larger batches. |
| `RequestResolver.batchN(n)` | Cap each batch at `n` requests; the rest roll into the next batch. |
| `RequestResolver.grouped(resolver, keyFn)` | Split a batch into sub-batches by key (e.g. per shard or per tenant), running the resolver once per group. |
| `RequestResolver.withSpan(name)` | Wrap each batch in a tracing span and link it to every contributing request's span. |
| `RequestResolver.withCache({ capacity })` | Add an LRU/FIFO cache keyed by request value. Returns an `Effect<RequestResolver>` because it allocates the cache, so `yield*` it. |
| `RequestResolver.around(resolver, before, after)` | Run setup/teardown effects around each batch (open a connection, log, etc.). |

## Resolving from an Effect

When fetching is itself effectful and you do not need a shared bulk call, build the resolver
from a per-request effect with `RequestResolver.fromEffect`. The runtime forks one fiber per
entry and completes each from that fiber's result:

```ts
import { Effect, Request, RequestResolver } from "effect"

class GetUser extends Request.TaggedClass("GetUser")<
  { readonly id: number },
  string
> {}

const resolver = RequestResolver.fromEffect<GetUser>((entry) =>
  Effect.gen(function*() {
    // Each entry resolved independently; still batched at the call site,
    // so you get concurrency control and dedup without writing a bulk fetch.
    yield* Effect.sleep("50 millis")
    return `user-${entry.request.id}`
  })
)
```

`Request.TaggedClass(tag)` is the variant to reach for when several request types share one
resolver — the `_tag` field lets `RequestResolver.fromEffectTagged` dispatch each tag to its
own batched handler.

## Why a service?

Wrapping the resolver in a [service](https://effect.plants.sh/services-and-layers/) keeps the batching machinery —
the request class, the delay, the cache — private to the `Users.layer`. Callers depend only
on the `Users` interface and call `getUserById(id)`; they never see `Request`,
`RequestResolver`, or `Effect.request`. Swapping the implementation (real DB, in-memory fake
for [tests](https://effect.plants.sh/testing/)) is just providing a different layer.

## Related

- [Caching](https://effect.plants.sh/caching/) — `withCache` and `RequestResolver.asCache` let resolver results live
  in a full `Cache` with TTLs, invalidation, and refresh.
- [Concurrency](https://effect.plants.sh/concurrency/) — the batch is only as large as the number of requests in
  flight, so drive resolvers with concurrent `Effect.forEach`.
- [Observability](https://effect.plants.sh/observability/) — `withSpan` makes each batch and its `batchSize` visible
  in your traces.