Skip to main content

Overview

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

v3: Context.GenericTag
import { Context } from "effect"

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

const Database = Context.GenericTag<Database>("Database")
v4: ServiceMap.Service
import { ServiceMap } from "effect"

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

const Database = ServiceMap.Service<Database>("Database")
The API is almost identical — just replace Context.GenericTag with ServiceMap.Service.

Class-Based Services

v3: Context.Tag class syntax
import { Context } from "effect"

class Database extends Context.Tag("Database")<Database, {
  readonly query: (sql: string) => string
}>() {}
v4: ServiceMap.Service class syntax
import { ServiceMap } from "effect"

class Database extends ServiceMap.Service<Database, {
  readonly query: (sql: string) => string
}>()("Database") {}
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).

Service Accessors: Effect.TagServiceMap.Service with use

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")

Why Accessors Were Removed

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>. For the same reason, overloaded signatures were not preserved.

Using Service.use

In v4, accessors are removed. The most direct replacement is Service.use, which receives the service instance and runs a callback: v3
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")
v4 — use
import { Effect, ServiceMap } from "effect"

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

// use: access the service and call a method in one step
const program = Notifications.use((n) => n.notify("hello"))

use vs 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>. Both return Effects — useSync just allows the accessor function itself to be synchronous:
//      ┌─── Effect<void, never, Notifications>
//      ▼
const program = Notifications.use((n) => n.notify("hello"))

//      ┌─── Effect<number, never, Config>
//      ▼
const port = Config.useSync((c) => c.port)

When to Use yield* Instead

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. 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")
})

Services with Constructors: Effect.ServiceServiceMap.Service with make

v3’s Effect.Service allowed defining a service with an effectful constructor and dependencies inline. In v4, use ServiceMap.Service with a make option.

Basic Pattern

v3 In v3, Effect.Service automatically generated a .Default layer from the provided constructor, and wired dependencies into it:
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>
// (dependencies are already wired in)
const program = Effect.gen(function*() {
  const logger = yield* Logger
  yield* logger.log("hello")
}).pipe(Effect.provide(Logger.Default))
v4 In v4, ServiceMap.Service with make stores the constructor effect on the class but does not auto-generate a layer. Define layers explicitly using Layer.effect:
import { Effect, Layer, ServiceMap } from "effect"

class Logger extends ServiceMap.Service<Logger>()("Logger", {
  make: Effect.gen(function*() {
    const config = yield* Config
    return { log: (msg: string) => Effect.log(`[${config.prefix}] ${msg}`) }
  })
}) {
  // Build the layer yourself from the make effect
  static readonly layer = Layer.effect(this, this.make).pipe(
    Layer.provide(Config.layer)
  )
}
The dependencies option no longer exists. Wire dependencies via Layer.provide as shown above.

Layer Naming Convention

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

v3: Context.Reference
import { Context } from "effect"

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

const LogLevel = ServiceMap.Reference<"info" | "warn" | "error">("LogLevel", {
  defaultValue: () => "info" as const
})

Context Operations

All Context operations have equivalent ServiceMap operations:
v3v4
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(...)

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)
Tag static accessors (e.g. Tag.method())Tag.use((t) => t.method()) or yield* Tag

Complete Example

Here’s a complete before/after example showing service definition, dependencies, and layers: v3
import { Context, Effect, Layer } from "effect"

class Config extends Context.Tag("Config")<Config, {
  readonly dbUrl: string
  readonly port: number
}>() {}

const ConfigLive = Layer.succeed(Config, {
  dbUrl: "postgresql://localhost/mydb",
  port: 3000
})

class Database extends Effect.Service<Database>()("Database", {
  effect: Effect.gen(function*() {
    const config = yield* Config
    return {
      query: (sql: string) => Effect.succeed(`Querying ${config.dbUrl}: ${sql}`)
    }
  }),
  dependencies: [ConfigLive]
}) {}

class UserService extends Effect.Tag("UserService")<UserService, {
  readonly getUser: (id: number) => Effect.Effect<string>
}>() {}

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

const program = UserService.getUser(1)

Effect.runPromise(
  program.pipe(Effect.provide(UserServiceLive))
)
v4
import { Effect, Layer, ServiceMap } from "effect"

class Config extends ServiceMap.Service<Config, {
  readonly dbUrl: string
  readonly port: number
}>()("Config") {}

const ConfigLive = Layer.succeed(Config, {
  dbUrl: "postgresql://localhost/mydb",
  port: 3000
})

class Database extends ServiceMap.Service<Database>()("Database", {
  make: Effect.gen(function*() {
    const config = yield* Config
    return {
      query: (sql: string) => Effect.succeed(`Querying ${config.dbUrl}: ${sql}`)
    }
  })
}) {
  static readonly layer = Layer.effect(this, this.make).pipe(
    Layer.provide(ConfigLive)
  )
}

class UserService extends ServiceMap.Service<UserService, {
  readonly getUser: (id: number) => Effect.Effect<string>
}>()("UserService") {}

const UserServiceLive = Layer.effect(
  UserService,
  Effect.gen(function*() {
    const db = yield* Database
    return {
      getUser: (id) => db.query(`SELECT * FROM users WHERE id = ${id}`)
    }
  })
).pipe(Layer.provide(Database.layer))

// Use .use() instead of static accessor
const program = UserService.use((service) => service.getUser(1))

// Or prefer yield* for clarity
const program2 = Effect.gen(function*() {
  const userService = yield* UserService
  return yield* userService.getUser(1)
})

Effect.runPromise(
  program.pipe(Effect.provide(UserServiceLive))
)

Migration Checklist

  • Replace Context.GenericTag<T>(id) with ServiceMap.Service<T>(id)
  • Replace Context.Tag(id)<Self, Shape>() with ServiceMap.Service<Self, Shape>()(id)
  • Replace Effect.Tag(id)<Self, Shape>() with ServiceMap.Service<Self, Shape>()(id)
  • Replace Effect.Service with ServiceMap.Service and make option
  • Replace static accessor calls with .use() or yield*
  • Replace Context.Reference with ServiceMap.Reference
  • Rename .Default layers to .layer
  • Remove dependencies option and use Layer.provide instead
  • Update Context.* operations to ServiceMap.*

Build docs developers (and LLMs) love