Skip to content
Docs menu / Managing Layers

Managing Layers

In the Managing Services page, you learned how to create effects which depend on some service to be provided in order to execute, as well as how to provide that service to an effect.

However, what if we have a service within our effect program that has dependencies on other services in order to be built? We want to avoid leaking these implementation details into the service interface.

To represent the “dependency graph” of our program and manage these dependencies more effectively, we can utilize a powerful abstraction called “Layer”.

Layers act as constructors for creating services, allowing us to manage dependencies during construction rather than at the service level. This approach helps to keep our service interfaces clean and focused.

Let’s review some key concepts before diving into the details:

Concept Description
service A reusable component providing specific functionality, used across different parts of an application.
service key A unique identifier representing a service, allowing Effect to locate and use it.
context A collection of services, functioning like a map with service keys as keys and services as values.
layer An abstraction for constructing services, managing dependencies during construction rather than at the service level.

Designing the Dependency Graph

Let’s imagine that we are building a web application. We could imagine that the dependency graph for an application where we need to manage configuration, logging, and database access might look something like this:

  • The Config service provides application configuration.
  • The Logger service depends on the Config service.
  • The Database service depends on both the Config and Logger services.

Our goal is to build the Database service along with its direct and indirect dependencies. This means we need to ensure that the Config service is available for both Logger and Database, and then provide these dependencies to the Database service.

Avoiding Requirement Leakage

When constructing the Database service, it’s important to avoid exposing the dependencies on Config and Logger within the Database interface.

You might be tempted to define the Database service as follows:

Example (Leaking Dependencies in the Service Interface)

import { Effect, Context } from "effect"
// Declaring a service key for the Config service
class Config extends Context.Service<Config, {}>()("Config") {}
// Declaring a service key for the Logger service
class Logger extends Context.Service<Logger, {}>()("Logger") {}
// Declaring a service key for the Database service
class Database extends Context.Service<
Database,
{
// ❌ Avoid exposing Config and Logger as a requirement
readonly query: (
sql: string,
) => Effect.Effect<unknown, never, Config | Logger>
}
>()("Database") {}
Database.key // => "Database"

Here, the query function of the Database service requires both Config and Logger. This design leaks implementation details, making the Database service aware of its dependencies, which complicates testing and makes it difficult to mock.

To demonstrate the problem, let’s create a test instance of the Database service:

Example (Creating a Test Instance with Leaked Dependencies)

import { Effect, Context } from "effect"
15 collapsed lines
// Declaring a service key for the Config service
class Config extends Context.Service<Config, {}>()("Config") {}
// Declaring a service key for the Logger service
class Logger extends Context.Service<Logger, {}>()("Logger") {}
// Declaring a service key for the Database service
class Database extends Context.Service<
Database,
{
readonly query: (
sql: string,
) => Effect.Effect<unknown, never, Config | Logger>
}
>()("Database") {}
// Declaring a test instance of the Database service
const DatabaseTest = Database.of({
// Simulating a simple response
query: (sql: string) => Effect.succeed([]),
})
import * as assert from "node:assert"
// A test that uses the Database service
const test = Effect.gen(function* () {
const database = yield* Database
const result = yield* database.query("SELECT * FROM users")
assert.deepStrictEqual(result, [])
})
// ┌─── Effect<unknown, never, Config | Logger>
// ▼
const incompleteTestSetup = test.pipe(
// Attempt to provide only the Database service without Config and Logger
Effect.provideService(Database, DatabaseTest),
)
Database.key // => "Database"

Because the Database service interface directly includes dependencies on Config and Logger, it forces any test setup to include these services, even if they’re irrelevant to the test. This adds unnecessary complexity and makes it difficult to write simple, isolated unit tests.

Instead of directly tying dependencies to the Database service interface, dependencies should be managed at the construction phase.

We can use layers to properly construct the Database service and manage its dependencies without leaking details into the interface.

Creating Layers

The Layer type is structured as follows:

┌─── The service to be created
│ ┌─── The possible error
│ │ ┌─── The required dependencies
▼ ▼ ▼
Layer<RequirementsOut, Error, RequirementsIn>

A Layer represents a blueprint for constructing a RequirementsOut (the service). It requires a RequirementsIn (dependencies) as input and may result in an error of type Error during the construction process.

Parameter Description
RequirementsOut The service or resource to be created.
Error The type of error that might occur during the construction of the service.
RequirementsIn The dependencies required to construct the service.

By using layers, you can better organize your services, ensuring that their dependencies are clearly defined and separated from their implementation details.

For simplicity, let’s assume that we won’t encounter any errors during the value construction (meaning Error = never).

Now, let’s determine how many layers we need to implement our dependency graph:

Layer Dependencies Type
ConfigLive The Config service does not depend on any other services Layer<Config>
LoggerLive The Logger service depends on the Config service Layer<Logger, never, Config>
DatabaseLive The Database service depends on Config and Logger Layer<Database, never, Config | Logger>

When a service has multiple dependencies, they are represented as a union type. In our case, the Database service depends on both the Config and Logger services. Therefore, the type for the DatabaseLive layer will be:

Layer<Database, never, Config | Logger>

Config

The Config service does not depend on any other services, so ConfigLive will be the simplest layer to implement. Just like in the Managing Services page, we must create a service key for the service. And because the service has no dependencies, we can create the layer directly using the Layer.succeed constructor:

import { Effect, Context, Layer } from "effect"
// Declaring a service key for the Config service
class Config extends Context.Service<
Config,
{
readonly getConfig: Effect.Effect<{
readonly logLevel: string
readonly connection: string
}>
}
>()("Config") {}
// Layer<Config, never, never>
const ConfigLive = Layer.succeed(
Config,
Config.of({
getConfig: Effect.succeed({
logLevel: "INFO",
connection: "mysql://username:password@hostname:port/database_name",
}),
}),
)
await Effect.runPromise(
Effect.provide(Config.pipe(Effect.andThen((c) => c.getConfig)), ConfigLive),
) // => { logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name" }

Looking at the type of ConfigLive we can observe:

  • RequirementsOut is Config, indicating that constructing the layer will produce a Config service
  • Error is never, indicating that layer construction cannot fail
  • RequirementsIn is never, indicating that the layer has no dependencies

Note that, to construct ConfigLive, we used the Config.of constructor. However, this is merely a helper to ensure correct type inference for the implementation. It’s possible to skip this helper and construct the implementation directly as a simple object:

import { Effect, Context, Layer } from "effect"
// Declaring a service key for the Config service
9 collapsed lines
class Config extends Context.Service<
Config,
{
readonly getConfig: Effect.Effect<{
readonly logLevel: string
readonly connection: string
}>
}
>()("Config") {}
// Layer<Config, never, never>
const ConfigLive = Layer.succeed(Config, {
getConfig: Effect.succeed({
logLevel: "INFO",
connection: "mysql://username:password@hostname:port/database_name",
}),
})
await Effect.runPromise(
Effect.provide(Config.pipe(Effect.andThen((c) => c.getConfig)), ConfigLive),
) // => { logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name" }

Logger

Now we can move on to the implementation of the Logger service, which depends on the Config service to retrieve some configuration.

Just like we did in the Managing Services page, we can yield the Config service key to “extract” the service from the context.

Given that using the Config service key is an effectful operation, we use Layer.effect to create a layer from the resulting effect.

import { Effect, Context, Layer } from "effect"
// Declaring a service key for the Config service
17 collapsed lines
class Config extends Context.Service<
Config,
{
readonly getConfig: Effect.Effect<{
readonly logLevel: string
readonly connection: string
}>
}
>()("Config") {}
// Layer<Config, never, never>
const ConfigLive = Layer.succeed(Config, {
getConfig: Effect.succeed({
logLevel: "INFO",
connection: "mysql://username:password@hostname:port/database_name",
}),
})
// Declaring a service key for the Logger service
class Logger extends Context.Service<
Logger,
{ readonly log: (message: string) => Effect.Effect<void> }
>()("Logger") {}
// Layer<Logger, never, Config>
const LoggerLive = Layer.effect(
Logger,
Effect.gen(function* () {
const config = yield* Config
return {
log: (message) =>
Effect.gen(function* () {
const { logLevel } = yield* config.getConfig
console.log(`[${logLevel}] ${message}`)
}),
}
}),
)
await Effect.runPromise(
Effect.provide(
Logger.pipe(Effect.andThen((logger) => logger.log("hello"))),
Layer.provide(LoggerLive, ConfigLive),
),
) // => undefined

Looking at the type of LoggerLive:

Layer<Logger, never, Config>

