Claude-skill-registry effect-service-authoring

Creating Effect.Service<>() classes, Context.Tag patterns, Layer composition. Decision trees for when to use which pattern. Covers service definition, dependency injection, and layer management.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/effect-service-authoring" ~/.claude/skills/majiayu000-claude-skill-registry-effect-service-authoring && rm -rf "$T"
manifest: skills/data/effect-service-authoring/SKILL.md
source content

Effect Service Authoring

Overview

This skill covers the three canonical patterns for defining Effect-TS services in TMNL:

  1. Effect.Service<>() — Auto-layered services with effectful construction (RECOMMENDED DEFAULT)
  2. Context.Tag — Strategy Pattern for swappable implementations
  3. Context.Tag + Config — Parameterized services with configuration injection

Master these patterns to build composable, testable, type-safe service architectures.

Canonical Sources

Effect-TS Core

  • Submodule:
    ../../submodules/effect/packages/effect/src/
    • Context.ts:507-585
      — Context.Tag and Context.Reference
    • Layer.ts:289-292
      — Layer.effect (effectful construction)
    • Layer.ts:772-775
      — Layer.succeed (synchronous construction)
    • Layer.ts:727-735
      — Layer.scoped (resource management)
    • Layer.ts:583-589
      — Layer.mergeAll (composition)

Effect Website Documentation

  • Submodule:
    ../../submodules/website/content/src/content/docs/docs/
    • requirements-management/services.mdx
      — Service patterns
    • requirements-management/layers.mdx
      — Layer composition
    • requirements-management/default-services.mdx
      — Default layers

TMNL Battle-tested Implementations

  • SliderBehavior
    src/lib/slider/v1/services/SliderBehavior.ts:15
    (Strategy Pattern, 5 variants)
  • DataManager
    src/lib/data-manager/v1/DataManager.ts:73
    (Effect.Service with dependencies)
  • SearchKernel
    src/lib/data-manager/v1/kernels/SearchKernel.ts:308
    (Effect.Service)
  • IdGenerator
    src/lib/layers/v1/services/IdGenerator.ts:34
    (Config + Service)

Patterns

Decision Tree: Which Service Pattern?

Need a service?
│
├─ Multiple swappable implementations (Strategy Pattern)?
│  └─ Use: class extends Context.Tag
│     Example: SliderBehavior (linear, log, decibel, exponential, stepped)
│
├─ Effectful construction or service dependencies?
│  └─ Use: class extends Effect.Service<>()
│     Example: DataManager (depends on SearchKernel)
│
└─ Simple configuration tag?
   └─ Use: class extends Context.Tag
      with Static Default + Custom factories
      Example: IdGeneratorConfig

Pattern 1: Effect.Service<>() — RECOMMENDED DEFAULT

When to use:

  • Service has dependencies on other services
  • Construction is effectful (async, fallible, requires resources)
  • You want auto-generated
    .Default
    layer
  • Default choice for most new services

Signature:

class ServiceName extends Effect.Service<ServiceName>()(
  "app/namespace/ServiceName",
  {
    effect: Effect.Effect<Interface>,
    dependencies?: Layer.Layer<Deps>[]
  }
) {}

Full Example:

import { Effect, Context, Layer } from 'effect'

// Define interface
interface DatabaseService {
  readonly query: (sql: string) => Effect.Effect<unknown[]>
  readonly execute: (sql: string) => Effect.Effect<void>
}

// Define service with effectful construction
class Database extends Effect.Service<Database>()(
  "app/Database",
  {
    effect: Effect.gen(function* () {
      // Yield dependencies
      const config = yield* DatabaseConfig

      // Effectful construction
      const pool = yield* Effect.tryPromise(() =>
        createPool(config.connectionString)
      )

      // Define interface methods
      const query = (sql: string) =>
        Effect.tryPromise(() => pool.query(sql))

      const execute = (sql: string) =>
        Effect.tryPromise(() => pool.execute(sql))

      return { query, execute } as const
    }),
    dependencies: [DatabaseConfig.Default]
  }
) {}

