Skip to main content
In v3, services were defined using Context.Tag, Context.GenericTag, Effect.Tag, or Effect.Service. In v4, all of these have been replaced by ServiceMap.Service. The underlying data structure has also changed: Context has been replaced by ServiceMap — a typed map from service identifiers to their implementations.

Defining Services

Function Syntax

import { Context } from "effect"

interface Database {
  readonly query: (sql: string) => string
}

const Database = Context.GenericTag<Database>("Database")

Class-Based Services

import { Context } from "effect"

class Database extends Context.Tag("Database")<Database, {
  readonly query: (sql: string) => string
}>() {}
Note the difference in argument order: in v3, the identifier string is passed to Context.Tag(id) before the type parameters. In v4, the type parameters come first via ServiceMap.Service<Self, Shape>() and the identifier string is passed to the returned constructor (id).

Migrating from Effect.Tag Accessors

v3’s Effect.Tag provided proxy access to service methods as static properties on the tag class (accessors). This allowed calling service methods directly without first yielding the service:
// v3 — static accessor proxy
const program = Notifications.notify("hello")
This pattern had significant limitations. The proxy was implemented via mapped types over the service shape, which meant generic methods lost their type parameters. A service method like get<T>(key: string): Effect<T> would have its generic erased when accessed through the proxy, collapsing to get(key: string): Effect<unknown>.

Using Service.use

In v4, accessors are removed. The most direct replacement is Service.use, which receives the service instance and runs a callback:
import { Effect } from "effect"

class Notifications extends Effect.Tag("Notifications")<Notifications, {
  readonly notify: (message: string) => Effect.Effect<void>
}>() {}

// Static proxy access
const program = Notifications.notify("hello")

Service.use vs Service.useSync

use takes an effectful callback (service: Shape) => Effect<A, E, R> and returns an Effect<A, E, R | Identifier>. useSync takes a pure callback (service: Shape) => A and returns an Effect<A, never, Identifier>:
//      ┌─── Effect<void, never, Notifications>
//      ▼
const program = Notifications.use((n) => n.notify("hello"))

//      ┌─── Effect<number, never, Config>
//      ▼
const port = Config.useSync((c) => c.port)
Prefer yield* over use in most cases. While use is a convenient one-liner, it makes it easy to accidentally leak service dependencies into return values. When you call use, the service is available inside the callback but the dependency is not visible at the call site — making it harder to track which services your code depends on.

Best Practice: Use yield*

Using yield* in a generator makes dependencies explicit and keeps service access co-located with the rest of your effect logic:
const program = Effect.gen(function*() {
  const notifications = yield* Notifications
  yield* notifications.notify("hello")
  yield* notifications.notify("world")
})

Migrating Effect.Service

v3’s Effect.Service allowed defining a service with an effectful constructor and dependencies inline. In v4, use ServiceMap.Service with a make option.
import { Effect, Layer } from "effect"

class Logger extends Effect.Service<Logger>()("Logger", {
  effect: Effect.gen(function*() {
    const config = yield* Config
    return { log: (msg: string) => Effect.log(`[${config.prefix}] ${msg}`) }
  }),
  dependencies: [Config.Default]
}) {}

// Logger.Default is auto-generated: Layer<Logger, never, never>
const program = Effect.gen(function*() {
  const logger = yield* Logger
  yield* logger.log("hello")
}).pipe(Effect.provide(Logger.Default))
The dependencies option no longer exists. Wire dependencies via Layer.provide as shown above.v4 adopts the convention of naming layers with layer (e.g. Logger.layer) instead of v3’s Default or Live. Use layer for the primary layer and descriptive suffixes for variants (e.g. layerTest, layerConfig).

References (Services with Defaults)

import { Context } from "effect"

class LogLevel extends Context.Reference<LogLevel>()("LogLevel", {
  defaultValue: () => "info" as const
}) {}

Quick Reference

v3v4
Context.GenericTag<T>(id)ServiceMap.Service<T>(id)
Context.Tag(id)<Self, Shape>()ServiceMap.Service<Self, Shape>()(id)
Effect.Tag(id)<Self, Shape>()ServiceMap.Service<Self, Shape>()(id)
Effect.Service<Self>()(id, opts)ServiceMap.Service<Self>()(id, { make })
Context.Reference<Self>()(id, opts)ServiceMap.Reference<T>(id, opts)
Context.make(tag, impl)ServiceMap.make(tag, impl)
Context.get(ctx, tag)ServiceMap.get(map, tag)
Context.add(ctx, tag, impl)ServiceMap.add(map, tag, impl)
Context.mergeAll(...)ServiceMap.mergeAll(...)

Summary

The migration from Context.Tag to ServiceMap.Service involves:
  1. Rename imports from Context to ServiceMap
  2. Update service definitions to use ServiceMap.Service
  3. Replace accessor patterns with yield* or Service.use
  4. Manually create layers for services with make (no auto-generated .Default)
  5. Use layer naming convention instead of Default or Live

Build docs developers (and LLMs) love