we can observe that:

  • RequirementsOut is Logger
  • Error is never, indicating that layer construction cannot fail
  • RequirementsIn is Config, indicating that the layer has a requirement

Database

Finally, we can use our Config and Logger services to implement the Database service.

import { Effect, Context, Layer } from "effect"
// Declaring a service key for the Config service
17 collapsed lines
class Config extends Context.Service<
Config,
{
readonly getConfig: Effect.Effect<{
readonly logLevel: string
readonly connection: string
}>
}
>()("Config") {}
// Layer<Config, never, never>
const ConfigLive = Layer.succeed(Config, {
getConfig: Effect.succeed({
logLevel: "INFO",
connection: "mysql://username:password@hostname:port/database_name",
}),
})
// Declaring a service key for the Logger service
19 collapsed lines
class Logger extends Context.Service<
Logger,
{ readonly log: (message: string) => Effect.Effect<void> }
>()("Logger") {}
// Layer<Logger, never, Config>
const LoggerLive = Layer.effect(
Logger,
Effect.gen(function* () {
const config = yield* Config
return {
log: (message) =>
Effect.gen(function* () {
const { logLevel } = yield* config.getConfig
console.log(`[${logLevel}] ${message}`)
}),
}
}),
)
// Declaring a service key for the Database service
class Database extends Context.Service<
Database,
{ readonly query: (sql: string) => Effect.Effect<unknown> }
>()("Database") {}
// Layer<Database, never, Config | Logger>
const DatabaseLive = Layer.effect(
Database,
Effect.gen(function* () {
const config = yield* Config
const logger = yield* Logger
return {
query: (sql: string) =>
Effect.gen(function* () {
yield* logger.log(`Executing query: ${sql}`)
const { connection } = yield* config.getConfig
return { result: `Results from ${connection}` }
}),
}
}),
)
const AppConfigLive = Layer.merge(ConfigLive, LoggerLive).pipe(
Layer.provide(ConfigLive),
)
await Effect.runPromise(
Effect.provide(
Database.pipe(Effect.andThen((db) => db.query("SELECT * FROM users"))),
Layer.provide(DatabaseLive, AppConfigLive),
),
) // => { result: "Results from mysql://username:password@hostname:port/database_name" }

Looking at the type of DatabaseLive:

Layer<Database, never, Config | Logger>

we can observe that the RequirementsIn type is Config | Logger, i.e., the Database service requires both Config and Logger services.

Combining Layers

Layers can be combined in two primary ways: merging and composing.

Merging Layers

Layers can be combined through merging using the Layer.merge function:

import { Layer } from "effect"
declare const layer1: Layer.Layer<"Out1", never, "In1">
declare const layer2: Layer.Layer<"Out2", never, "In2">
// Layer<"Out1" | "Out2", never, "In1" | "In2">
const merging = Layer.merge(layer1, layer2)

When we merge two layers, the resulting layer:

  • requires all the services that both of them require ("In1" | "In2").
  • produces all services that both of them produce ("Out1" | "Out2").

For example, in our web application above, we can merge our ConfigLive and LoggerLive layers into a single AppConfigLive layer, which retains the requirements of both layers (never | Config = Config) and the outputs of both layers (Config | Logger):

import { Effect, Context, Layer } from "effect"
// Declaring a service key for the Config service
17 collapsed lines
class Config extends Context.Service<
Config,
{
readonly getConfig: Effect.Effect<{
readonly logLevel: string
readonly connection: string
}>
}
>()("Config") {}
// Layer<Config, never, never>
const ConfigLive = Layer.succeed(Config, {
getConfig: Effect.succeed({
logLevel: "INFO",
connection: "mysql://username:password@hostname:port/database_name",
}),
})
// Declaring a service key for the Logger service
19 collapsed lines
class Logger extends Context.Service<
Logger,
{ readonly log: (message: string) => Effect.Effect<void> }
>()("Logger") {}
// Layer<Logger, never, Config>
const LoggerLive = Layer.effect(
Logger,
Effect.gen(function* () {
const config = yield* Config
return {
log: (message) =>
Effect.gen(function* () {
const { logLevel } = yield* config.getConfig
console.log(`[${logLevel}] ${message}`)
}),
}
}),
)
// Layer<Config | Logger, never, Config>
const AppConfigLive = Layer.merge(ConfigLive, LoggerLive)
await Effect.runPromise(
Effect.provide(
Config.pipe(Effect.andThen((c) => c.getConfig)),
AppConfigLive.pipe(Layer.provide(ConfigLive)),
),
) // => { logLevel: "INFO", connection: "mysql://username:password@hostname:port/database_name" }