// Auto-generated layer available at:
Database.Default

// Usage in other services:
const userService = Effect.gen(function* () {
  const db = yield* Database
  const users = yield* db.query('SELECT * FROM users')
  return users
})

Key Features:

  • Double
    ()()
    syntax
    : First
    ()
    parameterizes generic type, second
    ()
    provides config
  • dependencies: [...]
    : Auto-provides required layers (optional but recommended)
  • as const
    : Ensures readonly interface, prevents accidental mutations
  • Auto-generated
    .Default
    : Layer is automatically created

TMNL Example (

src/lib/data-manager/v1/DataManager.ts:73
):

class DataManager extends Effect.Service<DataManager>()(
  "tmnl/data-manager/DataManager",
  {
    effect: Effect.gen(function* () {
      const kernel = yield* SearchKernel

      const search = (query: string, limit: number) =>
        Effect.gen(function* () {
          const results = yield* kernel.search(query, limit)
          return results
        })

      return { search } as const
    }),
    dependencies: [SearchKernel.Default]
  }
) {}

Pattern 2: Context.Tag — STRATEGY PATTERN

When to use:

  • Multiple implementations of same interface (Strategy Pattern)
  • Runtime behavior swapping
  • Need to export both
    .Default
    (Layer) AND
    .shape
    (direct access)

Signature:

class TagName extends Context.Tag("namespace/TagName")<
  TagName,
  InterfaceShape
>() {}

Full Example:

import { Context, Layer } from 'effect'

// 1. Define interface shape
interface TransformBehavior {
  readonly id: string
  readonly transform: (value: number) => number
  readonly inverse: (value: number) => number
}

// 2. Define tag
class ValueTransform extends Context.Tag("app/ValueTransform")<
  ValueTransform,
  TransformBehavior
>() {}

// 3. Create implementations
const linearImpl: TransformBehavior = {
  id: 'linear',
  transform: (v) => v,
  inverse: (v) => v,
}

const logarithmicImpl: TransformBehavior = {
  id: 'logarithmic',
  transform: (v) => Math.log(v),
  inverse: (v) => Math.exp(v),
}

// 4. Export BOTH .Default AND .shape
export const LinearTransform = {
  Default: Layer.succeed(ValueTransform, linearImpl),
  shape: linearImpl,  // Direct access without Layer
}

export const LogarithmicTransform = {
  Default: Layer.succeed(ValueTransform, logarithmicImpl),
  shape: logarithmicImpl,
}

// 5. Runtime swapping
const withLinear = program.pipe(Effect.provide(LinearTransform.Default))
const withLog = program.pipe(Effect.provide(LogarithmicTransform.Default))

Key Features:

  • Export
    .shape
    : Allows direct access without Effect runtime
  • Export
    .Default
    : Layer for Effect programs
  • Layer.succeed: Creates layer from synchronous value
  • Swappable: Change behavior via
    Effect.provide

TMNL Example (

src/lib/slider/v1/services/SliderBehavior.ts
):

// Interface
interface SliderBehaviorShape {
  readonly id: string
  readonly normalize: (value: number, min: number, max: number) => number
  readonly denormalize: (normalized: number, min: number, max: number) => number
  readonly format: (value: number, precision: number, unit: string) => string
}

// Tag
class SliderBehavior extends Context.Tag('tmnl/slider/SliderBehavior')<
  SliderBehavior,
  SliderBehaviorShape
>() {}

// Linear implementation
const linearBehavior: SliderBehaviorShape = {
  id: 'linear',
  normalize(value, min, max) {
    return (value - min) / (max - min)
  },
  denormalize(normalized, min, max) {
    return min + normalized * (max - min)
  },
  format(value, precision, unit) {
    return `${value.toFixed(precision)}${unit}`
  },
}

export const LinearBehavior = {
  Default: Layer.succeed(SliderBehavior, linearBehavior),
  shape: linearBehavior,
}

