Skip to content

Keyed Resources with LayerMap

Ordinary layers build a fixed set of services. But some applications need a service instance per key that isn’t known ahead of time: a database client per tenant, a connection per region, an API client per account. LayerMap is a scoped, reference-counted cache that builds a layer on demand for each key, reuses it while it’s live, and finalizes it when it’s no longer needed.

import { Console, Context, Effect, Layer, LayerMap } from "effect"
// The per-key service we want a distinct instance of.
class Greeter extends Context.Service<Greeter, {
readonly greet: Effect.Effect<string>
}>()("myapp/Greeter") {}
// A LayerMap.Service wraps the cache and exposes accessor helpers on the class.
// `lookup` is called once per distinct key to produce that key's layer.
class GreeterMap extends LayerMap.Service<GreeterMap>()("myapp/GreeterMap", {
lookup: (name: string) =>
Layer.succeed(Greeter, {
greet: Effect.succeed(`Hello, ${name}!`)
}),
// Entries unused for this long are released automatically.
idleTimeToLive: "5 seconds"
}) {}

To use a keyed resource, provide the layer for a specific key with GreeterMap.get(key). That layer makes the Greeter service available, built from the cache:

import { Console, Effect } from "effect"
import { Greeter } from "./Greeter.ts"
import { GreeterMap } from "./GreeterMap.ts"
const program = Effect.gen(function*() {
const greeter = yield* Greeter
yield* Console.log(yield* greeter.greet)
}).pipe(
// `get("John")` provides a Greeter built for the key "John". Asking again for
// "John" reuses the cached instance; a different key builds a new one.
Effect.provide(GreeterMap.get("John")),
// The map itself is a layer too — provide it once at the top.
Effect.provide(GreeterMap.layer)
)
Effect.runFork(program)

LayerMap.Service<Self>()("id", options) produces a class that is both a normal service (so it has a .layer) and a façade over the underlying cache. The accessor helpers it generates:

  • GreeterMap.get(key) — a Layer that provides the key’s services, building them on first use and reusing the cached instance afterwards.
  • GreeterMap.contextEffect(key) — the acquired Context directly, for when you want the services as a value rather than a layer (requires a Scope).
  • GreeterMap.invalidate(key) — finalize the current cached entry for a key; the next access rebuilds it.

The cache is reference-counted: while anything is using a key’s resources they stay live, and they’re finalized once nobody holds them and idleTimeToLive elapses.

The lookup function returns a full layer, so each key can have its own configured resource — including one with dependencies and cleanup:

import { Context, Effect, Layer, LayerMap } from "effect"
class TenantDb extends Context.Service<TenantDb, {
query(sql: string): Effect.Effect<ReadonlyArray<unknown>>
}>()("myapp/TenantDb") {}
class TenantDbMap extends LayerMap.Service<TenantDbMap>()("myapp/TenantDbMap", {
// Build a distinct, resource-owning layer per tenant id.
lookup: (tenantId: string) =>
Layer.effect(
TenantDb,
Effect.gen(function*() {
// Each tenant gets its own connection, released when the entry expires.
const conn = yield* Effect.acquireRelease(
Effect.sync(() => ({ tenantId, close: () => {} })),
(c) => Effect.sync(() => c.close())
)
return TenantDb.of({
query: (sql) => Effect.succeed([{ tenantId: conn.tenantId, sql }])
})
})
),
idleTimeToLive: "30 seconds"
}) {}
// Handle a request scoped to one tenant.
const handle = (tenantId: string) =>
Effect.gen(function*() {
const db = yield* TenantDb
return yield* db.query("SELECT * FROM orders")
}).pipe(
Effect.provide(TenantDbMap.get(tenantId)),
Effect.provide(TenantDbMap.layer)
)

When the keys are known and finite, pass a layers record instead of a lookup function. The key type becomes the union of record keys:

import { Context, Effect, Layer, LayerMap } from "effect"
import { Greeter } from "./Greeter.ts"
class RegionGreeters extends LayerMap.Service<RegionGreeters>()("myapp/RegionGreeters", {
layers: {
us: Layer.succeed(Greeter, { greet: Effect.succeed("Hi from US") }),
eu: Layer.succeed(Greeter, { greet: Effect.succeed("Hallo from EU") })
}
}) {}
// RegionGreeters.get("us") | "eu" — anything else is a compile error.

LayerMap builds on the same caching machinery as Caching and the scope rules from Resource Management; reach for it specifically when the identity of a resource is a runtime key.