# How Cordis Resolves Configuration for Services: A Deep Dive into the Prototype-Based Strategy

> Discover how Cordis resolves service configuration using its prototype chain traversal and Service.resolveConfig method. Learn about fragment collection and merge hooks.

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

---

**Cordis resolves service configuration by traversing the `Context.intercept` prototype chain, collecting configuration fragments, and merging them via the `Service.resolveConfig` method with optional service-specific `Config.merge` hooks.**

The **Cordis** framework (maintained in the `cordiverse/cordis` repository) implements a deterministic, prototype-based strategy to build final configuration objects for every service. When a plugin requests a service like the logger or timer, Cordis aggregates configuration contributions from across the plugin tree. This process centers on the `Service.resolveConfig` static symbol defined in [`packages/core/src/service.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/service.ts) and the intercept object stored on the Context.

## The Configuration Resolution Pipeline

Cordis constructs service configuration through a six-step deterministic pipeline. Each step leverages specific source files in the core and loader packages.

### Step 1: Creating the Intercept Map

When a context is instantiated, Cordis creates an **intercept object** that holds configuration fragments contributed by plugins. This object forms a prototype chain so that later plugins can shadow earlier ones without mutating the original fragments.

The intercept mechanism is defined in [`packages/core/src/utils.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/utils.ts), where `Context.intercept` is initialized. This prototype chain is crucial for hierarchical configuration inheritance.

### Step 2: Invoking Service.resolveConfig

Every service class inherits a static symbol `Service.resolveConfig`. The loader invokes this resolver via `Service.prototype[Service.resolveConfig].call(this)` during service initialization.

This call occurs in [`packages/loader/src/config/entry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/config/entry.ts), where the loader prepares to hydrate a service instance with its final configuration.

### Step 3: Gathering Configuration Fragments

Inside `Service.resolveConfig` (implemented in [`packages/core/src/service.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/service.ts)), the code walks up the prototype chain of the intercept object. It collects any own property named after the service (`intercept[serviceName]`). The collected entries are *unshifted* so that more-generic (higher-level) entries are placed before more-specific ones in the final array.

### Step 4: Integrating Base and Head Configs

Callers can supply two optional configuration injections:

- **Base config**: Default values prepended to the fragment array
- **Head config**: Runtime-only overrides appended to the fragment array

This allows the loader to inject temporary overrides (such as command-line flags) while preserving plugin defaults and service hardcoded defaults.

### Step 5: Merging Strategy

Cordis applies one of two merging strategies based on service definition:

- If the service defines a static `Config` class with a `merge(...configs)` method, that method is invoked to perform deep merging, validation, or transformation.
- Otherwise, Cordis falls back to a shallow merge using `Object.assign({}, ...configs)`.

This architecture gives services full control over how their configuration fragments combine.

### Step 6: Storing the Final Configuration

The merged object is returned and stored under the service’s internal config symbol (`symbols.config`). All subsequent calls to the service read from this merged configuration, ensuring consistent behavior throughout the service lifecycle.

## Key Implementation Details

### Prototype-Based Inheritance in Context.intercept

The **prototype-based inheritance** model lets later plugins override configuration without mutating earlier fragments. Because each plugin context extends the previous intercept object, configuration lookups naturally resolve to the most specific available value while maintaining access to parent values up the chain.

### The Service.resolveConfig Method Signature

The resolution logic lives in [`packages/core/src/service.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/service.ts). The method signature effectively accepts:

1. The service instance (`this`)
2. Optional base configuration
3. Optional head configuration

It returns the fully merged configuration object that has been validated and transformed according to the service's `Config.merge` implementation.

## Practical Implementation Examples

### Defining a Service with Custom Merge Logic

Services can implement deep merging by defining a static `Config` class:

```typescript
// src/services/example.ts
import { Service } from 'cordis'

export class ExampleService extends Service<ExampleConfig> {
  static Config = class {
    static merge(...configs: Partial<ExampleConfig>[]) {
      // Deep-merge example (you could use lodash.merge, etc.)
      const result: ExampleConfig = { enabled: true, options: {} }
      for (const cfg of configs) Object.assign(result, cfg)
      return result
    }
  }

  // Service logic …
}

```

This implementation in [`packages/core/src/service.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/service.ts) checks for the presence of `Config.merge` before falling back to shallow assignment.