// 4 more behaviors: Logarithmic, Decibel, Exponential, Stepped
// Each follows same pattern: implementation + { Default, shape }

Pattern 3: Context.Tag + Config — PARAMETERIZED SERVICE

When to use:

  • Service needs configuration injection
  • Configuration should be overridable at composition time
  • Want to separate config from service logic

Critical Rule: Define Config tag BEFORE Service (avoids circular dependencies)

Full Example:

import { Context, Effect, Layer } from 'effect'

// 1. Config tag FIRST
class ApiConfig extends Context.Tag("app/ApiConfig")<
  ApiConfig,
  { readonly baseUrl: string; readonly timeout: number }
>() {
  static Default = Layer.succeed(this, {
    baseUrl: 'https://api.example.com',
    timeout: 5000
  })

  static Custom = (config: { baseUrl: string; timeout: number }) =>
    Layer.succeed(this, config)
}

// 2. Service depends on config
class ApiClient extends Effect.Service<ApiClient>()(
  "app/ApiClient",
  {
    effect: Effect.gen(function* () {
      const config = yield* ApiConfig  // Dependency on config

      const fetch = (path: string) =>
        Effect.tryPromise(() =>
          window.fetch(`${config.baseUrl}${path}`, {
            signal: AbortSignal.timeout(config.timeout)
          })
        )

      return { fetch } as const
    }),
    dependencies: [ApiConfig.Default]
  }
) {}

// 3. Override config at composition
const productionLayer = ApiClient.Default.pipe(
  Layer.provide(ApiConfig.Custom({
    baseUrl: 'https://prod.example.com',
    timeout: 10000
  }))
)

const testLayer = ApiClient.Default.pipe(
  Layer.provide(ApiConfig.Custom({
    baseUrl: 'http://localhost:3000',
    timeout: 1000
  }))
)

Key Features:

  • Static
    .Default
    : Default configuration
  • Static
    .Custom
    : Factory for custom configuration
  • Order matters: Config tag MUST be defined before service
  • Layer.provide: Override dependencies at composition

TMNL Example (

src/lib/layers/v1/services/IdGenerator.ts
):

// Config FIRST
class IdGeneratorConfig extends Context.Tag('app/layers/IdGeneratorConfig')<
  IdGeneratorConfig,
  { strategy: 'nanoid' | 'uuid' | 'custom'; customFn?: () => string }
>() {
  static Default = Layer.succeed(this, { strategy: 'nanoid' })
  static Custom = (config: { strategy: 'nanoid' | 'uuid' | 'custom'; customFn?: () => string }) =>
    Layer.succeed(this, config)
}

// Service SECOND
class IdGenerator extends Effect.Service<IdGenerator>()(
  "app/layers/IdGenerator",
  {
    effect: Effect.gen(function* () {
      const config = yield* IdGeneratorConfig

      const generate = () => {
        switch (config.strategy) {
          case 'nanoid': return nanoid()
          case 'uuid': return uuidv4()
          case 'custom': return config.customFn?.() ?? nanoid()
        }
      }

      return { generate } as const
    }),
    dependencies: [IdGeneratorConfig.Default]
  }
) {}

Pattern 4: Layer Composition

Layer.mergeAll — Combine multiple service layers:

import { Layer } from 'effect'

// Individual service layers
const configLayer = ConfigService.Default
const loggerLayer = LoggerService.Default
const dbLayer = DatabaseService.Default

// Merge into single runtime layer
const AppLayer = Layer.mergeAll(
  configLayer,
  loggerLayer,
  dbLayer
)

// Use with effect-atom
const appRuntime = Atom.runtime(AppLayer)

Layer.provide — Inject dependencies:

// Service depends on Database
const UserServiceLive = Layer.effect(
  UserService,
  Effect.gen(function* () {
    const db = yield* Database
    return { findUser: (id) => db.query(`SELECT * FROM users WHERE id = ${id}`) }
  })
).pipe(
  Layer.provide(Database.Default)  // Inject dependency
)

