Context.Tag is the lock, and the Service is the key, then a Layer is the factory that builds the key. Layers are the ultimate solution for wiring complex, asynchronous dependency graphs.TL;DR (Quick Summary)#
- The Problem: Using
.provideService()is fine for simple mocks, but fails when a Service itself requires other Services to be built (e.g.,UserServiceneedsDatabaseService). - What is a Layer?: A
Layeris a blueprint or recipe that describes how to construct a service. - Asynchronous Wiring: Layers can be created from Effects, meaning you can easily model a service that requires an asynchronous connection (like a database pool) before it starts.
- Providing Layers: You construct a massive graph of Layers, merge them, and provide them to your main program using
Effect.provide(layer).
1. Introduction: The Dependency Graph Problem#
In Episode 65, we learned how to inject a single DatabaseService using Effect.provideService.
But real applications are not flat. They are massive graphs.
- The
RouteHandlerrequires theUserService. - The
UserServicerequires theDatabaseServiceand theLoggerService. - The
DatabaseServicerequires theConfigService.
If we tried to wire this using only Effect.provideService, we would have to manually instantiate the ConfigService, then manually pass it into the DatabaseService constructor, and so on. This is exactly what NestJS or Spring Boot handles for you under the hood.
In Effect, we solve this using Layers.
2. What is a Layer?#
A Layer (Layer<ServiceOut, Error, ServiceIn>) is an instruction manual for building a service.
Notice it has three type parameters, just like Effect!
- ServiceOut: The Service this layer produces.
- Error: The error that might happen while building the service (e.g., failed to connect to the DB).
- ServiceIn: The other Services this layer requires to build itself.
3. Step-by-Step: Building and Wiring Layers#
Let’s build a mini-application with three tiers: Config -> Database -> User.
Step 1: Define the Tags#
import { Context, Effect, Layer } from "effect";
// Define the interfaces
interface ConfigService { readonly dbUrl: string; }
interface DatabaseService { readonly query: (sql: string) => Effect.Effect<any, Error>; }
interface UserService { readonly getUser: (id: string) => Effect.Effect<string, Error>; }
// Create the Tags
const ConfigTag = Context.GenericTag<ConfigService>("ConfigService");
const DatabaseTag = Context.GenericTag<DatabaseService>("DatabaseService");
const UserTag = Context.GenericTag<UserService>("UserService");Step 2: Build the Simple Layers#
The ConfigService doesn’t require anything else. We can build it synchronously using Layer.succeed.
// 🟢 A layer that requires nothing (never) and produces ConfigService
const ConfigLayer = Layer.succeed(
ConfigTag,
{ dbUrl: "postgres://localhost:5432" }
);Step 3: Build the Dependent Layers#
The DatabaseService requires the ConfigService to initialize! We use Layer.effect to describe this requirement.
// 🟢 A layer that requires ConfigService and produces DatabaseService
const DatabaseLayer = Layer.effect(
DatabaseTag,
Effect.pipe(
// We ask the runtime for the ConfigService
ConfigTag,
Effect.map((config) => {
console.log(`Connecting to ${config.dbUrl}...`);
return {
query: (sql) => Effect.succeed(`Result for ${sql}`)
};
})
)
);Step 4: Wire the Graph Together#
Now we need to provide ConfigLayer into DatabaseLayer. We do this using the Layer.provide function.
// 🟢 DatabaseLayer fully resolved! It no longer requires ConfigService!
const ResolvedDatabaseLayer = Layer.provide(ConfigLayer, DatabaseLayer);You can also use Layer.merge to combine two independent layers together (e.g., merging a LoggerLayer and a MetricsLayer).
5. Providing the Graph to the Program#
Finally, we have our business logic that requires the DatabaseService. We provide the fully resolved layer to the program.
const program = Effect.pipe(
DatabaseTag,
Effect.flatMap(db => db.query("SELECT * FROM users")),
Effect.flatMap(res => Effect.sync(() => console.log(res)))
);
// 🔴 Error: TS2345: Argument of type 'Effect<void, Error, DatabaseService>'...
// Effect.runPromise(program);
// 🟢 Provide the layer!
const executableProgram = Effect.provide(program, ResolvedDatabaseLayer);
// Output:
// "Connecting to postgres://localhost:5432..."
// "Result for SELECT * FROM users"
Effect.runPromise(executableProgram);6. Context vs Layers Comparison#
| Feature | Effect.provideService (Context) | Layer.provide (Layers) |
|---|---|---|
| Use Case | Single, simple, synchronous mock implementations. | Complex, asynchronous, multi-tiered dependency graphs. |
| Initialization | Must be instantiated manually before calling. | Instantiated automatically by the Effect Runtime when needed. |
| Dependencies | Cannot easily depend on other injected services. | Natively depends on and resolves other Layers. |
| Memoization | N/A. | Layers are memoized by default (initialized only once per runtime). |
7. Troubleshooting & Common Errors#
Error 1: Circular Dependencies#
The Mistake: ServiceA requires ServiceB, and ServiceB requires ServiceA.
The Cause: You have an architectural flaw. The Effect compiler will detect this when you try to use Layer.provide and will throw a massive type error indicating that the ServiceIn requirement can never be satisfied.
The Fix: Refactor your architecture. Extract the shared logic into a ServiceC that both A and B depend on.
Error 2: Forgetting to Provide a Deep Layer#
TS2345: Argument of type 'Layer<DatabaseService, never, ConfigService>' is not assignable to parameter of type 'Layer<DatabaseService, never, never>'.The Cause: You tried to provide the DatabaseLayer directly to the program, but you forgot to provide the ConfigLayer to the DatabaseLayer first!
The Fix: Always resolve the graph from the bottom up using Layer.provide.
Summary & Next Steps#
In this episode:
- We discovered why
.provideServicefalls short for complex dependency graphs. - We defined a
Layeras a blueprint for building a service. - We built independent layers (
Layer.succeed) and dependent layers (Layer.effect). - We wired the layers together and provided them to our main program.
Our architecture is now perfectly decoupled and testable. But what happens when we need to do 10,000 database queries at the exact same time? How do we handle massive concurrency without destroying Node.js?
In Episode 67: Concurrency & Fibers, we will unlock the true performance superpower of Effect-TS!

