Files
effect/migration/layer-memoization.md
T

3.6 KiB

Layer Memoization

In v3, each call to Effect.provide created its own memoization scope. Layers were memoized / deduplicated within a single Effect.provide call, but would not be shared across separate calls — so two Effect.provide calls with overlapping layers would silently build those layers twice.

In v4, a layer already built by an enclosing Effect.provide or runtime is reused by nested Effect.provide calls. Each call forks the ambient MemoMap: it can reuse parent entries, but newly built layers are not written back to the parent. Sibling Effect.provide calls therefore build their own copies, just as in v3. Use { local: true } to build a fresh layer subtree without reusing the enclosing memo map.

Example

import { Console, Context, Effect, Layer } from "effect"

const MyService = Context.Service<{ readonly value: string }>("MyService")

const MyServiceLayer = Layer.effect(
  MyService,
  Effect.gen(function*() {
    yield* Console.log("Building MyService")
    return { value: "hello" }
  })
)

const program = Effect.gen(function*() {
  const a = yield* MyService
  return a.value
})

// Same layer provided twice in nested provide calls
const main = program.pipe(
  Effect.provide(MyServiceLayer),
  Effect.provide(MyServiceLayer)
)

// Effect v3: "Building MyService" is logged TWICE
// Effect v4: "Building MyService" is logged ONCE
Effect.runPromise(main)

The calls above are nested, not siblings. In contrast, the following builds MyServiceLayer twice in v4 when no enclosing provide or runtime has already built it:

const siblings = Effect.gen(function*() {
  yield* program.pipe(Effect.provide(MyServiceLayer))
  yield* program.pipe(Effect.provide(MyServiceLayer))
})

Effect.runPromise(siblings) // "Building MyService" is logged TWICE

Prefer Layer Composition Over Multiple Provides

Even though nested v4 provide calls reuse already-built layers, composing layers before providing is still the recommended pattern. Layer composition makes your dependency graph explicit and lets you see the full structure in one place:

// Preferred — provide once
const main = program.pipe(Effect.provide(MyServiceLayer))

Reusing enclosing layers is not a substitute for proper layer composition and does not deduplicate independent sibling builds.

Opting Out of Shared Memoization

There are cases where you want a layer to be built fresh — for example, test isolation or creating independent resource pools. v4 provides two mechanisms:

Layer.fresh

Wraps a layer so it always builds with a fresh memo map, bypassing the shared cache. This existed in v3 as well.

import { Effect, Layer } from "effect"

const main = program.pipe(
  Effect.provide(MyServiceLayer),
  Effect.provide(Layer.fresh(MyServiceLayer))
)
// "Building MyService" is logged TWICE — fresh bypasses the shared cache

Effect.provide with { local: true }

New in v4. Builds the provided layer with a local memo map instead of the fiber's ambient one. The layer and all its sublayers are built from scratch, without reusing layers built by an enclosing provide or runtime. Nested provide calls can still reuse layers built in this local map.

import { Effect } from "effect"

const main = program.pipe(
  Effect.provide(MyServiceLayer, { local: true }),
  Effect.provide(MyServiceLayer)
)
// "Building MyService" is logged TWICE — local creates its own memo map

Use local: true when you need an entire layer subtree to be isolated — for example, when providing layers in a test harness where each test should get independent resources.