TODO: Remove the incorrect parts.
See: https://gist.github.com/asfktz/49e2ca5030cb60be83da5386a6c07bbf2 for more recent insights.
This tutorial demonstrates how to prevent recreating layers on every request by using a singleton managed runtime with Effect while still allowing dynamic, per-request dependencies. The main problem this solves is avoiding the wasteful recreation of the same layers repeatedly.
First, let's define our context tags. We'll have two types: constants (singleton services) and dynamics (per-request services).
import { Context, Effect, Layer, ManagedRuntime } from 'effect'
// Constants - created once at startup
export class ConstantA extends Context.Tag('ConstantA')<
ConstantA,
{ value: string; type: 'constant' }
>() {}
export class ConstantB extends Context.Tag('ConstantB')<
ConstantB,
{ value: number; type: 'constant' }
>() {}
// Dynamics - created per request
export class DynamicC extends Context.Tag('DynamicC')<
DynamicC,
{ value: string; timestamp: Date }
>() {}
export class DynamicD extends Context.Tag('DynamicD')<
DynamicD,
{ value: boolean; sessionId: string }
>() {}
A useful pattern is extracting the requirement types from layer definitions. This helps you type your wrapper functions correctly:
// Utility type to extract requirements from a layer
type ExtractReqIn<T> = T extends Layer.Layer<infer RIn, any, any> ? RIn : never
// Define your layers
const ConstantsLayer = Layer.mergeAll(
Layer.succeed(ConstantA, {
value: 'Text',
type: 'constant',
}),
Layer.succeed(ConstantB, {
value: 42,
type: 'constant',
}),
)
// Extract the types - now Constants = ConstantA | ConstantB
type Constants = ExtractReqIn<typeof ConstantsLayer>
This pattern is convenient because if you add new services to your layer, the extracted type automatically updates.
The singleton runtime is created once and contains all your expensive, stateful services:
// Create the singleton runtime - this happens ONCE at application startup
const runtime = ManagedRuntime.make(ConstantsLayer)
Key Point: This runtime is created once and reused across all requests. All services in ConstantsLayer
are singletons.
For per-request services, we create a separate layer that can depend on request-specific context:
const DynamicLayer = Layer.mergeAll(
Layer.succeed(DynamicC, { value: 'bob', timestamp: new Date() }),
Layer.effect(
DynamicD,
Effect.gen(function* () {
// This can access request-specific services like CurrentUser
const user = yield* CurrentUser
return {
value: true,
sessionId: user.name,
}
}),
),
)
// Extract the dynamic types
type Dynamics = ExtractReqIn<typeof DynamicLayer>
Now for the most important part - creating a properly typed runner that combines singleton runtime with dynamic context:
function run<A, E>(effect: Effect.Effect<A, E, Constants | Dynamics>) {
// Create request-specific context
const user = CurrentUser.of({
createdAt: new Date(),
email: '[email protected]',
id: UserId('user-id'),
name: 'John Doe',
})
return runtime.runPromise(
effect.pipe(
// First provide the dynamic layer
Effect.provide(DynamicLayer),
// Then provide request-specific services
Effect.provideService(CurrentUser, user),
),
)
}
The function signature Effect<A, E, Constants | Dynamics>
uses a union type, so effects can require any service from either the constants layer or the dynamics layer.
// The union expands to:
// Effect<A, E, ConstantA | ConstantB | DynamicC | DynamicD>
Here's how you use the runner:
const program = Effect.gen(function* () {
const constantA = yield* ConstantA // From singleton runtime
const constantB = yield* ConstantB // From singleton runtime
const dynamicC = yield* DynamicC // From dynamic layer
const dynamicD = yield* DynamicD // From dynamic layer (depends on CurrentUser)
return {
constantA,
constantB,
dynamicC,
dynamicD
}
})
// This runs the effect with both singleton and dynamic context
const result = run(program)
- Prevents Layer Recreation: The main benefit - avoids recreating layers on every request
- Singleton Runtime: Expensive services (repositories, connections) created once at startup
- Type Safety: Extracted types ensure you can only use available services
- Clean Organization: Clear distinction between singleton and per-request services
This pattern gives you:
- ⭐ Singleton Runtime: Created once, reused everywhere
- ⭐ Dynamic Context: Added per request without runtime overhead
- ⭐ Type Extraction: Automatic type safety from layer definitions
- ⭐ Proper Typing: Wrapper functions that enforce available services
This is the foundation for building scalable Effect applications with clean separation between singleton and per-request concerns.