# How the @Inject Decorator Works in Cordis for Dependency Injection

> Understand how the @Inject decorator works in Cordis for dependency injection. Learn about its class and method level functionalities for seamless service resolution and initialization.

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

---

**The `@Inject` decorator in Cordis operates in two distinct modes: at the class level, it defines static dependency requirements that the framework resolves during plugin instantiation; at the method level, it registers initialization hooks that execute the method once specific services become available in the context.**

The `@Inject` decorator is the primary mechanism for declaring dependency injection requirements in the Cordis plugin framework. Implemented in [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts) within the `cordiverse/cordis` repository, it enables both class-level service injection and method-level initialization hooks that resolve automatically when plugins load through the Fiber system.

## Class-Level vs. Method-Level Injection Patterns

The decorator adapts its behavior based on whether it is applied to a class or a method, utilizing JavaScript decorator metadata to store injection requirements.

### Class-Level Injection (Static Property Declaration)

When applied to a class, `@Inject` adds a static `inject` property to the class (or its prototype chain) that records the requested dependency name with optional configuration. This static map is later resolved by `Inject.resolve` when the class is instantiated as a plugin.

The decorator ensures inheritance by using `Object.create(Object.getPrototypeOf(value).inject ?? null)`, allowing subclasses to inherit injection requirements from their base classes while defining their own.

### Method-Level Injection (Runtime Initialization Hooks)

When applied to a method, the decorator stores injection metadata in the method's `symbols.metadata.inject` object and registers an initialization hook via `symbols.initHooks`. This hook executes after the plugin's context is created, calling `ctx.inject(inject, callback)` to create a temporary Fiber that executes the method with resolved services injected as arguments.

## Core Implementation in registry.ts

The decorator implementation in [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts) handles both usage patterns through a single factory function that inspects the `decorator.kind` property:

```typescript
// https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts#L17-L39
export function Inject<K extends InjectKey>(name: K, config?: …) {
  return function (value, decorator) {
    if (decorator.kind === 'class') {
      // ensure the class has an `inject` map (inherits prototype chain)
      defineProperty(value, 'inject', Object.create(Object.getPrototypeOf(value).inject ?? null));
      value.inject[name] = config;
    } else if (decorator.kind === 'method') {
      const inject = (value[symbols.metadata] ??= {}).inject ??= Object.create(null);
      inject[name] = config;
      decorator.addInitializer(function () {
        const property = this[symbols.tracker]?.property;
        (this[symbols.initHooks] ??= []).push(() => {
          (this.ctx as Context).inject(inject, (ctx) => {
            return value.call(property ? withProps(this, { [property]: ctx }) : this);
          });
        });
      });
    } else {
      throw new Error('@Inject() can only be used on class or class methods');
    }
  };
}

```

The implementation distinguishes between classes and methods through the `decorator.kind` check. For classes, it manipulates the constructor's static properties. For methods, it leverages `decorator.addInitializer` to defer execution until the plugin context is fully established.

## Dependency Resolution and Prototype Chain

The `Inject.resolve` static method (located in the same file) handles the merging of injection requirements across class hierarchies. It walks the prototype chain and consolidates static `inject` objects, supporting both array-style and object-style specifications:

```typescript
// https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts#L42-L60
export namespace Inject {
  export function resolve(inject, result = Object.create(null)) {
    if (!inject) return result;
    if (Array.isArray(inject)) { … }
    else if (Reflect.has(inject, symbols.checkProto)) { … }
    else { … }
    return result;
  }
}

```

This resolution ensures that subclasses inherit all dependency requirements from their parent classes while allowing overrides and extensions.

## Context Integration and Fiber Wiring

The `Context` surface exposes convenience methods that delegate to the registry for actualplugin loading. The `RegistryService.inject` method constructs the bridge between the decorator metadata and the Fiber execution system:

```typescript
// https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts#L89-L92
export class RegistryService {
  …
  inject(inject: Inject, callback: Plugin.Function<void>) {
    return this.plugin({ inject, apply: callback, name: callback.name });
  }
}

```

When a plugin is loaded, the `RegistryService.plugin` method constructs a `Fiber` with the resolved injection map (`Inject.resolve(plugin.inject)`). The Fiber then makes the requested services available as part of the plugin's execution context, ensuring dependencies are present before the plugin logic executes.

## Practical Code Examples

### Class-Level Injection

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

@Service('timer')
class TimerService { /* … */ }

@Inject('timer')
class MyPlugin {
  static inject = {};   // automatically filled by the decorator
  constructor(public readonly ctx: Context) {}

  async start() {
    // The timer service is available via `ctx.timer`
    this.ctx.timer.start();
  }
}

// Register the plugin
ctx.plugin(MyPlugin);

```

The decorator adds `MyPlugin.inject = { timer: undefined }`. When `MyPlugin` is instantiated, Cordis creates a Fiber that resolves `ctx.timer` and injects it into the plugin's context.

### Method-Level Injection

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

@Service('logger')
class LoggerService {
  log(msg: string) { console.log(msg); }
}

class LoggerPlugin {
  constructor(public readonly ctx: Context) {}

  @Inject('logger')
  init(logger: LoggerService) {
    logger.log('LoggerPlugin initialised');
  }
}

// Register the plugin
ctx.plugin(LoggerPlugin);

```

The method decorator records that `init` needs the `logger` service. When the plugin's Fiber is ready, Cordis calls `ctx.inject` which executes `init` with the resolved `LoggerService` instance passed as an argument.

### Array-Style Injection

```typescript
@Inject(['config', 'database'])
class ConfiguredPlugin {
  // `ctx.config` and `ctx.database` are injected automatically
}

```

Array syntax tells Cordis to inject the listed services with default values if they are not present in the context.

## Summary

- **Dual-mode operation**: The `@Inject` decorator handles both class-level dependency declarations (via static `inject` properties) and method-level initialization hooks (via `symbols.initHooks`).
- **Prototype chain awareness**: `Inject.resolve` in [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts) merges injection requirements across class hierarchies, ensuring proper inheritance of dependencies.
- **Fiber integration**: The decorator ultimately wires into Cordis's Fiber system through `RegistryService.plugin`, creating execution contexts where requested services are guaranteed to be available.
- **Source locations**: Core logic resides in [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts), with resolution invoked during plugin loading in [`packages/loader/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/index.ts).

## Frequently Asked Questions

### Can @Inject be used on properties or accessors?

No. According to the source implementation in [`packages/core/src/registry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/registry.ts), the `@Inject` decorator explicitly throws an error if applied to anything other than a class or method: `throw new Error('@Inject() can only be used on class or class methods')`.

### How does Cordis handle missing dependencies when using @Inject?

For class-level injection, the Fiber system ensures all requested services are available before the plugin constructor runs, throwing resolution errors if services are missing. For method-level injection, the initialization hook waits until the dependency appears in the context, optionally using default values when specified in the decorator configuration.

### Do subclasses inherit injection requirements from parent classes?

Yes. The `Inject.resolve` function specifically walks the prototype chain using `Object.create(Object.getPrototypeOf(value).inject ?? null)`, allowing subclasses to inherit and extend the injection maps defined by their parent classes without explicit re-declaration.

### What is the difference between @Inject on a class versus on a method?

Class-level `@Inject` declares that the entire plugin requires certain services to be present in its context, resolved when the Fiber is created. Method-level `@Inject` registers a callback that executes only after the specific requested services become available, allowing for lazy initialization or conditional setup logic within the plugin lifecycle.