Layer composition
Every service is a Layer; something has to wire them into one running program. This page answers where that wiring lives and which combinator joins the pieces.
Use Layer.provide by default
Section titled “Use Layer.provide by default”Impact: HIGH keeps the assembled layer’s type honest
Layer.providesatisfies a requirement and removes it from the type, so the assembled layer’s type says exactly what is still missing.- Reach for
Layer.provideMergeonly when a layer genuinely should export what it consumed. As a default it widens every downstream signature. - Accepted cost: a service needed at two heights of the graph is provided twice. That is safe
because memoization collapses the same layer object into one instance, with one exception: a
stateful layer shared between an
HttpRouter.serveapp and a sibling goes outsideserve(see layer memoization).
❌ Incorrect — provideMerge everywhere; satisfied requirements leak into the output:
const HttpLayer = ApiHandlersLayer.pipe( Layer.provideMerge(PolicyTesterLayer), Layer.provideMerge(GitHubClient.layer),)// HttpLayer now also "provides" PolicyTester and GitHubClient✅ Correct — provide, and re-provide where a higher layer needs the same service:
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-composition.md · Decision 1, amended
Assemble the graph in one file per deployable, infrastructure last
Section titled “Assemble the graph in one file per deployable, infrastructure last”Impact: HIGH one place answers “what does this app need”
- Each deployable has one file,
main.ts, that builds its whole graph. Intermediate layers are named<Name>Layerconsts, and unrelated peers go in as an array (Layer.provide([Etag.layer, Path.layer])). - Infrastructure (the database, Redis, …) is provided last, at the root, so everything above can use it and there is exactly one instance of it.
- Accepted cost: that file grows, and every new feature touches it.
❌ Incorrect — each feature assembles itself with its own database layer:
export const LabelingLayer = Labeling.layer.pipe(Layer.provide(Db.layerWithOptions(cfg)))// billing/layer.tsexport const BillingLayer = Billing.layer.pipe(Layer.provide(Db.layerWithOptions(cfg))) // a second pool✅ Correct — one root graph, infrastructure at the bottom:
export const ServicesLayer = Layer.mergeAll(Labeling.layer, Billing.layer)
const AppLayer = Layer.mergeAll( HttpRouter.serve(RoutesLayer), Outbox.dispatcherLayer,).pipe( Layer.provide(ServicesLayer), // shared with the dispatcher → outside serve Layer.provide(SqlLayer), // infrastructure last)Source: notes/05-effect-fundamentals/layer-composition.md · Decision 2, Rejected