Skip to content

Services & Layers

Most real programs depend on things they don’t construct themselves: a database connection, an HTTP client, a clock, a configuration value. Effect models these dependencies as services, tracked in the third type parameter of Effect<Success, Error, Requirements>. When an effect needs a service, that requirement shows up in its type and stays there until you provide an implementation.

import { Context, Effect } from "effect"
// A service is just a typed key into the "context" — the bag of dependencies
// an effect can reach for. The class itself is the key.
class Database extends Context.Service<Database, {
query(sql: string): Effect.Effect<ReadonlyArray<unknown>>
}>()("myapp/Database") {}
// Yielding the key gives you the implementation. Notice the requirement
// `Database` appears in the effect's type — this program cannot run until a
// `Database` is provided.
const program: Effect.Effect<ReadonlyArray<unknown>, never, Database> =
Effect.gen(function*() {
const db = yield* Database
return yield* db.query("SELECT * FROM users")
})

The compiler now enforces that program is given a Database before it runs. You never thread services through function arguments by hand — Effect tracks them for you and fails to compile if one is missing.

There are two halves to dependency injection in Effect:

  • A service is the interface and its context key. You declare it once with Context.Service (or Context.Reference when it should have a default).
  • A layer is the recipe that builds the service’s implementation, possibly acquiring resources and depending on other services. You construct layers with Layer.effect / Layer.succeed and wire them together with Layer.provide.

Keeping these separate is what makes Effect code testable: the same program runs against a real Database.layer in production and an in-memory stub in tests, with no change to the program itself.

  • Services — define a service with Context.Service and attach its layer.
  • ReferencesContext.Reference for defaults, config values, and feature flags that don’t need to be provided.
  • Managing layers — build layers with effect / succeed / scoped acquisition and provide them.
  • Layer composition — combine layers with Layer.provide and Layer.provideMerge.
  • Layers from config — choose an implementation at build time with Layer.unwrap.
  • Keyed resources with LayerMap — manage a dynamic, cached set of per-key services.
  • Managed runtime — bridge your layers into non-Effect frameworks with ManagedRuntime.

Configuration is covered in depth in Configuration, and resource lifecycles in Resource Management.