Composing Layers

Layers can be composed using the Layer.provide function:

import { Layer } from "effect"
declare const inner: Layer.Layer<"OutInner", never, "InInner">
declare const outer: Layer.Layer<"InInner", never, "InOuter">
// Layer<"OutInner", never, "InOuter">
const composition = Layer.provide(inner, outer)

Sequential composition of layers implies that the output of one layer is supplied as the input for the inner layer, resulting in a single layer with the requirements of the outer layer and the output of the inner.

Now we can compose the AppConfigLive layer with the DatabaseLive layer:

import { Effect, Context, Layer } from "effect"
// Declaring a service key for the Config service
17 collapsed lines
class Config extends Context.Service<
Config,
{
readonly getConfig: Effect.Effect<{
readonly logLevel: string
readonly connection: string
}>
}
>()("Config") {}
// Layer<Config, never, never>
const ConfigLive = Layer.succeed(Config, {
getConfig: Effect.succeed({
logLevel: "INFO",
connection: "mysql://username:password@hostname:port/database_name",
}),
})
// Declaring a service key for the Logger service
19 collapsed lines
class Logger extends Context.Service<
Logger,
{ readonly log: (message: string) => Effect.Effect<void> }
>()("Logger") {}
// Layer<Logger, never, Config>
const LoggerLive = Layer.effect(
Logger,
Effect.gen(function* () {
const config = yield* Config
return {
log: (message) =>
Effect.gen(function* () {
const { logLevel } = yield* config.getConfig
console.log(`[${logLevel}] ${message}`)
}),
}
}),
)
// Declaring a service key for the Database service
21 collapsed lines
class Database extends Context.Service<
Database,
{ readonly query: (sql: string) => Effect.Effect<unknown> }
>()("Database") {}
// Layer<Database, never, Config | Logger>
const DatabaseLive = Layer.effect(
Database,
Effect.gen(function* () {
const config = yield* Config
const logger = yield* Logger
return {
query: (sql: string) =>
Effect.gen(function* () {
yield* logger.log(`Executing query: ${sql}`)
const { connection } = yield* config.getConfig
return { result: `Results from ${connection}` }
}),
}
}),
)
// Layer<Config | Logger, never, Config>
const AppConfigLive = Layer.merge(ConfigLive, LoggerLive)
// Layer<Database, never, never>
const MainLive = DatabaseLive.pipe(
// provides the config and logger to the database
Layer.provide(AppConfigLive),
// provides the config to AppConfigLive
Layer.provide(ConfigLive),
)
await Effect.runPromise(
Effect.provide(
Database.pipe(Effect.andThen((db) => db.query("SELECT * FROM users"))),
MainLive,
),
) // => { result: "Results from mysql://username:password@hostname:port/database_name" }

We obtained a MainLive layer that produces the Database service:

Layer<Database, never, never>

This layer is the fully resolved layer for our application.

Merging and Composing Layers

Let’s say we want our MainLive layer to return both the Config and Database services. We can achieve this with Layer.provideMerge:

import { Effect, Context, Layer } from "effect"
// Declaring a service key for the Config service
16 collapsed lines
class Config extends Context.Service<
Config,
{
readonly getConfig: Effect.Effect<{
readonly logLevel: string
readonly connection: string
}>
}
>()("Config") {}
const ConfigLive = Layer.succeed(Config, {
getConfig: Effect.succeed({
logLevel: "INFO",
connection: "mysql://username:password@hostname:port/database_name",
}),
})
// Declaring a service key for the Logger service
18 collapsed lines
class Logger extends Context.Service<
Logger,
{ readonly log: (message: string) => Effect.Effect<void> }
>()("Logger") {}
const LoggerLive = Layer.effect(
Logger,
Effect.gen(function* () {
const config = yield* Config
return {
log: (message) =>
Effect.gen(function* () {
const { logLevel } = yield* config.getConfig
console.log(`[${logLevel}] ${message}`)
}),
}
}),
)
// Declaring a service key for the Database service
20 collapsed lines
class Database extends Context.Service<
Database,
{ readonly query: (sql: string) => Effect.Effect<unknown> }
>()("Database") {}
const DatabaseLive = Layer.effect(
Database,
Effect.gen(function* () {
const config = yield* Config
const logger = yield* Logger
return {
query: (sql: string) =>
Effect.gen(function* () {
yield* logger.log(`Executing query: ${sql}`)
const { connection } = yield* config.getConfig
return { result: `Results from ${connection}` }
}),
}
}),
)
// Layer<Config | Logger, never, Config>
const AppConfigLive = Layer.merge(ConfigLive, LoggerLive)
// Layer<Config | Database, never, never>
const MainLive = DatabaseLive.pipe(
Layer.provide(AppConfigLive),
Layer.provideMerge(ConfigLive),
)
await Effect.runPromise(
Effect.provide(
Effect.gen(function* () {
const config = yield* Config
const database = yield* Database
const queryResult = yield* database.query("SELECT * FROM users")
return { logLevel: (yield* config.getConfig).logLevel, queryResult }
}),
MainLive,
),
) // => { logLevel: "INFO", queryResult: { result: "Results from mysql://username:password@hostname:port/database_name" } }

Providing a Layer to an Effect

Now that we have assembled the fully resolved MainLive for our application, we can provide it to our program to satisfy the program’s requirements using Effect.provide:

import { Effect, Context, Layer } from "effect"
63 collapsed lines
class Config extends Context.Service<
Config,
{
readonly getConfig: Effect.Effect<{
readonly logLevel: string
readonly connection: string
}>
}
>()("Config") {}
const ConfigLive = Layer.succeed(Config, {
getConfig: Effect.succeed({
logLevel: "INFO",
connection: "mysql://username:password@hostname:port/database_name",
}),
})
class Logger extends Context.Service<
Logger,
{ readonly log: (message: string) => Effect.Effect<void> }
>()("Logger") {}
const LoggerLive = Layer.effect(
Logger,
Effect.gen(function* () {
const config = yield* Config
return {
log: (message) =>
Effect.gen(function* () {
const { logLevel } = yield* config.getConfig
console.log(`[${logLevel}] ${message}`)
}),
}
}),
)
class Database extends Context.Service<
Database,
{ readonly query: (sql: string) => Effect.Effect<unknown> }
>()("Database") {}
const DatabaseLive = Layer.effect(
Database,
Effect.gen(function* () {
const config = yield* Config
const logger = yield* Logger
return {
query: (sql: string) =>
Effect.gen(function* () {
yield* logger.log(`Executing query: ${sql}`)
const { connection } = yield* config.getConfig
return { result: `Results from ${connection}` }
}),
}
}),
)
const AppConfigLive = Layer.merge(ConfigLive, LoggerLive)
const MainLive = DatabaseLive.pipe(
Layer.provide(AppConfigLive),
Layer.provide(ConfigLive),
)
// ┌─── Effect<unknown, never, Database>
// ▼
const program = Effect.gen(function* () {
const database = yield* Database
const result = yield* database.query("SELECT * FROM users")
return result
})
// ┌─── Effect<unknown, never, never>
// ▼
const runnable = Effect.provide(program, MainLive)
await Effect.runPromise(runnable) // => { result: "Results from mysql://username:password@hostname:port/database_name" }
/*
Output:
[INFO] Executing query: SELECT * FROM users
*/

Note that the runnable requirements type is never, indicating that the program does not require any additional services to run.

Converting a Layer to an Effect

Sometimes your entire application might be a Layer, for example, an HTTP server. You can convert that layer to an effect with Layer.launch. It constructs the layer and keeps it alive until interrupted.

Example (Launching an HTTP Server Layer)

import { Console, Context, Effect, Layer } from "effect"
class HTTPServer extends Context.Service<HTTPServer, void>()("HTTPServer") {}
// Simulating an HTTP server
const server = Layer.effect(
HTTPServer,
// Log a message to simulate a server starting
Console.log("Listening on http://localhost:3000"),
)
// Converts the layer to an effect and runs it
Effect.runFork(Layer.launch(server))
/*
Output:
Listening on http://localhost:3000
...
*/
// Layer.launch never completes on its own; building the layer directly
// lets us verify what it produces without hanging the process
const context = await Effect.runPromise(Effect.scoped(Layer.build(server)))
Context.get(context, HTTPServer) // => undefined

Tapping

The Layer.tap and Layer.tapError functions allow you to perform additional effects based on the success or failure of a layer. These operations do not modify the layer’s signature but are useful for logging or performing side effects during layer construction.

  • Layer.tap: Executes a specified effect when the layer is successfully acquired.
  • Layer.tapError: Executes a specified effect when the layer fails to acquire.

Example (Logging Success and Failure During Layer Acquisition)

import { Config, Context, Effect, Layer, Console } from "effect"
class HTTPServer extends Context.Service<HTTPServer, void>()("HTTPServer") {}
// Simulating an HTTP server
const server = Layer.effect(
HTTPServer,
Effect.gen(function* () {
const host = yield* Config.string("HOST")
console.log(`Listening on http://localhost:${host}`)
}),
).pipe(
// Log a message if the layer acquisition succeeds
Layer.tap((ctx) => Console.log(`layer acquisition succeeded with:\n${ctx}`)),
// Log a message if the layer acquisition fails
Layer.tapError((err) =>
Console.log(`layer acquisition failed with:\n${err}`),
),
)
Effect.runFork(Layer.launch(server))
/*
Output:
layer acquisition failed with:
(Missing data at HOST: "Expected HOST to exist in the process context")
*/

Error Handling

When constructing layers, it is important to handle potential errors. Layer.catch can inspect an acquisition error and return a fallback layer.

catch

The Layer.catch function allows you to recover from errors during layer construction by specifying a fallback layer. This can be useful for handling specific error cases and ensuring the application can continue with an alternative setup.

Example (Recovering from Errors During Layer Construction)

import { Config, Context, Effect, Layer } from "effect"
class HTTPServer extends Context.Service<HTTPServer, void>()("HTTPServer") {}
// Simulating an HTTP server
const server = Layer.effect(
HTTPServer,
Effect.gen(function* () {
const host = yield* Config.string("HOST")
console.log(`Listening on http://localhost:${host}`)
}),
).pipe(
// Recover from errors during layer construction
Layer.catch((configError) =>
Layer.effect(
HTTPServer,
Effect.gen(function* () {
console.log(`Recovering from error:\n${configError}`)
console.log(`Listening on http://localhost:3000`)
}),
),
),
)
Effect.runFork(Layer.launch(server))
/*
Output:
Recovering from error:
(Missing data at HOST: "Expected HOST to exist in the process context")
Listening on http://localhost:3000
...
*/

If the fallback does not need the error, ignore the argument passed to Layer.catch.

Example (Falling Back to an Alternative Layer)

import { Config, Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, void>()("Database") {}
// Simulating a database connection
const postgresDatabaseLayer = Layer.effect(
Database,
Effect.gen(function* () {
const databaseConnectionString = yield* Config.string("CONNECTION_STRING")
console.log(`Connecting to database with: ${databaseConnectionString}`)
}),
)
// Simulating an in-memory database connection
const inMemoryDatabaseLayer = Layer.effect(
Database,
Effect.gen(function* () {
console.log(`Connecting to in-memory database`)
}),
)
// Fallback to in-memory database if PostgreSQL connection fails
const database = postgresDatabaseLayer.pipe(
Layer.catch(() => inMemoryDatabaseLayer),
)
Effect.runFork(Layer.launch(database))
/*
Output:
Connecting to in-memory database
...
*/

Defining Services with Context.Service

Context.Service can define a service key and its construction effect together as a single class. You can then expose layers as static fields on that class.

Defining a Service with Dependencies

The following example defines a Cache service that depends on a file system.

Example (Defining a Cache Service)

import { NodeFileSystem } from "@effect/platform-node"
import { Context, Effect, FileSystem, Layer } from "effect"
// Define a Cache service
class Cache extends Context.Service<Cache>()("app/Cache", {
// Define how to create the service
make: Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
const lookup = (key: string) => fs.readFileString(`cache/${key}`)
return { lookup } as const
}),
}) {
// Specify dependencies
static readonly layer = Layer.effect(this, this.make).pipe(
Layer.provide(NodeFileSystem.layer),
)
}
Cache.key // => "app/Cache"

Declaring the Service’s Layers

Declare a service’s layers as static class fields, built from this.make with Layer.effect. The field names below are just a convention, not part of the API.

Static field Description
Cache.layer Provides the Cache service with its dependencies already included.
Cache.layerWithoutDependencies Provides the Cache service but requires dependencies to be provided separately.
import { NodeFileSystem } from "@effect/platform-node"
import { Context, Effect, FileSystem, Layer } from "effect"
// Define a Cache service
10 collapsed lines
class Cache extends Context.Service<Cache>()("app/Cache", {
make: Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
const lookup = (key: string) => fs.readFileString(`cache/${key}`)
return { lookup } as const
}),
}) {
static readonly layer = Layer.effect(this, this.make).pipe(
Layer.provide(NodeFileSystem.layer),
)
static readonly layerWithoutDependencies = Layer.effect(this, this.make)
}
// Layer that includes all required dependencies
//
// ┌─── Layer<Cache>
// ▼
const layer = Cache.layer
// Layer without dependencies, requiring them to be provided externally
//
// ┌─── Layer.Layer<Cache, never, FileSystem>
// ▼
const layerNoDeps = Cache.layerWithoutDependencies
// Exercise layerNoDeps with a test FileSystem to confirm the wiring works
const FileSystemTest = FileSystem.layerNoop({
readFileString: () => Effect.succeed("File Content..."),
})
await Effect.runPromise(
Effect.provide(
Effect.gen(function* () {
const cache = yield* Cache
return yield* cache.lookup("my-key")
}),
Layer.provide(layerNoDeps, FileSystemTest),
),
) // => "File Content..."

Accessing the Service

A service created with Context.Service can be accessed like any other Effect service.

Example (Accessing the Cache Service)

import { NodeFileSystem } from "@effect/platform-node"
import { Context, Effect, FileSystem, Layer, Console } from "effect"
// Define a Cache service
10 collapsed lines
class Cache extends Context.Service<Cache>()("app/Cache", {
make: Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
const lookup = (key: string) => fs.readFileString(`cache/${key}`)
return { lookup } as const
}),
}) {
static readonly layer = Layer.effect(this, this.make).pipe(
Layer.provide(NodeFileSystem.layer),
)
static readonly layerWithoutDependencies = Layer.effect(this, this.make)
}
// Accessing the Cache Service
const program = Effect.gen(function* () {
const cache = yield* Cache
const data = yield* cache.lookup("my-key")
console.log(data)
}).pipe(Effect.catchCause((cause) => Console.log(cause)))
const runnable = program.pipe(Effect.provide(Cache.layer))
Effect.runFork(runnable)
/*
{
_id: 'Cause',
_tag: 'Fail',
failure: {
_tag: 'SystemError',
reason: 'NotFound',
module: 'FileSystem',
method: 'readFile',
pathOrDescriptor: 'cache/my-key',
syscall: 'open',
message: "ENOENT: no such file or directory, open 'cache/my-key'",
[Symbol(@effect/platform/Error/PlatformErrorTypeId)]: Symbol(@effect/platform/Error/PlatformErrorTypeId)
}
}
*/

Since this example uses Cache.layer, it interacts with the real file system. If the file does not exist, it results in an error.

Injecting Test Dependencies

To test the program without depending on the real file system, we can inject a test file system using the Cache.layerWithoutDependencies layer.

Example (Using a Test File System)

import { NodeFileSystem } from "@effect/platform-node"
import { Context, Effect, FileSystem, Layer, Console } from "effect"
// Define a Cache service
10 collapsed lines
class Cache extends Context.Service<Cache>()("app/Cache", {
make: Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
const lookup = (key: string) => fs.readFileString(`cache/${key}`)
return { lookup } as const
}),
}) {
static readonly layer = Layer.effect(this, this.make).pipe(
Layer.provide(NodeFileSystem.layer),
)
static readonly layerWithoutDependencies = Layer.effect(this, this.make)
}
5 collapsed lines
// Accessing the Cache Service
const program = Effect.gen(function* () {
const cache = yield* Cache
const data = yield* cache.lookup("my-key")
console.log(data)
}).pipe(Effect.catchCause((cause) => Console.log(cause)))
// Create a test file system that always returns a fixed value
const FileSystemTest = FileSystem.layerNoop({
readFileString: () => Effect.succeed("File Content..."),
})
const runnable = program.pipe(
Effect.provide(Cache.layerWithoutDependencies),
// Provide the mock file system
Effect.provide(FileSystemTest),
)
Effect.runFork(runnable)
// Output: File Content...
await Effect.runPromise(runnable) // => undefined

