# How Group Configuration Enables Hierarchical Plugin Loading in Cordis

> Learn how Cordis uses group configuration for hierarchical plugin loading. Discover nested EntryGroups for isolated contexts and prototype chaining inheritance.

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

---

**Cordis uses a special `group` configuration flag in `EntryOptions` to create nested `EntryGroup` instances that isolate plugins in separate contexts while maintaining parent-child inheritance through prototype chaining.**

Group configuration is the foundation of Cordis's hierarchical plugin system. It allows developers to organize plugins into nested, dynamically reconfigurable trees—each with its own scope, lifecycle, and service namespace. This article explains how `cordiverse/cordis` implements this architecture through its `EntryTree`, `EntryGroup`, and `Entry` classes.

## What Is a Group in Cordis?

A **group** is a container entry that holds an array of plugin configurations (`EntryOptions[]`) and manages a dedicated `Context`. Unlike regular plugin entries, groups don't directly execute code—they orchestrate child plugins.

When `group: true` is set in `EntryOptions`, Cordis instantiates a `Group` subclass instead of a standard `Entry`. This disables the disabled-check cascade and enables nested loading behavior.

```ts
// packages/loader/src/config/entry.ts
export interface EntryOptions {
  id?: string
  name?: string
  group?: boolean | null  // ← triggers group mode
  disabled?: boolean
  // ...
}

```

## The EntryGroup Base Class and Group Implementation

The `EntryGroup` abstract class (located in [`packages/loader/src/config/group.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/config/group.ts)) defines the contract for group behavior. Its concrete implementation, `Group`, handles runtime configuration updates through the `internal/update` event.

```ts
// packages/loader/src/config/group.ts
export abstract class EntryGroup {
  abstract data: EntryOptions[]
  abstract ctx: Context
  // ...
}

export class Group extends EntryGroup {
  data: EntryOptions[]
  ctx: Context
  
  constructor(public parent: Context, public config: EntryOptions) {
    super()
    // Link to the fiber tree for hierarchy tracking
    const tree = parent.fiber.entry!.parent.tree
    // Register update listener for dynamic reconfiguration
    parent.on('internal/update', (config) => this.update(config))
  }
  
  *init(): Generator<() => void, void, unknown> {
    yield () => this.stop()
    await this.update(this.config)
  }
  
  async update(config: EntryOptions) {
    // Iterate data array, create/remove/patch child entries
    // ...
  }
  
  stop() {
    // Cleanup all child entries
    // ...
  }
}

```

The constructor establishes three critical connections:

1. **Tree linkage** — Connects to `parent.fiber.entry!.parent.tree` for global entry tracking
2. **Context creation** — Creates a child `Context` that inherits from the parent
3. **Update subscription** — Listens for `internal/update` to enable runtime reconfiguration

## How the Entry Tree Manages Hierarchy

`EntryTree` (in [`packages/loader/src/config/tree.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/config/tree.ts)) maintains the global registry of all entries and provides group resolution utilities.

```ts
// packages/loader/src/config/tree.ts
export abstract class EntryTree {
  store = new Map<string, Entry>()
  
  resolveGroup(id: string | null): EntryGroup {
    if (id === null) return this.root
    const entry = this.store.get(id)
    if (!entry) throw new Error(`entry ${id} not found`)
    if (!entry.group) throw new Error(`entry ${id} is not a group`)
    return entry.group
  }
  
  create(parentId: string | null, options: EntryOptions): string {
    const group = this.resolveGroup(parentId)
    const id = randomId()
    group.data.push({ ...options, id })
    this.write()
    return id
  }
  
  remove(id: string) {
    // Find and splice from parent's data array
    // ...
    this.write()
  }
  
  update(id: string, options: EntryOptions) {
    // Patch existing entry config
    // ...
    this.write()
  }
  
  abstract write(): void  // Persist configuration
}

```

The `resolveGroup` method is the gateway to hierarchical navigation:

- Returns the **root group** when `id` is `null`
- Validates that the target entry is actually a group
- Enables CRUD operations at any level of the tree

## The Hierarchical Loading Flow

Cordis builds plugin hierarchies through a recursive initialization process:

### 1. Root Group Creation

When `ctx.loader.init(config)` is called, the `EntryTree` constructor creates the root `EntryGroup` as the entry point.

### 2. Group Detection and Instantiation

