Guideline
For large applications, avoid a single, flat list of services. Instead, structure your application by creating hierarchical layers:
BaseLayer: Provides application-wide infrastructure (Logger, Config, Database).FeatureModuleLayers: Provide the services for a specific business domain (e.g.,UserModule,ProductModule). These depend on theBaseLayer.AppLayer: The top-level layer that composes the feature modules by providing them with theBaseLayer.
Rationale
As an application grows, a flat composition strategy where all services are merged into one giant layer becomes unwieldy and hard to reason about. The Composable Modules pattern solves this by introducing structure.
This approach creates a clean, scalable, and highly testable architecture where complexity is contained within each module. The top-level composition becomes a clear, high-level diagram of your application's architecture, and feature modules can be tested in isolation by providing them with a mocked BaseLayer.
Good Example
This example shows a BaseLayer with a Logger, a UserModule that uses the Logger, and a final AppLayer that wires them together.
1. The Base Infrastructure Layer
// src/core/Logger.ts
import { Effect } from "effect";
export class Logger extends Effect.Service<Logger>()("App/Core/Logger", {
sync: () => ({
log: (msg: string) => Effect.log(`[LOG] ${msg}`),
}),
}) {}
// src/features/User/UserRepository.ts
export class UserRepository extends Effect.Service<UserRepository>()(
"App/User/UserRepository",
{
// Define implementation that uses Logger
effect: Effect.gen(function* () {
const logger = yield* Logger;
return {
findById: (id: number) =>
Effect.gen(function* () {
yield* logger.log(`Finding user ${id}`);
return { id, name: `User ${id}` };
}),
};
}),
// Declare Logger dependency
dependencies: [Logger.Default],
}
) {}
// Example usage
const program = Effect.gen(function* () {
const repo = yield* UserRepository;
const user = yield* repo.findById(1);
return user;
});
// Run with default implementations
Effect.runPromise(Effect.provide(program, UserRepository.Default));
const programWithLogging = Effect.gen(function* () {
const result = yield* program;
yield* Effect.log(`Program result: ${JSON.stringify(result)}`);
return result;
});
Effect.runPromise(Effect.provide(programWithLogging, UserRepository.Default));
2. The Feature Module Layer
// src/core/Logger.ts
import { Effect } from "effect";
export class Logger extends Effect.Service<Logger>()("App/Core/Logger", {
sync: () => ({
log: (msg: string) => Effect.sync(() => console.log(`[LOG] ${msg}`)),
}),
}) {}
// src/features/User/UserRepository.ts
export class UserRepository extends Effect.Service<UserRepository>()(
"App/User/UserRepository",
{
// Define implementation that uses Logger
effect: Effect.gen(function* () {
const logger = yield* Logger;
return {
findById: (id: number) =>
Effect.gen(function* () {
yield* logger.log(`Finding user ${id}`);
return { id, name: `User ${id}` };
}),
};
}),
// Declare Logger dependency
dependencies: [Logger.Default],
}
) {}
// Example usage
const program = Effect.gen(function* () {
const repo = yield* UserRepository;
const user = yield* repo.findById(1);
return user;
});
// Run with default implementations
Effect.runPromise(Effect.provide(program, UserRepository.Default)).then(
console.log
);
3. The Final Application Composition
// src/layers.ts
import { Layer } from "effect";
import { BaseLayer } from "./core";
import { UserModuleLive } from "./features/User";
// import { ProductModuleLive } from "./features/Product";
const AllModules = Layer.mergeAll(UserModuleLive /*, ProductModuleLive */);
// Provide the BaseLayer to all modules at once, creating a self-contained AppLayer.
export const AppLayer = Layer.provide(AllModules, BaseLayer);
Anti-Pattern
A flat composition strategy for a large application. While simple at first, it quickly becomes difficult to manage.
// ❌ This file becomes huge and hard to navigate in a large project.
const AppLayer = Layer.mergeAll(
LoggerLive,
ConfigLive,
DatabaseLive,
TracerLive,
UserServiceLive,
UserRepositoryLive,
ProductServiceLive,
ProductRepositoryLive,
BillingServiceLive
// ...and 50 other services
);