Skip to content

Services

A service is a typed dependency: an interface plus a context key that lets Effect find its implementation at runtime. In Effect v4 you declare one by extending Context.Service, passing the class as the first type parameter and the service’s shape as the second. By convention the implementation lives on a static layer, so the whole service — interface, key, and how to build it — stays in one file.

src/db/Database.ts
import { Context, Effect, Layer, Schema } from "effect"
// Errors are schema-defined tagged errors. `Schema.Defect` carries an unknown
// underlying cause (e.g. a thrown driver error) in a typed field.
export class DatabaseError extends Schema.TaggedErrorClass<DatabaseError>()(
"DatabaseError",
{ cause: Schema.Defect }
) {}
// The first type param is the class itself (the context key); the second is the
// service's interface. The string id should be unique — prefix it with your
// package and the file path.
export class Database extends Context.Service<Database, {
query(sql: string): Effect.Effect<ReadonlyArray<unknown>, DatabaseError>
}>()("myapp/db/Database") {
// The implementation is built lazily by a Layer. `Layer.effect` runs an
// Effect once, when the layer is built, and stores the result as the service.
static readonly layer = Layer.effect(
Database,
Effect.gen(function*() {
// Functions that return Effects are written with `Effect.fn` so they get
// a span/name — never a plain function that returns `Effect.gen(...)`.
const query = Effect.fn("Database.query")(function*(sql: string) {
yield* Effect.log(`Executing: ${sql}`)
return [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]
})
// `Database.of` constructs an instance that satisfies the interface — it
// gives you type checking on the object you return.
return Database.of({ query })
})
)
}

Using the service is symmetric: yield* the class to get the implementation, then call its methods. The requirement is tracked in the effect’s type until the layer is provided.

src/main.ts
import { Effect } from "effect"
import { Database } from "./db/Database.ts"
const program = Effect.gen(function*() {
const db = yield* Database
const users = yield* db.query("SELECT * FROM users")
yield* Effect.log(`Found ${users.length} users`)
})
// `program` has type Effect<void, DatabaseError, Database>. Provide the layer to
// discharge the `Database` requirement, then run it.
Effect.runFork(program.pipe(Effect.provide(Database.layer)))
  • The class is the key. Context.Service<Self, Shape>()("id") returns a class whose runtime identity is the string id. Two services with the same id occupy the same slot in the context, so keep ids unique.
  • yield* Database reaches into the surrounding context and returns the stored implementation. If no Database was provided, the program won’t type-check at the point you try to run it.
  • Database.of(impl) is an identity function that exists purely for type inference — it checks impl against the declared interface.

Outside of Effect.gen, two helpers let you reach a service inline. They return an effect that still carries the service requirement:

import { Effect } from "effect"
import { Database } from "./db/Database.ts"
// `use` for an effectful access...
const rows = Database.use((db) => db.query("SELECT 1"))
// ...`useSync` when the accessor returns a plain value, not an effect.
const queryFn = Database.useSync((db) => db.query)

A service’s layer is an ordinary Effect, so it can yield* other services it needs. Those become requirements of the layer, which you satisfy when you compose it — see Layer composition.

import { Context, Effect, Layer } from "effect"
import { Database } from "./db/Database.ts"
export class UserRepo extends Context.Service<UserRepo, {
countUsers: Effect.Effect<number>
}>()("myapp/UserRepo") {
static readonly layer = Layer.effect(
UserRepo,
Effect.gen(function*() {
// Depending on Database here makes `Database` a requirement of this layer.
const db = yield* Database
const countUsers = Effect.gen(function*() {
const rows = yield* db.query("SELECT * FROM users")
return rows.length
})
return UserRepo.of({ countUsers })
})
// `Layer.provide` feeds Database in, so the resulting layer only needs nothing.
).pipe(Layer.provide(Database.layer))
}

Next: give a service a default value with References, or learn the full set of layer constructors in Managing layers.