Skip to main content
The adapters layer is the outermost infrastructure ring. It provides concrete Layer values that satisfy the port contracts defined in the service layer. Two full stacks are available: lightweight in-memory implementations used in most tests, and SQLite-backed SQL implementations for production and contract tests. Because both stacks implement the same port interfaces, swapping between them requires changing a single Layer at the composition root.

In-Memory Adapters

The in-memory adapters use plain JavaScript Map objects and synchronous Effect.sync / Effect.succeed calls. They have no setup cost, need no migration, and can be constructed fresh for each test.
export const InMemoryMenuRepositoryLive = Layer.succeed(MenuRepository)({
  list: Effect.succeed(menuItems),
  findById: (drinkId) =>
    Effect.succeed(Option.fromUndefinedOr(menuItems.find((item) => item.id === drinkId))),
});
The menu catalog is the static menuItems array from #domain/menu. Layer.succeed is used here because the implementation is pure and requires no Effect-level setup.

SQL Adapters

The SQL adapters use Effect’s unstable/sql module with an SQLite backend (@effect/sql-sqlite-bun for local runs, @effect/sql-d1 for Cloudflare Workers). They wrap all SQL errors in PersistenceError before returning, so use cases see a uniform error type regardless of the underlying driver.
export const SqlMenuRepositoryLive = Layer.effect(
  MenuRepository,
  Effect.gen(function* () {
    const queries = yield* makeSqlMenuQueries;

    return MenuRepository.of({
      list: queries.list.pipe(
        PersistenceError.refail("Failed to load the coffee menu"),
      ),
      findById: (drinkId) =>
        queries
          .findById(drinkId)
          .pipe(PersistenceError.refail(`Failed to load menu item "${drinkId}"`)),
    });
  }),
);
makeSqlMenuQueries yields SqlClient.SqlClient and builds typed query helpers using SqlSchema.findAll and SqlSchema.findOneOption. Each query selects from the menu_items table ordered by sortOrder, id.
PersistenceError.refail is a helper on the PersistenceError class that catches any failure or defect from a sub-effect and wraps it in a PersistenceError with a descriptive message. This prevents low-level SQL errors from leaking into the service layer’s error channel.

Layer Pattern: Wiring Adapters to Ports

Each adapter file exports a Layer value typed as Layer.Layer<PortClass, never, Dependencies>. The service layer’s CoffeeOrderApp.layer requires MenuRepository, OrderRepository, and OrderIdGenerator — all three must be provided before the application can start.
CoffeeOrderApp.layer
  requires: MenuRepository | OrderRepository | OrderIdGenerator

           ├── InMemoryMenuRepositoryLive  (provides MenuRepository)
           ├── InMemoryOrderRepositoryLive (provides OrderRepository)
           └── InMemoryOrderIdGeneratorLive (provides OrderIdGenerator)

live.ts: The Composition Root

backend/src/external/live.ts is the single place where individual adapter layers are merged into named stacks.
// In-memory stack (tests and local dev)
export const InMemoryCoffeeRepositoriesLive = Layer.mergeAll(
  InMemoryMenuRepositoryLive,
  InMemoryOrderRepositoryLive,
);

export const InMemoryCoffeeAppLive = Layer.mergeAll(
  InMemoryCoffeeRepositoriesLive,
  InMemoryOrderIdGeneratorLive,
);

// SQL stack (production)
export const SqlCoffeeRepositoriesLive = Layer.mergeAll(
  SqlCoffeePersistenceLive,
  SqlMenuRepositoryLive,
  SqlOrderRepositoryLive,
);

export const SqlCoffeeAppLive = Layer.mergeAll(
  SqlCoffeeRepositoriesLive,
  InMemoryOrderIdGeneratorLive,
);
Both InMemoryCoffeeAppLive and SqlCoffeeAppLive reuse InMemoryOrderIdGeneratorLive. The SQL stack does not have a database-backed ID generator — the in-memory counter is sufficient because IDs are monotonically increasing strings, not database sequences.

Swapping Adapter Stacks

1

Tests

Use InMemoryCoffeeAppLive. Provide it alongside CoffeeOrderApp.layer in the Effect test setup. Each test gets a fresh layer with an empty Map.
2

Local HTTP server

The presentation layer’s main.ts provides either InMemoryCoffeeAppLive or SqlCoffeeAppLive depending on a runtime flag or environment variable.
3

Cloudflare Workers

SqlCoffeeAppLive backed by @effect/sql-d1 replaces the SQLite Bun driver. The rest of the stack — ports, use cases, domain — is unchanged.
Because the Layer type carries its requirements in the type parameter, TypeScript will report a compile error if you provide SqlCoffeeAppLive without also providing SqlCoffeePersistenceLive, which supplies the SqlClient that the SQL repositories depend on.

Build docs developers (and LLMs) love