The concrete friction
Hardcoded module imports couple domain logic to physical infrastructure
When a domain module directly imports a database client or external SDK, it binds business logic to physical infrastructure. Testing that logic requires global monkey-patching (like jest.mock or module hijacking), which breaks type checking and causes cross-test race conditions. Alternatively, passing dependencies through function arguments (parameter drilling) clutters every intermediate caller across the entire call stack.
A computation that constructs its own dependencies cannot be reused in another environment. The domain logic becomes hostage to the concrete database, network protocol, and authentication credentials chosen by the author.
type Issue = { readonly id: string; readonly title: string };
type IssueRepository = {
search: (query: string) => Promise<readonly Issue[]>;
};
// The domain receives the dependency explicitly via parameter drilling.
async function searchIssueTitles(
repository: IssueRepository,
query: string
) {
const issues = await repository.search(query);
return issues.map((issue) => issue.title);
}
const repository: IssueRepository = {
search: async (_query) => [
{ id: "ISSUE-101", title: "Add typed search errors" }
]
};
// Every caller must manually pass the repository down the call stack.
const titles = await searchIssueTitles(repository, "effect");The mental model
The Requirements channel (R) inverts dependency management
Context.Service<IssueRepository>("IssueRepository") defines a unique service tag. The domain calls yield* IssueRepository, which registers the dependency in the R channel: Effect<readonly string[], never, IssueRepository>. The domain does not know or care whether the implementation uses PostgreSQL, an in-memory Map, or a mock. The execution boundary satisfies R with Effect.provideService.
searchIssueTitles: Effect<readonly string[], never, IssueRepository>
│
▼ Effect.provideService(...)
runnableProgram: Effect<readonly string[], never, never>
│
▼ Effect.runPromise(...)
Output: titles: Add typed search errorsMinimal working example
Executable source
This is the checked source file used by the course. Read it before you run it.
import { Console, Context, Effect } from "effect"
export type Issue = {
readonly id: string
readonly title: string
}
export interface IssueRepository {
readonly search: (query: string) => Effect.Effect<ReadonlyArray<Issue>>
}
export const IssueRepository =
Context.Service<IssueRepository>("IssueRepository")
const IssueRepositoryTest: IssueRepository = {
search: (_query) =>
Effect.succeed([
{ id: "ISSUE-101", title: "Add typed search errors" }
])
}
export const searchIssueTitles = (
query: string
): Effect.Effect<ReadonlyArray<string>, never, IssueRepository> =>
Effect.gen(function* () {
const repository = yield* IssueRepository
const issues = yield* repository.search(query)
return issues.map((issue) => issue.title)
})
const program = Effect.provideService(
searchIssueTitles("effect"),
IssueRepository,
IssueRepositoryTest
)
await Effect.runPromise(
program.pipe(
Effect.tap((titles) =>
Console.log(`titles: ${titles.join(", ")}`)
)
)
)Execute and verify
Predict the result, run the command, and compare the output with the model.
pnpm exec tsx src/course/examples/services.tsWhere does searchIssueTitles declare that it needs IssueRepository, and what changes after Effect.provideService supplies the test implementation?
Reveal expected output
titles: Add typed search errorsError anatomy and edge cases
Attempting to execute an Effect with unsatisfied requirements
Calling Effect.runPromise on searchIssueTitles("effect") before providing IssueRepository.
TypeScript reports: "TS2345: Argument of type 'Effect<readonly string[], never, IssueRepository>' is not assignable to parameter of type 'Effect<readonly string[], never, never>'. Type 'IssueRepository' is not assignable to type 'never'". At runtime, if executed anyway, the fiber fails immediately with: "Error: Service not found: IssueRepository".
Provide the missing service using Effect.provideService(program, IssueRepository, implementation) or compose dependencies with Layer. Execution functions require R to be never.
Active lab exercise
Exercise: provide the required repository
Fix the Context tag at the execution boundary so the domain receives the repository it declared.
pnpm exec tsx src/exercises/exercise4/exercise.test.ts starterThe harness should fail against the starter. Inspect the assertion, change the starter implementation, and run it again.
Inspect starter code
import { Context, Effect } from "effect"
type Issue = {
readonly id: string
readonly title: string
}
interface IssueRepository {
readonly search: (query: string) => Effect.Effect<ReadonlyArray<Issue>>
}
const IssueRepository = Context.Service<IssueRepository>("IssueRepository")
const testRepository: IssueRepository = {
search: (_query) =>
Effect.succeed([
{ id: "ISSUE-101", title: "Add typed search errors" }
])
}
const WrongIssueRepository = Context.Service<IssueRepository>(
"IssueRepositoryForTests"
)
const searchIssueTitles = (
query: string
): Effect.Effect<ReadonlyArray<string>, never, IssueRepository> =>
Effect.gen(function* () {
const repository = yield* IssueRepository
const issues = yield* repository.search(query)
return issues.map((issue) => issue.title)
})
// TODO: provide testRepository through the IssueRepository tag.
export const program = Effect.provideService(
searchIssueTitles("effect"),
WrongIssueRepository,
testRepository
)Reveal reference solution
import { Context, Effect } from "effect"
type Issue = {
readonly id: string
readonly title: string
}
interface IssueRepository {
readonly search: (query: string) => Effect.Effect<ReadonlyArray<Issue>>
}
const IssueRepository = Context.Service<IssueRepository>("IssueRepository")
const testRepository: IssueRepository = {
search: (_query) =>
Effect.succeed([
{ id: "ISSUE-101", title: "Add typed search errors" }
])
}
const searchIssueTitles = (
query: string
): Effect.Effect<ReadonlyArray<string>, never, IssueRepository> =>
Effect.gen(function* () {
const repository = yield* IssueRepository
const issues = yield* repository.search(query)
return issues.map((issue) => issue.title)
})
export const program = Effect.provideService(
searchIssueTitles("effect"),
IssueRepository,
testRepository
)Context tags are runtime identities. Effect.provideService must use the same IssueRepository tag that the domain retrieves with yield* IssueRepository.