How the Effect-TS Dependency Injection System Works in Magnitude
The Effect-TS DI system in Magnitude uses Context.Tag as type-safe service identifiers, Layer as immutable providers, and Effect.provide to wire dependencies into the runtime, enabling modular, testable service composition.
The open-source Magnitude project relies entirely on the Effect-TS library for runtime composition and dependency management. Understanding how its Effect-TS dependency injection system functions is essential for contributing to or extending the codebase. This article explains the architectural patterns found in the magnitudedev/magnitude repository, from declaring service tags to composing complex layer graphs.
Core Concepts: Tag, Layer, and Effect
Effect-TS dependency injection revolves around three primitives that work together to create a type-safe service container.
Context.Tag acts as a unique key that identifies a service interface. It carries the type signature of the service, ensuring that consumers receive the correct implementation at compile time.
Layer describes how to construct and provide a service. Layers are immutable values that can be composed, merged, or overridden without side effects. They come in two primary flavors: Layer.succeed for immediate values and Layer.effect for asynchronous construction.
Effect.provide attaches a layer to an effect, making the services available at runtime. Inside the effect, you access the implementation using Effect.service or Effect.serviceWith.
Defining Services with Context.Tag
Every public service in Magnitude begins as a subclass of Context.Tag. The generic arguments specify both the identifier and the service interface.
In [packages/storage/src/storage.ts](https://github.com/magnitudedev/magnitude/blob/main/packages/storage/src/storage.ts#L39), the storage layer declares its service tag:
import { Context, Effect } from "effect";
export class MagnitudeStorage extends Context.Tag('MagnitudeStorage')<
MagnitudeStorage,
{ get(key: string): Effect.Effect<unknown, unknown, unknown> }
>() {}
This pattern appears throughout the codebase. For example, the VCS filesystem service is defined similarly in [packages/vcs/src/vcs-fs.ts](https://github.com/magnitudedev/magnitude/blob/main/packages/vcs/src/vcs-fs.ts), while the query client is defined in [packages/effect-query/src/internal.ts](https://github.com/magnitudedev/magnitude/blob/main/packages/effect-query/src/internal.ts#L44).
Providing Implementations via Layer
Once a tag exists, you supply the concrete implementation through a Layer. Magnitude uses two primary strategies for creating layers.
Layer.effect constructs the service asynchronously within an Effect workflow. This allows you to fetch dependencies and perform setup logic. The storage service uses this approach in [packages/storage/src/storage.ts](https://github.com/magnitudedev/magnitude/blob/main/packages/storage/src/storage.ts#L44):
import { Layer, Effect } from "effect";
export const StorageLive = Layer.effect(
MagnitudeStorage,
Effect.gen(function* (_) {
const fs = yield* _(/* lower-level filesystem tag */);
return {
get: (key) => Effect.succeed(/* read from fs */),
};
})
);
Layer.succeed immediately provides a pre-constructed value. This is ideal for deterministic implementations with no setup cost. The VCS filesystem uses this pattern in [packages/vcs/src/vcs-fs.ts](https://github.com/magnitudedev/magnitude/blob/main/packages/vcs/src/vcs-fs.ts#L11):
export const VcsFsLive = Layer.succeed(VcsFs, realFs);
Composing the Dependency Graph
Complex applications require multiple services. Magnitude assembles these using Layer.mergeAll and Layer.provide to build a unified environment.
The VCS layer factory in [packages/vcs/src/layer.ts](https://github.com/magnitudedev/magnitude/blob/main/packages/vcs/src/layer.ts#L833) demonstrates this composition. It constructs a shadow VCS layer and injects the filesystem dependency:
const vcsLayer = makeShadowVcsLayer({ worktreePath, storagePath })
.pipe(Layer.provide(VcsFsLive));
const appLayer = Layer.mergeAll(StorageLive, vcsLayer, OtherDeps);
Layer.mergeAll combines multiple individual layers into a single layer that provides every service simultaneously. Layer.provide creates a dependency relationship where one layer's requirements are satisfied by another layer's output.
Accessing Services in Effects
After providing layers, effects access services using Effect.service or Effect.serviceWith. This retrieves the concrete implementation from the runtime context.
In [packages/client-common/src/state/client-services.ts](https://github.com/magnitudedev/magnitude/blob/main/packages/client-common/src/state/client-services.ts#L39), the client query is accessed via its tag:
import { Effect } from "effect";
const fetchData = Effect.gen(function* (_) {
const query = yield* _(Effect.service(ClientEffectQuery));
return yield* _(query.run(/* ... */));
});
const result = fetchData.pipe(Effect.provide(appLayer));
The Effect.service function looks up the implementation associated with the tag in the current context. If the layer providing that service has not been applied, the effect fails to compile or run.
Testing with Mock Layers
The DI system enables testability by allowing you to swap production implementations with mocks. You achieve this by replacing the production layer with Layer.succeed containing a test double.
For example, when testing the VCS module, you can replace the real filesystem with an in-memory version:
import { Layer, Effect } from "effect";
import { VcsFs } from "@magnitudedev/vcs";
import { MemoryFileSystem } from "./test-utils";
const testLayer = Layer.succeed(VcsFs, new MemoryFileSystem());
test("VCS operation", async () => {
const result = await someVcsEffect.pipe(
Effect.provide(testLayer),
Effect.runPromise
);
// assertions...
});
This pattern appears throughout Magnitude's test suites, allowing components to be validated in isolation without external side effects.
Summary
- Context.Tag defines type-safe service identifiers that carry interface contracts.
- Layer.effect builds services asynchronously with dependency fetching, while Layer.succeed provides immediate values.
- Layer.mergeAll and Layer.provide compose individual layers into a complete application context.
- Effect.service retrieves service implementations inside effect workflows.
- Tests replace production layers with mocks using
Layer.succeedto eliminate side effects.
Frequently Asked Questions
What is Context.Tag in Effect-TS?
Context.Tag is a type-safe identifier that acts as a key for services in the dependency injection container. It carries both the service interface and a unique symbol to prevent key collisions. In Magnitude, every major service like MagnitudeStorage or ClientEffectQuery is declared as a subclass of Context.Tag.
How does Layer.succeed differ from Layer.effect?
Layer.succeed creates a layer that immediately provides a pre-constructed service value without any effects, ideal for deterministic implementations like the filesystem in VcsFsLive. Layer.effect constructs the service asynchronously within an Effect workflow, allowing you to fetch dependencies via yield* and perform setup logic, as seen in the StorageLive implementation.
How do you override dependencies for testing?
You replace the production layer with a mock using Layer.succeed(Tag, mockImplementation) and then provide that layer to your test effect via Effect.provide. This pattern appears throughout Magnitude's test suites, where VcsFs is swapped with an in-memory filesystem to avoid side effects during validation.
What is the purpose of Layer.mergeAll?
Layer.mergeAll combines multiple individual layers into a single layer that provides all dependencies simultaneously, creating a unified environment for complex effects. In Magnitude, this is used to assemble the complete application context from modular service layers like storage, VCS, and client services before executing the main program.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →