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.Service must 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.