| name | effect-v4-services |
| description | Effect v4 Service patterns โ Context.Service, Layer composition, ManagedRuntime, dependency injection, and service testing with @effect/vitest. |
| governed-by | metaskill |
Effect v4 Context.Service โ Canonical Reference
up: none
prereqs: none
provides: services, layers, dependency-injection, managed-runtime, testing
children: CHANGELOG.md
governed-by: metaskill
Source of truth: submodules/effect-smol/packages/effect/src/Context.ts
Migration guide: submodules/effect-smol/MIGRATION.md
Import
import { Context, Effect, Layer, ManagedRuntime } from "effect"
When to Load
- Defining an Effect service (dependency injection)
- Creating Layers (wiring services)
- Composing layers (provide, merge, provideMerge)
- Running programs with ManagedRuntime
- Testing services with @effect/vitest
Defining Services
Class-style (canonical pattern in v4)
class Database extends Context.Service<Database, {
readonly query: (sql: string) => Effect.Effect<Array<unknown>, DatabaseError>
}>()(
"myapp/db/Database"
) {
static readonly layer = Layer.effect(
Database,
Effect.gen(function*() {
const query = Effect.fn("Database.query")(function*(sql: string) {
yield* Effect.log("Executing:", sql)
return [{ id: 1, name: "Alice" }]
})
return Database.of({ query })
})
)
}
Class-style with inline make + dependencies
class Database extends Context.Service<Database, {
readonly query: (sql: string) => Effect.Effect<Array<unknown>, DatabaseError>
}>()(
"myapp/db/Database",
{
make: Effect.gen(function*() {
const pool = yield* ConnectionPool
return {
query: Effect.fn("Database.query")(function*(sql: string) {
const conn = yield* pool.acquire()
return yield* conn.execute(sql)
})
}
})
}
) {}
Function-style (simple, no class)
const Database = Context.Service<{
query: (sql: string) => Effect.Effect<Array<unknown>>
}>("myapp/Database")
Key Differences from v3
| v3 | v4 |
|---|
class Foo extends Context.Tag("Foo")<Foo, Shape>() {} | class Foo extends Context.Service<Foo, Shape>()("Foo") {} |
class Foo extends Effect.Service<Foo>()("Foo", { ... }) | class Foo extends Context.Service<Foo, Shape>()("Foo", { make: ... }) {} |
Auto-generated .Default layer | No auto .Default. Define .layer manually. |
Effect.Service with scoped + dependencies | Context.Service with make. Use Layer.provide for deps. |
Context.Tag as marker | Context.Service IS the tag/key |
Runtime<R> type | REMOVED. Use Context<R> directly |
Effect.provideService(Tag, impl) | Effect.provide(Layer.succeed(Tag)(impl)) |
Layer.scoped | Layer.effect (scoped semantics folded in) |
Layer.scopedDiscard | Layer.effectDiscard |
ServiceMap.* (beta.23) | Context.* (renamed in beta.44) |
fiber.services | fiber.context (renamed in beta.44) |
Creating Layers
const layer = Layer.effect(
Database,
Effect.gen(function*() {
return Database.of({ query: ... })
})
)
const layer = Layer.sync(Database, () =>
Database.of({ query: ... })
)
const layer = Layer.succeed(Database)({
query: (sql) => Effect.succeed([])
})
const layer = Layer.effectContext(
Effect.gen(function*() {
const impl = { query: ... }
return Context.make(Database, impl)
})
)
const layer = Layer.suspend(() => conditionalLayer)
Composing Layers
const AppLayer = DatabaseLive.pipe(
Layer.provide(ConnectionPoolLive)
)
const FullLayer = DatabaseLive.pipe(
Layer.provideMerge(ConnectionPoolLive)
)
const InfraLayer = Layer.mergeAll(
Database.layer,
Cache.layer,
Logger.layer
)
Consuming Services
const program = Effect.gen(function*() {
const db = yield* Database
const cache = yield* Cache
return yield* db.query("SELECT 1")
})
Effect.runPromise(
program.pipe(Effect.provide(AppLayer))
)
const result = Effect.runSyncWith(services)(program)
const fiber = Effect.runForkWith(services)(program)
Context Data Structure
const ctx = Context.make(Database, dbImpl)
const ctx2 = Context.add(ctx, Cache, cacheImpl)
const db = Context.getUnsafe(ctx, Database)
const combined = Context.merge(ctx1, ctx2)
Context.isContext(value)
ManagedRuntime
import { ManagedRuntime } from "effect"
const runtime = ManagedRuntime.make(AppLayer)
await runtime.runPromise(program)
await runtime.dispose()
Service Naming Convention
Use package + path for uniqueness:
"@tmnl/stx/StxFactory"
"@tmnl/codemode-metaskill/SkillDiscovery"
"myapp/db/Database"
Testing with @effect/vitest
import { describe, it } from "@effect/vitest"
import { Effect, Layer } from "effect"
describe("Database", () => {
it.effect("queries successfully", () =>
Effect.gen(function*() {
const db = yield* Database
const rows = yield* db.query("SELECT 1")
expect(rows).toHaveLength(1)
}).pipe(
Effect.provide(Database.layerTest)
)
)
})