During `update`, the loader checks each configuration object. If `group: true`, it instantiates a new `Group` rather than creating a leaf `Entry`.

### 3. Context Isolation with Inheritance

Each group creates its own `Context` with prototype-chained inheritance:

```ts
Object.setPrototypeOf(this.ctx, this.parent.ctx)

```

This design provides:

- **Service access** — Child plugins can resolve services from ancestor contexts
- **Namespace isolation** — Each group's services don't collide with siblings
- **Clean disposal** — Removing a group automatically cleans up its entire subtree

### 4. Recursive Descent

The `Group.update()` method iterates over its `data` array, calling `tree.create()` for each child. If a child has `group: true`, the process repeats, building deeper levels.

## Runtime Reconfiguration of Groups

Groups remain mutable after initialization. The `internal/update` event triggers `Group.update()`, which performs **diff-driven reconciliation**:

| Operation | Trigger | Effect |
|-----------|---------|--------|
| **Create** | New entry in `data` array | Instantiate new `Entry` or `Group` |
| **Remove** | Entry missing from `data` | Call `entry.stop()` and cleanup |
| **Patch** | Same `id`, different options | Update fiber config or reinitialize |

This makes hierarchical plugin trees fully dynamic—groups can be restructured without application restart.

```ts
import { Context } from 'cordis'

// Initial configuration with nested group
const config = [
  {
    id: 'api',
    name: '@cordis/api',
    group: true  // ← creates isolated API context
  },
  { id: 'web', name: '@cordis/web' }
]

const ctx = new Context()
await ctx.loader.init(config)

// Dynamically extend the 'api' subgroup with new plugins
ctx.loader.updateGroup('api', [
  { id: 'graphql', name: '@cordis/graphql' },
  { id: 'rest', name: '@cordis/rest', group: true }  // ← nested subgroup
])

```

## Key Files and Responsibilities

- **[`packages/loader/src/config/group.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/config/group.ts)** — Defines `EntryGroup` and `Group`; handles group-level lifecycle and update propagation
- **[`packages/loader/src/config/entry.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/config/entry.ts)** — Implements plugin entries; recognizes `group` flag and delegates to `EntryGroup`
- **[`packages/loader/src/config/tree.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/src/config/tree.ts)** — Manages global entry registry; provides `resolveGroup` and CRUD operations
- **[`packages/loader/tests/group.spec.ts`](https://github.com/cordiverse/cordis/blob/main/packages/loader/tests/group.spec.ts)** — Test coverage for group declaration, nesting, and hot-reloading

## Summary

- **Group configuration** uses `group: true` in `EntryOptions` to trigger `Group` instantiation instead of leaf `Entry`
- **`EntryGroup`** stores `EntryOptions[]` and manages a isolated `Context` with prototype inheritance from parent
- **`EntryTree`** provides hierarchical navigation via `resolveGroup(id)` and persists changes through `write()`
- **Recursive initialization** builds context trees where each group can contain plugins or nested groups
- **Runtime updates** through `internal/update` enable dynamic addition, removal, and reconfiguration of entire subtrees

## Frequently Asked Questions

### How do I declare a plugin group in Cordis configuration?

Add `group: true` to your `EntryOptions` object. This can be done in your initial config array passed to `ctx.loader.init()` or via `ctx.loader.create()` at runtime. The loader will instantiate a `Group` class instead of a standard `Entry`, creating a new context scope for its children.

### Can groups be nested arbitrarily deep?

Yes. Any entry inside a group's `data` array can itself have `group: true`, triggering recursive `Group` instantiation. The `EntryTree` tracks all entries in a flat `store` map while maintaining parent-child relationships through the tree structure, so depth is limited only by memory and practical organization needs.

### How does context inheritance work between parent and child groups?

When a `Group` is created, it calls `Object.setPrototypeOf(this.ctx, this.parent.ctx)` to establish prototype chaining. This means child contexts can access services registered in ancestors via normal property lookup, while services registered in the child shadow parent definitions. Removing a group breaks the chain and triggers cleanup of its entire subtree.

### What happens when a group's configuration is updated at runtime?

The `Group` constructor registers an `internal/update` listener that calls `this.update(config)`. This method diffs the new `data` array against existing entries: new items trigger `tree.create()`, missing items trigger `entry.stop()`, and changed items trigger `entry.update()` or full reinitialization if the plugin `name` changed. The entire process is atomic and persists via `tree.write()`.