# How Cordis Handles Callable Service Instances: A Complete Guide to Service.invoke

> Learn how Cordis handles callable service instances with Service.invoke. Discover how Cordis wraps instances in proxies for context-aware tracing and efficient method forwarding.

- Repository: [Cordiverse/cordis](https://github.com/cordiverse/cordis)
- Tags: how-to-guide
- Published: 2026-08-24

---

**Cordis treats a service as callable when the service class defines the special symbol `Service.invoke`, wrapping the instance in a proxy that forwards function calls to the service's invoke method while preserving context-aware tracing.**

Cordis, the dependency injection framework powering the Cordiverse ecosystem, enables developers to treat service instances as regular functions through a sophisticated proxy mechanism. This capability allows callable service instances to behave like ordinary functions while maintaining full integration with Cordis' context isolation, shadowing, and lifecycle management systems.

## What Makes a Service Callable in Cordis

A service becomes callable based on the presence of a specific symbol rather than inheritance or interface implementation. This design keeps the system extensible without forcing base class complexity.

### The Service.invoke Symbol

The foundation of callable services resides in [`packages/core/src/service.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/service.ts), where `Service.invoke` is defined as a unique symbol. When a service class implements this symbol, it signals to the Cordis runtime that instances should support function-like invocation. The constructor logic explicitly checks for this marker using `self[symbols.invoke]`, determining whether proxy wrapping is necessary.

## How Cordis Creates Callable Service Instances

The instantiation process involves detection, proxy creation, and trap installation to seamlessly blend object-oriented services with functional interfaces.

### Constructor Detection and Proxy Wrapping

During construction in [`packages/core/src/service.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/service.ts) (lines 26-28), the base `Service` class examines the newly created instance. If `self[symbols.invoke]` exists, the constructor replaces `self` with a proxy returned by `createCallable(name, …, tracker)`. This replacement happens before the instance is fully initialized, ensuring all external references obtain the callable proxy rather than the raw service object.

### The createCallable Implementation

The `createCallable` function, implemented in [`packages/core/src/utils.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/utils.ts) (lines 19-26), constructs the actual function object that wraps the service. This function:

1. Calls `createTraceable` to obtain a traceable proxy of the service for context tracking
2. Executes the original function with supplied arguments when invoked
3. Sets the function's name property for debugging purposes

The result is a callable object that maintains the service's prototype chain while behaving like a native function.

### Invocation Handling via Proxy Traps

When the callable proxy is invoked, the `apply` trap defined in [`packages/core/src/utils.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/utils.ts) (lines 215-217) intercepts the call and delegates to `applyTraceable`. If the target possesses `symbols.invoke`, the proxy redirects the invocation to the service's `Service.invoke` implementation rather than executing the service object directly. This redirection preserves the `this` context and ensures proper error-stack composition through Cordis' tracing system.

## Extending Callable Services

Callable services support inheritance and extension through the `Service.extend` method (lines 41-49 in [`packages/core/src/service.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/service.ts)). When extending a callable service, Cordis recreates the callable proxy while preserving the original prototype chain. This mechanism allows derived services to inherit callable behavior while overriding or augmenting the `Service.invoke` implementation.

The extension process ensures that shadowed services and interceptors work correctly across the inheritance hierarchy, maintaining the context-aware tracing that makes Cordis services predictable in complex plugin ecosystems.

## Practical Implementation Example

The following example demonstrates defining and using a callable service with configuration merging and extension capabilities:

```typescript
// packages/core/src/service.ts pattern implementation
class Foo extends Service {
  constructor(ctx: Context, public config: Config) {
    super(ctx, 'foo')
  }

  // The core of the callable behavior
  protected [Service.invoke](init?: Config) {
    const result = { ...this.config }
    let intercept = this.ctx[Context.intercept]
    while (intercept) {
      Object.assign(result, intercept.foo)
      intercept = Object.getPrototypeOf(intercept)
    }
    return Object.assign(result, init)
  }

  // Expose a convenient method that forwards to the callable proxy
  invoke() {
    return this()
  }

  // Create an extension that retains callability
  extend(config?: Config) {
    return this[Service.extend]({ config: { ...this.config, ...config } })
  }
}

// Usage in application code
const ctx = new Context()
await ctx.plugin(Foo, { a: 1 })

// Direct call resolves to Service.invoke
ctx.foo()                     // → { a: 1 }
ctx.foo.extend({ b: 2 })()   // → { a: 1, b: 2 }

```

Callable services integrate seamlessly with dependency injection. Other services can inject and invoke them as functions:

```typescript
class Outer extends Service {
  static inject = ['callable']  // Inject another callable service
  
  call() {
    const callable = this.ctx['callable'] as Callable
    // Both original and extended versions support function syntax
    return [callable(), callable.extend()()]
  }
}

```

## Summary

- **Symbol-based activation**: Services become callable by defining the `Service.invoke` symbol in [`packages/core/src/service.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/service.ts), not through interface implementation.
- **Automatic proxy wrapping**: The constructor detects the symbol and wraps instances using `createCallable` from [`packages/core/src/utils.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/utils.ts).
- **Transparent invocation**: The proxy's `apply` trap (lines 215-217) forwards calls to `Service.invoke` while maintaining tracing and context isolation.
- **First-class extension**: `Service.extend` recreates callable proxies that preserve prototype chains and interceptors.
- **Traceable integration**: Every call automatically respects Cordis' shadowing and error-stack composition mechanisms through the `createTraceable` integration.

## Frequently Asked Questions

### How do I make a service callable in Cordis?

Define the `Service.invoke` symbol as a method in your service class. According to the source code in [`packages/core/src/service.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/service.ts), Cordis automatically detects this symbol during construction and wraps the instance with a callable proxy. No additional configuration or inheritance beyond extending the base `Service` class is required.

### What is the difference between Service.invoke and a regular method?

`Service.invoke` is a unique symbol that transforms the entire service instance into a function. While regular methods require explicit calls like `service.method()`, `Service.invoke` enables syntax like `service()` where the instance itself is callable. The implementation in [`packages/core/src/utils.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/utils.ts) routes these function calls through the proxy's `apply` trap to the invoke implementation.

### How does callable service extension work?

When you call `this[Service.extend]()` (implemented in [`packages/core/src/service.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/service.ts), lines 41-49), Cordis creates a new service instance with merged configuration and recreates the callable proxy wrapper. This ensures that extended services maintain their callable nature while properly inheriting the parent service's prototype and interceptors.

### Where is the callable proxy logic implemented?

The core proxy creation logic resides in [`packages/core/src/utils.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/utils.ts) within the `createCallable` function (lines 19-26). The invocation handling occurs in the same file through the `apply` trap (lines 215-217), which delegates to `applyTraceable` to maintain Cordis' context-tracking capabilities.