# How Cordis Handles Plugin Dependencies and Injection Resolution: A Deep Dive into the Fiber Architecture

> Discover how Cordis manages plugin dependencies and injection resolution using its Fiber architecture. Learn about hierarchical proxy handlers and automatic dependency refreshing for efficient service management.

- Repository: [Cordiverse/cordis](https://github.com/cordiverse/cordis)
- Tags: deep-dive
- Published: 2026-09-12

---

**Cordis implements a lightweight dependency-injection container built around Fiber execution contexts, where plugins declare required services via the `inject` field or `@Inject()` decorator, resolve them through a hierarchical proxy handler that climbs the fiber tree, and automatically refresh when dependencies change.**

Cordis is a modular plugin framework maintained by `cordiverse` that manages service lifecycles through a sophisticated dependency injection system. Understanding how Cordis handles plugin dependencies and injection resolution is essential for building reactive applications that adapt automatically when required services are added, removed, or updated.

## Declaring Dependencies with inject and @Inject

Cordis offers two syntaxes for declaring what services a plugin requires: the static `inject` property and the `@Inject()` decorator. Both approaches ultimately populate the plugin's injection map, which is processed when the plugin is registered.

In [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts), the `Inject` class parses these declarations and merges inherited entries into a flat map of service names to optional configuration values.

### Using the inject Field

The simplest approach assigns an array or object to the `inject` property on your plugin definition:

```typescript
// Array syntax for simple dependencies
export const MyPlugin = {
  name: 'my-plugin',
  inject: ['logger'],
  async apply(ctx) {
    ctx.logger.info('Plugin started')
  }
}

// Object syntax with configuration
export const OtherPlugin = {
  name: 'other',
  inject: { 
    myService: {}  // Config object can be supplied here
  },
  async apply(ctx) {
    const service = ctx.myService
  }
}

```

### Using the @Inject Decorator

For class-based plugins, the `@Inject()` decorator offers a method-level or class-level alternative. This decorator adds service names to the plugin's `inject` map before registration:

```typescript
import { Context, Inject } from 'cordis'

@Inject('logger', 'database')
class MyPluginClass {
  async apply(ctx: Context) {
    // Both logger and database are available on ctx
    ctx.logger.info('Connected to database')
  }
}

```

## Resolution and the Fiber Runtime

When a plugin is loaded via `RegistryService.plugin`, Cordis must resolve the injection map into a concrete set of services. The `Inject.resolve` helper walks the plugin's `inject` description, handles inheritance merging, and produces the final service map.

This map is passed to a newly created **Fiber** (implemented in [`packages/core/src/fiber.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/fiber.ts)), which stores the injection list and creates a runtime that tracks which services the plugin expects. Each Fiber maintains its own store of provided services while retaining a reference to its parent, forming a hierarchical tree that mirrors the plugin nesting structure.

## Providing and Accessing Services

Services enter the container through the `ctx.provide()` method, implemented in [`packages/core/src/reflect.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/reflect.ts). When a plugin or core component calls `ctx.provide(name, value)`, the `ReflectService` registers the implementation in two places: the current Fiber's local store and the shared `ReflectService.props` registry.

### Registering Services with ctx.provide

The internal implementation handles symbol isolation and fiber tracking:

```typescript
// Simplified from packages/core/src/reflect.ts
export class ReflectService {
  provide(name: string, value?: any) {
    const key = this.ctx[symbols.isolate][name] ??= Symbol(name)
    const impl = { name, value, fiber: this.ctx.fiber }
    this.store[key] = impl
    this.ctx.fiber.store![name] = impl
    // Trigger updates in dependent fibers
    this.notify([name])
  }
}

```

### Hierarchical Service Resolution

Property accesses on the context are intercepted by a **proxy handler** (`ReflectService.handler`). When a plugin accesses `ctx.logger`, the handler walks the **fiber hierarchy**:

- First, it checks the current fiber's store for an implementation.
- If not found, it climbs parent fibers until it locates a fiber that provides the requested service.
- If the service is missing or the requesting fiber is inactive, Cordis throws a clear error: "cannot get required service … in inactive context".

This hierarchical resolution allows child plugins to override parent services while maintaining access to shared core utilities.

## Automatic Reload on Dependency Changes

Cordis implements reactive dependency management. When `ReflectService.provide` updates a service, the `notify` method walks all fibers that inject the changed name. For each dependent fiber, Cordis triggers a refresh that re-executes the plugin with the new service instance.

This mechanism ensures that plugins always consume current service versions without manual lifecycle management.

## Module Resolution for Plugin Isolation

Beyond runtime service injection, Cordis handles file-level dependencies through the loader system in [`packages/loader/src/resolve.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/resolve.ts). For each plugin root, the system generates a tiny `resolve.mjs` file under `.cordis/` that forwards to `import.meta.resolve`.

The `createResolve` function returns a resolver scoped to the plugin's directory, ensuring that import specifiers resolve relative to the plugin's own context rather than the global node_modules. This prevents version collisions and allows plugins to import local dependencies safely.

## Summary

- **Declaration**: Plugins specify dependencies via `inject` arrays/objects or the `@Inject()` decorator, parsed in [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts).
- **Resolution**: `Inject.resolve` creates a flat service map that the Fiber runtime uses to track expectations.
- **Provision**: Services are registered via `ctx.provide()` in [`packages/core/src/reflect.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/reflect.ts) and stored in both the Fiber store and `ReflectService.props`.
- **Lookup**: A proxy handler climbs the fiber hierarchy to resolve service requests, throwing descriptive errors for missing or inactive dependencies.
- **Reactivity**: The `notify` system triggers automatic plugin refreshes when provided services change.
- **Isolation**: File-level imports are resolved through `createResolve` in [`packages/loader/src/resolve.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/resolve.ts), maintaining plugin-scoped module resolution.

## Frequently Asked Questions

### How does Cordis handle missing or optional dependencies?

If a plugin accesses `ctx.serviceName` and the service is not found in the fiber hierarchy, Cordis throws a runtime error indicating the service is missing or the context is inactive. Optional dependencies should be checked using `ctx.get()` rather than direct property access, or handled with try-catch blocks around the plugin initialization.

### Can services be overridden by child plugins?

Yes. Because the proxy handler checks the current fiber's store before climbing to parents, a child plugin can call `ctx.provide('logger', customLogger)` to override the parent's logger implementation for itself and its descendants, without affecting sibling or parent contexts.

### What triggers a plugin to reload automatically?

When any plugin calls `ctx.provide()` with a service name, the internal `notify` method identifies all fibers that declare that name in their `inject` map. Cordis then triggers a refresh cycle for those fibers, effectively re-running the dependent plugins with the updated service reference available in their context.

### How do I resolve module paths relative to my plugin's location?

Cordis generates a scoped resolver via `createResolve` in [`packages/loader/src/resolve.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/resolve.ts). Each plugin receives a `resolve` function that uses the generated `.cordis/resolve.mjs` to resolve import specifiers relative to the plugin's root directory, ensuring that `import('./local-config.js')` refers to the plugin's own files regardless of where the main application is executed.