### Contributing Configuration via Plugins

Plugins inject configuration fragments through the intercept object:

```typescript
// plugins/my-plugin.ts
export default {
  name: 'my-plugin',
  apply(ctx) {
    // Append config fragment for ExampleService
    ctx.intercept.ExampleService = { enabled: false, options: { foo: 42 } }
  },
}

```

According to [`packages/core/src/utils.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/utils.ts), this assignment extends the prototype chain, allowing the resolution algorithm to discover the fragment during the walk-up phase.

### Runtime Resolution in the Loader

The loader invokes the resolver during plugin registration, as seen in [`packages/loader/src/config/entry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/config/entry.ts):

```typescript
// loader logic (excerpt)
const service = ctx.registry.service('ExampleService')
const finalConfig = Service.prototype[Service.resolveConfig].call(service, undefined, { options: { bar: 7 } })
// finalConfig => { enabled: false, options: { foo: 42, bar: 7 } }

```

**Result:** The final configuration respects the default (`enabled: true`), the plugin’s fragment (`enabled: false, foo: 42`), and the runtime head (`bar: 7`).

### Accessing Merged Configuration

Services access their resolved configuration via the internal symbols export:

```typescript
// Inside ExampleService methods
const cfg = this[symbols.config]   // <-- the merged config object
if (!cfg.enabled) return
// use cfg.options …

```

This pattern ensures that services always work with the fully resolved, merged configuration regardless of how many plugins contributed fragments.

## Summary

- **Cordis resolves configuration** by walking the `Context.intercept` prototype chain defined in [`packages/core/src/utils.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/utils.ts).
- The **six-step pipeline** involves creating the intercept map, invoking `Service.resolveConfig`, gathering fragments, integrating base/head configs, merging via `Config.merge` or `Object.assign`, and storing to `symbols.config`.
- **Prototype-based inheritance** allows later plugins to shadow earlier configuration without mutation.
- Services control their merging behavior through the optional **static `Config.merge` method**.
- The loader in [`packages/loader/src/config/entry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/config/entry.ts) orchestrates resolution during plugin registration.

## Frequently Asked Questions

### How does Cordis handle configuration conflicts between plugins?

Cordis uses the intercept object's prototype chain to resolve conflicts. When multiple plugins define configuration for the same service, the resolution algorithm in [`packages/core/src/service.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/service.ts) walks up the prototype chain and unshifts entries into an array. This places parent (more generic) configurations before child (more specific) ones. The final merge respects this order, with later values typically overwriting earlier ones unless the service implements a custom `Config.merge` method that handles conflicts differently.

### What is the difference between base config and head config in Cordis?

**Base config** provides default values that are prepended to the configuration array, serving as fallback values when plugins don't specify a setting. **Head config** represents runtime-only overrides that are appended to the array, ensuring they take precedence over plugin-defined values. According to the implementation in [`packages/core/src/service.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/service.ts), base configs are added to the front of the fragments array while head configs are added to the end, creating a precedence hierarchy: base < plugin fragments < head.

### Can a service prevent configuration merging and use only the latest fragment?

Yes. A service can implement a static `Config` class with a custom `merge` method that ignores intermediate values. For example, the merge method could return only the last element of the configs array: `static merge(...configs) { return configs[configs.length - 1]; }`. Since `Service.resolveConfig` in [`packages/core/src/service.ts`](https://github.com/cordiverse/cordis/blob/main/packages/core/src/service.ts) invokes this method when present, the service has complete control over the merging strategy, including the ability to reject or filter specific configuration sources.

### Where does the loader trigger configuration resolution for services?

The loader triggers resolution in [`packages/loader/src/config/entry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/config/entry.ts). Specifically, it calls `Service.prototype[Service.resolveConfig].call(service, baseConfig, headConfig)` when preparing to instantiate or update a service. This occurs during the plugin registration phase defined in [`packages/loader/src/index.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/index.ts), where the loader creates the Context, wires the intercept map, and hydrates services with their final merged configurations before the application starts accepting requests.