Dependency Injection
Effect’s dependency injection is built on Context.Service and Layer. You define services as classes, access them with yield*, provide implementations with layers, and wire everything at the entry point.
Defining a Service
Use the Context.Service class syntax. The first type parameter is the service class itself, the second is the interface shape:
import { Context, Effect, Schema } from "effect"
class DatabaseError extends Schema.TaggedError<DatabaseError>()("DatabaseError", {
cause: Schema.Defect()
}) {}
export class Database extends Context.Service<Database, {
query: (sql: string) => Effect.Effect<unknown[], DatabaseError>
}>()("myapp/Database") {}
The string identifier ("myapp/Database") should include your package name and the path to the service file. It’s used for debugging and runtime identification.
Accessing a Service
Inside Effect.gen, yield the service class directly:
const program = Effect.gen(function*() {
const db = yield* Database
const rows = yield* db.query("SELECT * FROM users")
return rows
})
The service type flows into the R (requirements) channel. The program above has type Effect.Effect<unknown[], DatabaseError, Database> - it requires a Database to run.
Providing an Implementation
Layer.effect builds a service from an effect. Attach it as a static property on the service class:
import { Layer } from "effect"
export class Database extends Context.Service<Database, {
query: (sql: string) => Effect.Effect<unknown[], DatabaseError>
}>()("myapp/Database") {
static readonly Live = Layer.effect(Database, Effect.gen(function*() {
const query = Effect.fnUntraced(function*(sql: string) {
yield* Effect.log("Executing:", sql)
return [{ id: 1, name: "Alice" }]
})
return Database.of({ query })
}))
}
Layer.succeed provides a static instance when no construction effect is needed:
const DatabaseTest = Layer.succeed(Database, Database.of({
query: () => Effect.succeed([{ id: 0, name: "test" }])
}))
Wiring with Layer.provide
Compose layers and provide them to your program:
const program = Effect.gen(function*() {
const db = yield* Database
return yield* db.query("SELECT 1")
})
const runnable = program.pipe(
Effect.provide(Database.Live)
)
Effect.runPromise(runnable)
For multiple services, compose layers first, then provide once:
const MainLive = Layer.mergeAll(
Database.Live,
UserService.Live,
Logger.Live
)
const runnable = program.pipe(Effect.provide(MainLive))
See Layer Composition for the full composition model.
Context.Reference for Config
Context.Reference defines a service with a default value. Use it for configuration, feature flags, and settings that can be overridden but don’t need a full layer:
import { Context, Effect } from "effect"
class ApiBaseUrl extends Context.Reference<ApiBaseUrl>()("ApiBaseUrl", {
defaultValue: "http://localhost:3000"
}) {}
// Access like any service
const program = Effect.gen(function*() {
const url = yield* ApiBaseUrl
return `${url}/users`
})
// Override at the entry point
const runnable = program.pipe(
Effect.provide(Layer.succeed(ApiBaseUrl, "https://api.example.com"))
)
v4 Auto-Memoization
In v4, layers are automatically memoized within a runtime. If Database.Live is used by multiple services, it’s built once and shared. No manual Layer.memoize needed:
// Both UserService and OrderService depend on Database
// Database is constructed once, shared by both
const MainLive = Layer.mergeAll(
Database.Live,
UserService.Live,
OrderService.Live
)
Tip: Keep your service interfaces narrow. Only expose the methods that consumers need. This makes testing easier - you only need to mock what’s actually called.
Gotcha: The string identifier in
Context.Servicemust be unique across your application. Duplicate identifiers cause silent context collisions where one service overwrites another.
See The Effect Type for how the R channel tracks requirements, and Layer Composition for the full dependency graph model.