Mocking the Service Directly

Alternatively, you can mock the Cache service itself instead of replacing its dependencies.

Example (Mocking the Cache Service)

import { NodeFileSystem } from "@effect/platform-node"
import { Context, Effect, FileSystem, Layer, Console } from "effect"
// Define a Cache service
10 collapsed lines
class Cache extends Context.Service<Cache>()("app/Cache", {
make: Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
const lookup = (key: string) => fs.readFileString(`cache/${key}`)
return { lookup } as const
}),
}) {
static readonly layer = Layer.effect(this, this.make).pipe(
Layer.provide(NodeFileSystem.layer),
)
static readonly layerWithoutDependencies = Layer.effect(this, this.make)
}
5 collapsed lines
// Accessing the Cache Service
const program = Effect.gen(function* () {
const cache = yield* Cache
const data = yield* cache.lookup("my-key")
console.log(data)
}).pipe(Effect.catchCause((cause) => Console.log(cause)))
// Create a mock implementation of Cache
const cache = Cache.of({
lookup: () => Effect.succeed("Cache Content..."),
})
// Provide the mock Cache service
const runnable = program.pipe(Effect.provideService(Cache, cache))
Effect.runFork(runnable)
// Output: Cache Content...
await Effect.runPromise(runnable) // => undefined

Alternative Ways to Construct make

Context.Service’s make field accepts any Effect, allowing services to use different construction styles:

Style How to build make
Static value Effect.succeed(...): a constant implementation.
Synchronous Effect.sync(() => ...): a synchronous constructor.
Effectful Effect.gen(function* () { ... }) (or any other Effect): a constructor that itself depends on other services.
Scoped Effect.gen(...) using Effect.acquireRelease/Effect.addFinalizer inside make: lifecycle management, no separate option.

Example (Defining a Service with a Static Implementation)

This is the simplest way to define a service. It is useful when you want to provide a constant value for the service.

import { Context, Effect, Layer } from "effect"
class MagicNumber extends Context.Service<MagicNumber>()("MagicNumber", {
make: Effect.succeed({ value: 42 }),
}) {
static readonly layer = Layer.effect(this, this.make)
}
// ┌─── Effect<void, never, MagicNumber>
// ▼
const program = Effect.gen(function* () {
const magicNumber = yield* MagicNumber
console.log(`The magic number is ${magicNumber.value}`)
})
await Effect.runPromise(program.pipe(Effect.provide(MagicNumber.layer))) // => undefined
// The magic number is 42

Example (Defining a Service with a Synchronous Constructor)

import { Context, Effect, Layer, Random } from "effect"
class Sync extends Context.Service<Sync>()("Sync", {
make: Effect.sync(() => ({
next: Random.nextInt,
})),
}) {
static readonly layer = Layer.effect(this, this.make)
}
// ┌─── Effect<void, never, Sync>
// ▼
const program = Effect.gen(function* () {
const sync = yield* Sync
const n = yield* sync.next
console.log(`The number is ${n}`)
})
await Effect.runPromise(program.pipe(Effect.provide(Sync.layer))) // => undefined
// Example Output: The number is 3858843290019673

Example (Managing a Service with Lifecycle Control)

import { Context, Effect, Layer, Console } from "effect"
class Scoped extends Context.Service<Scoped>()("Scoped", {
make: Effect.gen(function* () {
// Acquire the resource and ensure it is properly released
const resource = yield* Effect.acquireRelease(
Console.log("Acquiring...").pipe(Effect.as("foo")),
() => Console.log("Releasing..."),
)
// Register a finalizer to run when the effect is completed
yield* Effect.addFinalizer(() => Console.log("Shutting down"))
return { resource }
}),
}) {
static readonly layer = Layer.effect(this, this.make)
}
// ┌─── Effect<void, never, Scoped>
// ▼
const program = Effect.gen(function* () {
const resource = (yield* Scoped).resource
console.log(`The resource is ${resource}`)
})
await Effect.runPromise(
program.pipe(
Effect.provide(
// ┌─── Layer<Scoped, never, never>
// ▼
Scoped.layer,
),
),
) // => undefined
/*
Acquiring...
The resource is foo
Shutting down
Releasing...
*/

The Scoped.layer layer does not require Scope as a dependency, since Scoped itself manages its lifecycle.