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

> Explore Effect-TS dependency injection patterns like Context.Tag services and Layer.provide in Magnitude. Build type-safe, testable, and modular applications.

- Repository: [Magnitude/magnitude](https://github.com/magnitudedev/magnitude)
- Tags: deep-dive
- Published: 2026-09-06

---

**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`](https://github.com/magnitudedev/magnitude/blob/main/packages/agent/src/services/fs.ts) demonstrates this pattern:

```typescript
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:

- `Version` in [`packages/storage/src/services/version.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/storage/src/services/version.ts) for version retrieval
- `VcsFs` in [`packages/vcs/src/vcs-fs.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/vcs/src/vcs-fs.ts) for version control file operations
- `MagnitudeStorage` in [`packages/storage/src/storage.ts`](https://github.com/magnitudedev/magnitude/blob/main/packages/storage/src/storage.ts) for persistence

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`](https://github.com/magnitudedev/magnitude/blob/main/packages/agent/src/services/fs.ts) (lines 45-74):

```typescript
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`](https://github.com/magnitudedev/magnitude/blob/main/packages/vcs/src/vcs-fs.ts) at line 19:

```typescript
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`](https://github.com/magnitudedev/magnitude/blob/main/packages/storage/src/storage.ts) (line 44) constructs its layer through a generator:

```typescript
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`](https://github.com/magnitudedev/magnitude/blob/main/packages/storage/src/state/default-initialization.test.ts):

```typescript
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):

```typescript
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`](https://github.com/magnitudedev/magnitude/blob/main/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:

```typescript
// 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.