Layer chains — Complex dependency graphs:

// A → B → C (A depends on B, B depends on C)
const CLayer = ConfigService.Default
const BLayer = DatabaseService.Default.pipe(Layer.provide(CLayer))
const ALayer = UserService.Default.pipe(Layer.provide(BLayer))

// All dependencies satisfied
const program = Effect.gen(function* () {
  const users = yield* UserService
  return users
}).pipe(Effect.provide(ALayer))

Pattern 5: Resource Management with Layer.scoped

When to use:

  • Service acquires resources (connections, file handles, subscriptions)
  • Resources need cleanup on shutdown
  • Want automatic resource disposal

Example:

import { Effect, Layer } from 'effect'

class ConnectionPool extends Context.Tag("app/ConnectionPool")<
  ConnectionPool,
  { query: (sql: string) => Effect.Effect<unknown[]> }
>() {}

const ConnectionPoolLive = Layer.scoped(
  ConnectionPool,
  Effect.acquireRelease(
    // Acquire
    Effect.gen(function* () {
      const pool = yield* Effect.tryPromise(() => createPool())
      yield* Effect.log("Pool created")
      return { query: (sql) => Effect.tryPromise(() => pool.query(sql)) }
    }),
    // Release (called on layer disposal)
    (pool) => Effect.gen(function* () {
      yield* Effect.log("Closing pool")
      yield* Effect.tryPromise(() => pool.close())
    })
  )
)

Examples

Example 1: Simple Service with No Dependencies

import { Effect } from 'effect'

class RandomService extends Effect.Service<RandomService>()(
  "app/RandomService",
  {
    effect: Effect.gen(function* () {
      const randomInt = (max: number) =>
        Effect.sync(() => Math.floor(Math.random() * max))

      const randomString = (length: number) =>
        Effect.sync(() =>
          Array.from({ length }, () =>
            String.fromCharCode(97 + Math.floor(Math.random() * 26))
          ).join('')
        )

      return { randomInt, randomString } as const
    })
  }
) {}

// Usage
const program = Effect.gen(function* () {
  const random = yield* RandomService
  const n = yield* random.randomInt(100)
  const s = yield* random.randomString(10)
  console.log({ n, s })
})

Example 2: Service with Multiple Dependencies

import { Effect, Layer } from 'effect'

// Service A
class Logger extends Effect.Service<Logger>()(
  "app/Logger",
  {
    effect: Effect.succeed({
      log: (msg: string) => Effect.sync(() => console.log(msg))
    })
  }
) {}

// Service B
class Config extends Effect.Service<Config>()(
  "app/Config",
  {
    effect: Effect.succeed({
      apiKey: "secret-key",
      baseUrl: "https://api.example.com"
    })
  }
) {}

// Service C depends on A and B
class ApiClient extends Effect.Service<ApiClient>()(
  "app/ApiClient",
  {
    effect: Effect.gen(function* () {
      const logger = yield* Logger
      const config = yield* Config

      const fetch = (path: string) =>
        Effect.gen(function* () {
          yield* logger.log(`Fetching: ${config.baseUrl}${path}`)
          const response = yield* Effect.tryPromise(() =>
            window.fetch(`${config.baseUrl}${path}`)
          )
          return response
        })

      return { fetch } as const
    }),
    dependencies: [Logger.Default, Config.Default]
  }
) {}

// All dependencies auto-satisfied
const program = Effect.gen(function* () {
  const api = yield* ApiClient
  yield* api.fetch('/users')
})

Example 3: Swappable Behaviors (Strategy Pattern)

import { Context, Effect, Layer } from 'effect'

interface CacheStrategy {
  readonly name: string
  readonly get: <A>(key: string) => Effect.Effect<A | null>
  readonly set: <A>(key: string, value: A) => Effect.Effect<void>
}

class Cache extends Context.Tag("app/Cache")<Cache, CacheStrategy>() {}

