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.