Creating Services in Effect v4

Learn to create services using Context.Service, IdentityComposer keys, and Layer composition in Effect v4. Covers dependency injection and service wiring.

Sby Skills Guide Bot
DevelopmentIntermediate
107/25/2026
Claude CodeCursorWindsurfCopilotCodex
#effect#typescript#dependency-injection#layer#service

Recommended for


name: effect-services description: > Creating services with Context.Service, IdentityComposer keys, and Layer composition. Trigger on: new service, service definition, Layer wiring, dependency injection, Context. version: 0.1.0 status: active

Creating a Service (Effect v4)

Step 1: Create the Identity

Every service needs a unique key from @beep/identity/packages. Never use plain strings.

// WHY: IdentityComposer produces branded strings with compile-time path validation.
// The tagged template literal validates segment characters at runtime.
import { $PackageNameId } from "@beep/identity/packages"

const $I = $PackageNameId.create("relative/path/to/file/from/package/src")
//                        ↑ package composer      ↑ file-local composer

Step 2: Define the Service Class

Use Context.Service<Self, Shape>()(identityKey). Note: type params FIRST, then key in second call.

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

// WHY: Class syntax gives you a nominal type (Self) + the service shape in one declaration.
// The $I template tag produces a branded IdentityString under the file-local path.
class Notifications extends Context.Service<Notifications, {
  readonly notify: (msg: string) => Effect.Effect<void>
}>()($I`Notifications`) {}

Step 3: Add the Constructor

Use an explicit constructor effect and Layer.effect(...). Parameterized or reusable constructor functions should be named Effect.fn("Service.make"). Zero-arg constructor values may stay Effect.gen(...).pipe(Effect.withSpan("Service.make")) to avoid immediate Effect.fn() IIFEs.

const makeNotifications = Effect.gen(function*() {
  const config = yield* AppConfig
  return {
    notify: Effect.fn("Notifications.notify")(function*(msg: string) {
      yield* Effect.annotateCurrentSpan({ message_length: msg.length })
      yield* Effect.logInfo({ message: "notification emitted" }).pipe(
        Effect.annotateLogs({ service: "notifications" })
      )
      return yield* Effect.log(`[${config.prefix}] ${msg}`)
    })
  }
}).pipe(Effect.withSpan("Notifications.make"))

class Notifications extends Context.Service<Notifications, {
  readonly notify: (msg: string) => Effect.Effect<void>
}>()($I`Notifications`) {
  // WHY: Explicit layer construction. Wire deps with Layer.provide, not `dependencies`.
  static layer = Layer.effect(this, makeNotifications).pipe(
    Layer.provide(AppConfig.layer)
  )
}

Step 4: Consume the Service

Prefer yield* over .use() — it makes dependencies visible at the call site.

Exception: in callback-only APIs (for example SchemaTransformation.transform* decode/encode callbacks) where yield* is not available, use Context.Service.use(...) directly.

// WHY: yield* in Effect.fn makes the Notifications dependency appear in the R channel.
const sendWelcome = Effect.fn("sendWelcome")(function*(userId: string) {
  const notifications = yield* Notifications
  yield* notifications.notify(`Welcome ${userId}`)
})

const JsoncTextToUnknown = S.String.pipe(
  S.decodeTo(
    S.Unknown,
    SchemaTransformation.transformOrFail({
      decode: (content) => JsoncCodecService.use((service) => service.parseUnknown(content)),
      encode: (value) => Effect.fail(`unsupported encode: ${value}`)
    })
  )
)

Step 5: Compose Layers

Wire the full dependency graph at the application entry point.

// WHY: MemoMap deduplicates layers by reference. Same const = built once.
// No intermediate bundles needed — just provide each layer directly.
const AppLayer = Layer.mergeAll(
  Notifications.layer,
  UserService.layer
)

const program = myEffect.pipe(Effect.provide(AppLayer))

Verify

  1. yield* ServiceName compiles — the R channel includes the service.
  2. Layer.effect(Service, constructorEffect) compiles — shape matches.
  3. No Context.Tag, Effect.Tag, Effect.Service, or .Default anywhere in your code.
  4. All service keys use $I\Name`` template tags, not string literals.
  5. Reusable service methods use named Effect.fn("Service.method").
  6. Constructor flows are observable with Effect.withSpan(...) plus structured log annotations where the path matters.
Related skills