// In-memory implementation
const memoryCache: CacheStrategy = {
  name: 'memory',
  get: (key) => Effect.sync(() => memoryStore.get(key) ?? null),
  set: (key, value) => Effect.sync(() => memoryStore.set(key, value)),
}

// Redis implementation
const redisCache: CacheStrategy = {
  name: 'redis',
  get: (key) => Effect.tryPromise(() => redis.get(key)),
  set: (key, value) => Effect.tryPromise(() => redis.set(key, value)),
}

export const MemoryCache = {
  Default: Layer.succeed(Cache, memoryCache),
  shape: memoryCache,
}

export const RedisCache = {
  Default: Layer.succeed(Cache, redisCache),
  shape: redisCache,
}

// Swap implementations
const dev = program.pipe(Effect.provide(MemoryCache.Default))
const prod = program.pipe(Effect.provide(RedisCache.Default))

Anti-Patterns

1. Missing Double
()()
Syntax

// WRONG
class MyService extends Effect.Service<MyService>("id", { effect: ... }) {}

// CORRECT
class MyService extends Effect.Service<MyService>()("id", { effect: ... }) {}

2. Config AFTER Service (Circular Dependency)

// WRONG — Circular dependency!
class MyService extends Effect.Service<MyService>()("id", {
  effect: Effect.gen(function* () {
    const config = yield* MyConfig  // MyConfig not defined yet!
  }),
}) {}
class MyConfig extends Context.Tag("config")<...>() {}

// CORRECT — Config first
class MyConfig extends Context.Tag("config")<...>() {}
class MyService extends Effect.Service<MyService>()("id", { ... }) {}

3. Forgetting
as const

// WRONG — Mutable interface
return { doThing }

// CORRECT — Readonly interface
return { doThing } as const

4. Not Exporting
.shape
for Strategy Pattern

// WRONG — Missing direct access
export const LinearBehavior = {
  Default: Layer.succeed(Behavior, linearImpl),
}

// CORRECT — Export both Layer and shape
export const LinearBehavior = {
  Default: Layer.succeed(Behavior, linearImpl),
  shape: linearImpl,  // Direct access without Layer
}

5. Using Effect.Ref in React-consumed Services

// WRONG — Ref + Atom bridging complexity
const service = Effect.gen(function* () {
  const stateRef = yield* Ref.make(initial)
  // Now need: polling, SubscriptionRef, streams-to-consume-streams
})

// CORRECT — Atom-as-State pattern
const stateAtom = Atom.make(initial)
const service = {
  update: (value) => Effect.sync(() => Atom.set(stateAtom, value))
}

Pattern Comparison

Feature
Effect.Service<>()
Context.Tag
(Strategy)
Context.Tag + Config
Auto-generates
.Default
✅ Yes❌ Manual
Layer.succeed
⚠️ Config: manual, Service: auto
Effectful construction
Effect.gen
⚠️ Needs
Layer.effect
✅ Service uses
Effect.gen
Dependencies array
dependencies: [...]
❌ Manual
Layer.provide
✅ Service has dependencies
Multiple implementations⚠️ Possible but awkward✅ Idiomatic❌ Not applicable
Configuration injection⚠️ Needs separate Config tag❌ Not applicable✅ Built for this
Recommended forDefault choiceStrategy Pattern onlyConfigurable services

Quick Reference

I need to...Use this pattern
Create a new service with dependencies
Effect.Service<>()
with
dependencies: [...]
Swap implementations at runtime
Context.Tag
with multiple
{ Default, shape }
exports
Inject configuration into a service
Context.Tag
for config +
Effect.Service<>()
for service
Manage resources (connections, files)
Layer.scoped
with
Effect.acquireRelease
Compose multiple services
Layer.mergeAll([...layers])
Override a dependency
Layer.provide(dependencyLayer)
Access service in Effect program
yield* ServiceName

Related Skills

  • effect-schema-mastery — Schema validation for service inputs/outputs
  • effect-atom-integration — Integrating services with React via effect-atom
  • effect-testing-patterns — Testing services in isolation