Effect Patterns
Battle-tested conventions for large Effect-TS codebases. The value is
uniformity at scale: every module follows the same skeleton, so any file is
navigable the moment you've seen one — dependency wiring stays type-safe, and
validation, errors, and IDs flow through a single Schema-based layer.
Effect v4 only. None of this works on v3 — the API surface is different
(Schema.TaggedErrorClass vs v3's Schema.TaggedError, Context.Service vs
Effect.Service, and so on). When an API question isn't answered here, check
in order: the nearest AGENTS.md / project Effect practices doc, the
project-pinned effect package source and version in node_modules, and
current upstream Effect source — before guessing from training data, which
skews v3.
How to use this skill
Read the reference file matching the task. Each is self-contained — load only what the task needs.
| If the task is about… | Read |
|---|---|
The repeated shape of a module: Interface + Service + layer + defaultLayer + self-reexport, no namespaces/barrels, the serviceUse accessor |
references/module-contract.md |
Dependency injection: Context.Service, Layer.effect/sync/succeed/suspend, composing one root Layer, ManagedRuntime, per-instance state, ambient context |
references/services-layers-runtime.md |
Modeling data with Schema: Struct vs Class vs TaggedClass, optional, unions, annotations, decode/encode, deriving types, DeepMutable |
references/schema.md |
Branded, prefix-checked IDs and attaching factory methods with statics |
references/branded-ids.md |
Typed errors: TaggedErrorClass, reason unions, Defect, and catching with catchTag |
references/errors.md |
| Evolving wire/storage formats with parallel versioned schemas instead of migrations | references/versioning.md |
| Designing the graph across many services: flat composition, hubs vs orchestrators, global vs per-scope scoping, resource lifecycle, tiered runtimes | references/service-graph.md |
Concurrency: forking into scopes, Ref/Deferred/Queue/PubSub/Semaphore, bounded concurrency, interruption/cleanup, background daemons |
references/concurrency.md |
Interop with the non-Effect world: tryPromise/async, running effects at the edges, wrapping resources, carrying context across callbacks |
references/interop.md |
Error recovery: Schedule retries, timeouts, the catch* family, failure vs defect, graceful degradation |
references/error-recovery.md |
Testing Effect code: layer-swapping harness, Layer.mock/Layer.succeed fakes, stateful dual-tag test services, TestClock, asserting via Effect.exit, scoped fixtures |
references/testing.md |
Runtime configuration: Config recipes, Config.redacted/option/withDefault, ConfigProvider layers, layerConfig helpers, config in tests |
references/config.md |
Caching and memoization: Cache.make/makeWith, exit-aware TTL, concurrent-lookup dedupe, Effect.cached, when RequestResolver batching is (and isn't) worth it |
references/caching.md |
Stream: source/transform/consume choosers, long-lived consumers in layers, exposing streams from services, backpressure and buffers, keyed concurrency |
references/streams.md |
Outgoing HTTP: Effect HttpClient, adapter boundary shape, status classification + schema decoding, retryTransient/rate limiting, disciplined raw fetch |
references/http-clients.md |
Observability: Effect.fn spans, structured logging + annotateLogs, metrics, one global observability layer |
references/observability.md |
The non-negotiables
These rules are what make the rest cohere. The references explain the why behind each:
- One module skeleton, always the same order —
Interface,Service,use,layer,defaultLayer, self-reexport. See module-contract. - No
export namespaceand no barrelindex.ts— both defeat tree-shaking and Node's native TS. Use flat exports + a self-reexport. schema.tsper domain — branded IDs, data shapes, and error classes live together, separate from the service that uses them.- Errors are
Schema.TaggedErrorClass— so they flow through Effect's typed error channel and narrow undercatchTag. - Version by parallel schema files, not migration code — old formats stay readable through their own schemas.
- Decide each service's scope explicitly — global singleton vs per-scope (tenant/request) state is the load-bearing call in the graph. See service-graph.
- Effect ships the hard part — reach for the primitive — retry is a
Schedule, a per-key cache with TTL/dedupe iseffect/Cache, config isConfigrecipes, a many-valued source is aStream. The hand-rolled equivalents (while+sleep,Map+prune,process.env, ad-hoc loops) are always missing the hard part: jitter, in-flight dedupe, backpressure, interruption.