Effect-TS Dependency Injection in Magnitude: Context.Tag Services and Layer.provide Patterns

Magnitude implements dependency injection using Effect-TS's Context.Tag for service declarations and Layer.provide for composing implementations, enabling type-safe, testable, and modular architecture across its monorepo.

Magnitude, an open-source testing framework, leverages Effect-TS's built-in dependency injection (DI) model to maintain stateless, composable functionality across its agent, storage, VCS, and launcher packages. This article examines the specific patterns used: declaring services with Context.Tag, implementing them with Layer, and wiring dependencies through Layer.provide and Layer.mergeAll.

Declaring Services with Context.Tag

In Effect-TS, a Context.Tag acts as a zero-runtime-cost identifier that describes the shape of a dependency. Magnitude defines all its core capabilities as tags, creating type-level contracts that consumers depend on without knowing implementation details.

The file system service in packages/agent/src/services/fs.ts demonstrates this pattern:

export class Fs extends Context.Tag('Fs')<Fs, {
  readonly readFile: (path: string) => Effect.Effect<Buffer, FsError>
  readonly writeFile: (path: string, data: Buffer) => Effect.Effect<void, FsError>
  // …additional methods
}>() {}

The string literal 'Fs' provides a unique key, while the type parameter specifies the interface. Other services follow identical patterns:

Because tags are phantom types—existing only at compile time—Magnitude achieves complete decoupling between interface and implementation.

Implementing Services with Layer Constructors

Magnitude provides concrete implementations through Effect-TS Layer constructors. The choice of constructor depends on the implementation's lifecycle requirements.

Layer.succeed for Immediate Values

Simple, stateless implementations use Layer.succeed to inject ready-made values. The production file system layer in packages/agent/src/services/fs.ts (lines 45-74):

export const FsLive = Layer.succeed(Fs, {
  readFile: (path) => tryFs('readFile', path, async () => await readFile(path)),
  writeFile: (path, data) => tryFs('writeFile', path, async () => await writeFile(path, data)),
  // …
})

Similarly, VcsFsLive in packages/vcs/src/vcs-fs.ts at line 19:

export const VcsFsLive = Layer.succeed(VcsFs, {
  getHeadCommit: (repoPath: string) => Effect.tryPromise(/* … */),
  // …
})

Layer.effect for Effectful Construction

When implementation requires running effects to acquire resources, Magnitude uses Layer.effect. The storage service in packages/storage/src/storage.ts (line 44) constructs its layer through a generator:

export const StorageLive = Layer.effect(
  MagnitudeStorage,
  Effect.gen(function* () {
    const storage = yield* GlobalStorage
    // …additional setup effects…
    return MagnitudeStorage.of({ /* implementation */ })
  })
)

This allows dependent services to be resolved during layer construction.

Layer.scoped for Resource Management

For resources requiring cleanup, Layer.scoped ensures proper acquisition and release. While less common in Magnitude's current codebase, this pattern appears in platform-specific integrations.

Composing Dependencies with Layer.provide and Layer.mergeAll

Magnitude's architecture shines in how it assembles complex dependency graphs. Individual layers merge into unified contexts through Layer.mergeAll, then layers provide dependencies to other layers via Layer.provide.

Merging Independent Layers

The Layer.mergeAll operator combines multiple layers into one. A typical production composition from packages/storage/src/state/default-initialization.test.ts:

const appLayer = Layer.mergeAll(
  BunFileSystem.layer,
  BunPath.layer,
  Layer.succeed(Version, Version.of({ getVersion: () => VERSION })),
  // …additional platform layers
)

Layer-to-Layer Dependency Provision

Merged layers often satisfy dependencies for other layers. The Layer.provide method wires this relationship. From the same test file (lines 16-31):

const program = Effect.gen(function* () {
  const storage = yield* MagnitudeStorage  // consumes multiple services
  // …business logic…
}).pipe(
  Effect.provide(
    StorageLive.pipe(
      Layer.provide(
        Layer.mergeAll(
          BunFileSystem.layer,
          BunPath.layer,
          Layer.succeed(Version, Version.of({ getVersion: () => VERSION }))
        )
      )
    )
  )
)

Here, the merged platform layers (BunFileSystem, BunPath, Version) satisfy StorageLive's requirements, which then fulfills MagnitudeStorage for the main program.

Effect.provide for Final Injection

At the application boundary, Effect.provide attaches the complete layer stack to runnable effects. The launcher in packages/launcher/src/main.ts demonstrates this final wiring step.

Test Implementations and Mocking Strategies

Magnitude's DI model enables seamless test doubles. Test layers replace production implementations without code changes:

// Mock file system for isolated tests
const testFsLayer = Layer.succeed(Fs, {
  readFile: (p) => Effect.succeed(Buffer.from('test fixture content')),
  writeFile: () => Effect.void
})

// Mock version for reproducible test behavior
const MockVersion = Layer.succeed(
  Version,
  Version.of({ getVersion: () => '0.0.0-test' })
)

Tests compose these mocks identically to production code, ensuring type safety across implementation swaps.

Architecture Benefits Across Magnitude Packages

Magnitude's DI patterns create clear architectural boundaries:

  • Clients (CLI, web) import only public SDK contracts
  • SDK exposes pure Context.Tag-based service definitions
  • Daemon and agent packages provide concrete Layer implementations

This hierarchy enables platform-specific adaptations—Bun for server-side, alternative implementations for other runtimes—without touching consumer code.

Summary

  • Context.Tag declares service contracts as compile-time identifiers with zero runtime overhead
  • Layer.succeed injects immediate values for stateless implementations
  • Layer.effect constructs layers from effectful resource acquisition
  • Layer.mergeAll combines independent layers into unified dependency contexts
  • Layer.provide wires layer dependencies, enabling hierarchical composition
  • Effect.provide attaches complete layer stacks to executable programs
  • Test mocks replace production layers through identical constructors, preserving type safety

Frequently Asked Questions

What is the difference between Context.Tag and a traditional service locator?

Context.Tag is a type-level construct with no runtime representation, whereas service locators require runtime registries and string-based lookups. Magnitude's approach catches missing dependencies at compile time and enables tree-shaking of unused implementations.

When should I use Layer.effect instead of Layer.succeed?

Use Layer.effect when constructing the implementation requires running effects—such as reading configuration, acquiring database connections, or resolving other services. Use Layer.succeed for pure, immediately available values.

How does Layer.provide differ from Effect.provide?

Layer.provide wires dependencies between layers during graph construction, producing a new layer. Effect.provide attaches a complete layer to a runnable effect, yielding an effect with reduced requirements. Magnitude uses both: Layer.provide builds the graph, Effect.provide executes it.

Can layers depend on multiple other layers?

Yes. Layer.mergeAll combines multiple layers that a consuming layer may depend on. The dependent layer specifies its requirements through Effect.gen or service constructors, and Effect-TS verifies that the merged layers satisfy all dependencies at compile time.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →