Skip to content

Layer memoization

Layers are memoized, so a service provided in several places is usually built once. This page answers what the memo key is, where the guarantee stops and the one habit that keeps it from breaking.

Provide a shared layer wherever it is needed

Section titled “Provide a shared layer wherever it is needed”

Impact: MEDIUM repetition is free; contortion is not

  • The memo key is the Layer object itself, compared by identity. A module-level export const layer is one object, so providing it in five places builds it once.
  • Don’t contort the graph to avoid repeating a Layer.provide. Provide the same const at every height that needs it.
  • There is no structural equality: two identical-looking but distinct layer objects build twice, and nothing in the types tells you.

❌ Incorrect — two layer objects that look the same, so two instances:

const PolicyTesterLayer = PolicyTester.layer.pipe(
Layer.provide(Layer.effect(GitHubClient.GitHubClient, GitHubClient.make)),
)
const HttpLayer = ApiHandlersLayer.pipe(
Layer.provide(PolicyTesterLayer),
Layer.provide(Layer.effect(GitHubClient.GitHubClient, GitHubClient.make)), // a second client
)

✅ Correct — the same const in both places, one instance:

const PolicyTesterLayer = PolicyTester.layer.pipe(
Layer.provide(GitHubClient.layer),
)
const HttpLayer = ApiHandlersLayer.pipe(
Layer.provide(PolicyTesterLayer),
Layer.provide(GitHubClient.layer), // same object → one instance
)

Source: notes/05-effect-fundamentals/layer-memoization.md · Decision 1

Provide a layer shared with a sibling outside HttpRouter.serve

Section titled “Provide a layer shared with a sibling outside HttpRouter.serve”

Impact: HIGH otherwise the server gets a private second instance

  • HttpRouter.serve, toHttpEffect and toWebHandler build their app with a forked MemoMap. A layer first built inside the app is private to that server.
  • A stateful layer used both inside the serve app and by a sibling (a dispatcher, a cron loop) is therefore provided outside serve. The parent map builds it first, and the server reuses it.
  • This is the one exception to “repetition is free”. Providing every shared layer once at the root was rejected: every new slice would have to edit main.ts.

❌ Incorrect — provided inside serve, so the dispatcher gets its own Outbox:

const AppLayer = Layer.mergeAll(
HttpRouter.serve(HttpLayer.pipe(Layer.provide(Outbox.layer))),
Outbox.dispatcherLayer.pipe(Layer.provide(Outbox.layer)),
)

✅ Correct — shared with a sibling, so provided outside serve:

const AppLayer = Layer.mergeAll(
HttpRouter.serve(HttpLayer),
Outbox.dispatcherLayer,
).pipe(
Layer.provide([Outbox.layer, Labeling.layer]), // shared with a sibling → outside serve
Layer.provide(SqlLayer),
)

Source: notes/05-effect-fundamentals/layer-memoization.md · Decision 1, amended

Hoist a parameterised layer to a const before use

Section titled “Hoist a parameterised layer to a const before use”

Impact: HIGH prevents silent duplicate pools and queues

  • layerWithOptions(opts) returns a new Layer object on every call. Two calls build two instances (two connection pools, two queues) with no type error and no warning.
  • Call it once per distinct configuration, at the assembly site, and bind it to a name. Never inline it into two Layer.provide calls.
  • If you really want separate instances, give them distinct names and say so in a comment. At the call site the deliberate version looks exactly like the bug.

❌ Incorrect — two calls, two objects, two pools:

Layer.provide(Db.layerWithOptions(cfg)) // in one place
Layer.provide(Db.layerWithOptions(cfg)) // in another

✅ Correct — one object, memoized:

const DbLayer = Db.layerWithOptions(cfg)
Layer.provide(DbLayer)
Layer.provide(DbLayer)

Source: notes/05-effect-fundamentals/layer-memoization.md · Decision 2, Rejected

Impact: HIGH identity cannot diverge if built once

  • Database, Redis, HTTP client and similar are provided at the deployable’s root, not by each consumer. With one build site, there is nothing to memoize wrongly.
  • Where the root graph lives and how it is assembled is covered in layer composition.

❌ Incorrect — each feature builds its own database layer:

export const LabelingLayer = Labeling.layer.pipe(Layer.provide(Db.layerWithOptions(cfg)))
export const BillingLayer = Billing.layer.pipe(Layer.provide(Db.layerWithOptions(cfg)))

✅ Correct — features stay open; the root provides infrastructure once:

const AppLayer = Layer.mergeAll(HttpRouter.serve(HttpLayer), Outbox.dispatcherLayer).pipe(
Layer.provide([Labeling.layer, Billing.layer]),
Layer.provide(SqlLayer),
)

Source: notes/05-effect-fundamentals/layer-memoization.md · Decision 3