Skip to content

Instantly share code, notes, and snippets.

@asfktz
Last active July 20, 2025 22:16
Show Gist options
  • Save asfktz/ba41b98b6f17d74ad26d0590442b8727 to your computer and use it in GitHub Desktop.
Save asfktz/ba41b98b6f17d74ad26d0590442b8727 to your computer and use it in GitHub Desktop.
Effect Tutorial: Preventing Layer Recreation Per Request - Singleton Runtime Pattern

TODO: Remove the incorrect parts.
See: https://gist.github.com/asfktz/49e2ca5030cb60be83da5386a6c07bbf2 for more recent insights.

Preventing Layer Recreation Per Request in Effect

Introduction

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.

1. Setting Up Context Tags

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 }
>() {}

2. Extracting Types from Layers

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.

3. Creating a Managed Runtime ⭐

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.

4. Handling Dynamic Requirements ⭐

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>

5. Creating a Typed Runner Function ⭐

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),
    ),
  )
}

Why This Typing Works

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>

6. Practical Example

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)

Key Benefits

  1. Prevents Layer Recreation: The main benefit - avoids recreating layers on every request
  2. Singleton Runtime: Expensive services (repositories, connections) created once at startup
  3. Type Safety: Extracted types ensure you can only use available services
  4. Clean Organization: Clear distinction between singleton and per-request services

Summary

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment