界面已本地化;技能介绍与说明文档按所选语言由 AI 自动翻译,代码、命令、路径和专业术语保留原文,以原作者文档为准。
探索技能
设计与创意 · SKILL.md

backend-plugin-architecture

backend-plugin-architecture

【设计与创意】Use this skill when the user says 'plugin system', 'plugin architecture', 'extension point', 'SPI', 'service provider interface', 'hot-plug', 'modular design', 'plugin loader', 'extensible', 'plugin API', 'custom extension', 'plug-in', 'dynamic loading'. This skill designs plugin/extension architecture with SPI, hot-plugging, and modular loading. App…

独立 GitHub 快照 · 自动分类 · 以原作者文档为准

技能说明

GitHub

name: backend-plugin-architecture description: > Use this skill when the user says 'plugin system', 'plugin architecture', 'extension point', 'SPI', 'service provider interface', 'hot-plug', 'modular design', 'plugin loader', 'extensible', 'plugin API', 'custom extension', 'plug-in', 'dynamic loading'. This skill designs plugin/extension architecture with SPI, hot-plugging, and modular loading. Applies to any backend stack. Do NOT use for: dependency injection frameworks, micro-frontends, or NPM packages. version: "2.0.0" author: "j4flmao" license: "MIT" compatibility: claude-code: true cursor: true codex: true windsurf: true tags: [backend, universal, plugin-architecture, extensibility, modular, spi]

Backend Plugin Architecture

Purpose

Design extensible backend systems where third-party code (plugins) can be loaded dynamically at runtime through well-defined extension points and service provider interfaces (SPI). A plugin architecture enables the host application to be extended without modifying its core code.

Agent Protocol

Trigger

Exact user phrases: "plugin system", "plugin architecture", "extension point", "SPI", "service provider interface", "hot-plug", "modular design", "plugin loader", "extensible", "plugin API", "custom extension", "plug-in", "dynamic loading".

Input Context

  • What the host application does and where extensibility is needed.
  • Language/runtime (supports dynamic loading?).
  • Security requirements for plugin isolation.

Output Artifact

Plugin system design or implementation code. No file unless requested.

Response Format

Extension Point: {name}
SPI: {interface/contract}
Loading: {dynamic|compile-time|classpath}
Isolation: {classloader|process|container}

Completion Criteria

  • Extension points defined and documented.
  • SPI interface created for each extension point.
  • Plugin loader implemented.
  • Plugin lifecycle (load, init, start, stop, destroy) managed.
  • Isolation and security boundaries established.

Max Response Length

3 lines per extension point. 20 lines for full system.

Architecture Decision Tree

Should I Build a Plugin System?

Do you need to support third-party extensions?
  ├── Yes → Plugin architecture
  └── No → Can the functionality be handled by configuration?
            ├── Yes → Use configuration, not plugins
            └── No → Is the extension point stable (won't change monthly)?
                      ├── Yes → Plugin architecture is appropriate
                      └── No → Plugin API will need constant breaking changes — reconsider

Plugin Loading Strategy

Can plugins be loaded at runtime without restart?
  ├── Yes → Dynamic loading (hot-plug)
  │   ├── Language supports dynamic loading (JS, Python, Java, .NET, Go plugins)
  │   ├── Plugin discovery via file system scanning or registry
  │   └── Risk: Version conflicts, dependency hell
  └── No → Compile-time loading
      ├── Simpler, safer, no runtime isolation concerns
      ├── Requires recompile/redeploy for new plugins
      └── Use when: Plugin churn is low, safety is critical

Isolation Level

Who writes the plugins?
  ├── Internal team (trusted code) → Same process, classloader isolation
  │   ├── Simple, fast, no IPC overhead
  │   └── Risk: Memory corruption, crash propagation
  ├── External contributors (semi-trusted) → Process isolation
  │   ├── Child process or subprocess
  │   ├── IPC for communication
  │   └── Risk: Performance overhead
  └── Third-party (untrusted) → Container or sandbox isolation
      ├── Docker, WASM, or sandboxed environment
      ├── Resource limits enforced at OS level
      └── Highest security, highest overhead

Workflow

Step 1: Define Extension Points

Identify where the system can be extended:

extension-points:
  authentication.provider:  Custom auth provider (LDAP, OAuth, SAML)
  notification.channel:     Custom notification channel (SMS, push, Slack)
  pipeline.step:            Custom build/deploy pipeline step
  storage.backend:          Custom storage backend (S3, GCS, local)
  exporter.metric:          Custom metric exporter (Prometheus, Datadog)
  payment.gateway:          Custom payment gateway (Stripe, PayPal, Braintree)

Step 2: Define SPI Interface

// Java — AuthenticationProvider SPI
public interface AuthenticationProvider {
  String getName();
  AuthResult authenticate(Credentials credentials);
  boolean supports(String providerType);
  void init(ProviderConfig config);
  void destroy();
}
// TypeScript — NotificationChannel SPI
export interface NotificationChannel {
  readonly name: string;
  readonly type: string;
  send(notification: Notification): Promise<DeliveryResult>;
  validate(config: ChannelConfig): Promise<ValidationResult>;
}

Step 3: Implement Plugin Loader

// Node.js — dynamic plugin loading
class PluginLoader {
  constructor(pluginDir) {
    this.pluginDir = pluginDir;
    this.plugins = new Map();
    this.hooks = {
      beforeLoad: [],
      afterLoad: [],
      beforeUnload: [],
    };
  }

  async loadAll() {
    const entries = await fs.readdir(this.pluginDir);
    const results = [];

    for (const entry of entries) {
      if (entry.endsWith('.plugin.js') || entry.endsWith('.plugin.mjs')) {
        results.push(this.loadPlugin(entry));
      }
    }
    return Promise.all(results);
  }

  async loadPlugin(entry) {
    const pluginPath = path.join(this.pluginDir, entry);
    try {
      await this.runHook('beforeLoad', { entry });

      const plugin = await import(pluginPath);
      const manifest = plugin.manifest || {};

      // Validate plugin manifest
      this.validateManifest(manifest);

      // Check API compatibility
      if (manifest.apiVersion && !this.isCompatible(manifest.apiVersion)) {
        throw new Error(`Plugin ${manifest.name} requires API v${manifest.apiVersion}, host has v${this.hostApiVersion}`);
      }

      // Initialize plugin
      if (plugin.init) {
        await plugin.init({ config: this.config, logger: this.logger });
      }

      this.plugins.set(manifest.name, plugin);
      logger.info(`Plugin loaded: ${manifest.name} v${manifest.version}`);

      await this.runHook('afterLoad', { entry, name: manifest.name });
      return { name: manifest.name, version: manifest.version, success: true };
    } catch (error) {
      logger.error(`Failed to load plugin: ${entry}`, { error });
      return { entry, success: false, error: error.message };
    }
  }

  getPlugins(type) {
    return [...this.plugins.values()].filter(p => p.type === type);
  }

  async unload(name) {
    await this.runHook('beforeUnload', { name });
    const plugin = this.plugins.get(name);
    if (plugin?.destroy) {
      await plugin.destroy();
    }
    this.plugins.delete(name);
    // Clear require cache for reload
    delete require.cache[require.resolve(path.join(this.pluginDir, name))];
    logger.info(`Plugin unloaded: ${name}`);
  }

  async reload(name) {
    await this.unload(name);
    return this.loadPlugin(name);
  }
}

Step 4: Manage Plugin Lifecycle

                 ┌──────────┐
                 │ DISCOVERED│
                 └────┬─────┘
                      │ validate manifest
                 ┌────▼─────┐
                 │ VALIDATED │
                 └────┬─────┘
                      │ load code
                 ┌────▼─────┐
                 │  LOADED  │
                 └────┬─────┘
                      │ init(config)
                 ┌────▼─────┐
                 │INITIALIZED│
                 └────┬─────┘
                      │ start()
                 ┌────▼─────┐
                 │ STARTED  │──── active
                 └────┬─────┘
                      │ stop()
                 ┌────▼─────┐
                 │ STOPPED  │
                 └────┬─────┘
                      │ destroy()
                 ┌────▼─────┐
                 │DESTROYED │
                 └──────────┘

Step 5: Isolate Plugins

// Process isolation (Node.js child_process)
const { fork } = require('child_process');

class IsolatedPlugin {
  constructor(pluginPath) {
    this.child = fork(pluginPath, [], {
      execArgv: ['--experimental-vm-modules'],
      env: { ...process.env, PLUGIN_ISOLATED: 'true' },
    });
    this.requestId = 0;
    this.pending = new Map();
  }

  async call(method, args) {
    const id = ++this.requestId;
    this.child.send({ type: 'call', id, method, args });

    return new Promise((resolve, reject) => {
      this.pending.set(id, { resolve, reject });
      // Timeout
      setTimeout(() => {
        if (this.pending.has(id)) {
          this.pending.delete(id);
          reject(new Error('Plugin call timed out'));
        }
      }, 30000);
    });
  }

  handleMessage(msg) {
    const pending = this.pending.get(msg.id);
    if (pending) {
      this.pending.delete(msg.id);
      if (msg.error) pending.reject(new Error(msg.error));
      else pending.resolve(msg.result);
    }
  }

  kill() {
    this.child.kill();
  }
}

Step 6: Version Plugin APIs

{
  "name": "my-plugin",
  "apiVersion": "1.0.0",
  "pluginVersion": "2.3.0",
  "entry": "./index.js",
  "description": "Custom payment gateway for region-specific processing",
  "author": "Acme Corp",
  "license": "MIT",
  "dependencies": {
    "host-api": "^1.0.0"
  },
  "permissions": ["network:outbound", "storage:read"]
}

Implementation Patterns

Plugin Registry Pattern

interface PluginManifest {
  name: string;
  version: string;
  apiVersion: string;
  description: string;
  author: string;
  permissions: string[];
}

class PluginRegistry {
  private plugins = new Map<string, PluginManifest>();
  private eventBus = new EventEmitter();

  register(plugin: PluginManifest): void {
    this.plugins.set(plugin.name, plugin);
    this.eventBus.emit('plugin:registered', plugin);
  }

  unregister(name: string): void {
    this.plugins.delete(name);
    this.eventBus.emit('plugin:unregistered', name);
  }

  getManifest(name: string): PluginManifest | undefined {
    return this.plugins.get(name);
  }

  listPlugins(): PluginManifest[] {
    return [...this.plugins.values()];
  }

  onEvent(event: string, handler: (data: any) => void): void {
    this.eventBus.on(event, handler);
  }
}

Extension Point Registry

class ExtensionPoint<T> {
  private implementations: Map<string, T> = new Map();

  constructor(public readonly name: string) {}

  register(id: string, implementation: T): void {
    if (this.implementations.has(id)) {
      throw new Error(`Extension point ${this.name}: ${id} already registered`);
    }
    this.implementations.set(id, implementation);
  }

  unregister(id: string): void {
    this.implementations.delete(id);
  }

  get(id: string): T | undefined {
    return this.implementations.get(id);
  }

  getAll(): Map<string, T> {
    return new Map(this.implementations);
  }

  get count(): number {
    return this.implementations.size;
  }
}

// Usage
const authProviders = new ExtensionPoint<AuthenticationProvider>('auth.providers');
authProviders.register('oauth-google', new GoogleOAuthProvider());
authProviders.register('ldap', new LDAPProvider());

Plugin Configuration Pattern

class PluginConfigManager {
  private configs = new Map<string, object>();

  async loadConfig(pluginName: string): Promise<object> {
    if (this.configs.has(pluginName)) return this.configs.get(pluginName)!;

    try {
      const configPath = path.join(CONFIG_DIR, `${pluginName}.json`);
      const config = JSON.parse(await fs.readFile(configPath, 'utf-8'));
      this.configs.set(pluginName, config);
      return config;
    } catch {
      return {};
    }
  }

  async updateConfig(pluginName: string, config: object): Promise<void> {
    this.configs.set(pluginName, config);
    const configPath = path.join(CONFIG_DIR, `${pluginName}.json`);
    await fs.writeFile(configPath, JSON.stringify(config, null, 2));
  }
}

Production Considerations

Plugin Failure Isolation

Failure TypeSame ProcessSubprocessContainer
CrashKills hostIsolatedIsolated
Memory leakDepletes hostDepletes subprocessContainer limit
Infinite loopBlocks host threadKilled by timeoutOOM killer
File systemFull accessRestrictedRead-only
NetworkFull accessRestrictedFirewall rules

Plugin API Compatibility

Use semantic versioning for the plugin API:

  • Major: Breaking change — plugins must be rewritten
  • Minor: New feature — backward compatible
  • Patch: Bug fix — no plugin changes needed

Resource Limits

class PluginResourceLimiter {
  private limits = new Map<string, {
    maxMemory: number;
    maxCpu: number;
    maxFileDescriptors: number;
    maxNetworkConnections: number;
  }>();

  setLimit(pluginName: string, limit: ResourceLimit): void {
    this.limits.set(pluginName, limit);
  }

  enforce(pluginName: string): void {
    const limit = this.limits.get(pluginName);
    if (!limit) return;

    // Check memory usage
    const usage = process.memoryUsage();
    if (usage.heapUsed > limit.maxMemory) {
      throw new Error(`Plugin ${pluginName} exceeded memory limit`);
    }
  }
}

Anti-Patterns

  1. Leaky SPI abstractions: SPI interfaces that expose host internals create tight coupling. Plugin APIs should be stable, minimal, and abstracted.
  2. No manifest validation: Loading a plugin without validating its manifest can load incompatible code. Always check apiVersion compatibility.
  3. Shared state: Plugins mutating shared global state causes unpredictable behavior. Each plugin should have isolated state.
  4. Ignoring plugin lifecycle: Not managing lifecycle (init, start, stop, destroy) leads to resource leaks and partial initialization.
  5. Plugin dependency hell: Plugins depending on specific versions of shared libraries causes conflicts. Isolate plugin dependencies.
  6. No resource limits: An unconstrained plugin can consume all system resources. Always set memory, CPU, and network limits.
  7. Host API instability: Frequently changing the SPI means plugins break often. Freeze the SPI once released, only add new methods.

Performance

Loading Overhead

StrategyLoad TimeMemory
Same process (JS/Python)10-100ms per pluginModule code in memory
Subprocess (Node fork)50-200ms per plugin~10-30MB per instance
Container (Docker)1-5s per plugin50-200MB per container

Runtime Overhead

  • Same process: <1μs per plugin call (direct function call)
  • Subprocess IPC: ~100μs per call (serialization + IPC)
  • Container IPC: ~1-5ms per call (network/HTTP)

Rules

  • Extension points are frozen once released — only additive changes allowed.
  • Plugins must never access internal APIs — only the public SPI.
  • Sandbox untrusted plugins: restrict filesystem, network, and process access.
  • Always validate plugin integrity (checksum or signature) before loading.
  • Plugin failures must never crash the host application.
  • Log all plugin lifecycle events (load, init, start, stop, destroy).
  • Provide a plugin manifest with name, version, author, and dependencies.
  • Test plugins in isolation before production deployment.
  • Use semantic versioning for plugin API compatibility.

References

  • references/plugin-implementation.md — Plugin Implementation
  • references/plugin-lifecycle.md — Plugin Lifecycle Reference
  • references/plugin-patterns.md — Plugin Patterns
  • references/plugin-security.md — Plugin Security Reference
  • references/plugin-system-design.md — Plugin Architecture Design
  • references/plugin-testing.md — Plugin Testing
  • references/plugin-versioning.md — Plugin API Versioning

Handoff

No artifact produced unless requested. Next skill: observability — add plugin lifecycle traces to the telemetry pipeline. Carry forward: extension points, SPI contracts, plugin manifest format.

我的收藏