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 layeris 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,toHttpEffectandtoWebHandlerbuild 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
serveapp and by a sibling (a dispatcher, a cron loop) is therefore provided outsideserve. 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.providecalls. - 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 placeLayer.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
Build infrastructure once, at the root
Section titled “Build infrastructure once, at the root”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