A plugin system without distinct lifecycle stages cannot release resources cleanly. When plugins open database connections, start intervals, or attach event listeners but have no deactivation or destruction hook to call, those resources accumulate for the process lifetime. Disabling a plugin through the UI becomes theater — the plugin keeps running. Over time this produces connection pool exhaustion, timer accumulation, and memory growth that lead to host instability. ISO 25010 maintainability.modularity requires components to be independently removable; lifecycle hooks are the mechanism that makes removal safe.
Critical because missing deactivation and cleanup hooks make plugin removal impossible at runtime, turning every installed plugin into a permanent resource leak that can exhaust connections and crash the host.
Add at minimum three lifecycle stages to the plugin interface in your host's type definitions. The host must invoke each method at the right moment and handle errors from each independently so one plugin's broken teardown does not block others from cleaning up.
interface Plugin {
/** Called once when plugin is first loaded. Set up state. */
init(context: PluginContext): Promise<void>;
/** Called when plugin becomes active. Start listening/processing. */
activate(): Promise<void>;
/** Called when plugin is disabled. Stop processing, keep state. */
deactivate(): Promise<void>;
/** Called when plugin is removed. Release all resources. */
destroy(): Promise<void>;
}
ID: plugin-extension-architecture.hook-system.lifecycle-hooks
Severity: critical
What to look for: Examine the plugin system's interface or base class for lifecycle methods. A complete lifecycle includes at minimum: initialization (called when plugin is first loaded), activation (called when plugin is enabled and ready to run), deactivation (called when plugin is being disabled but not removed), and destruction (called when plugin is fully removed and should release all resources). Look for method names like init/setup/onLoad, activate/start/enable, deactivate/stop/disable, destroy/teardown/dispose/cleanup. Real-world examples:
activate(context) and deactivate() in extension entry pointfastify.addHook('onReady'), fastify.addHook('onClose')environment, afterEnvironment, entryOption, afterPluginsregister_activation_hook(), register_deactivation_hook()
Check that the lifecycle is documented or typed, and that the host actually invokes these methods at the appropriate times (not just defined but never called).Pass criteria: Count all lifecycle hook types defined in the plugin API. The plugin system defines at least 3 distinct lifecycle stages (e.g., init + activate + deactivate, or setup + start + teardown) AND the host code invokes them at appropriate times. The stages must be documented or typed so plugin authors know when each runs. Report the count of lifecycle hook types even on pass.
Fail criteria: The plugin system has no lifecycle concept (plugins are loaded and that's it — no cleanup, no shutdown sequence), OR lifecycle methods are defined in an interface but never invoked by the host, OR the only lifecycle is "load" with no corresponding "unload."
Skip (N/A) when: The plugin system is purely declarative (e.g., config-only plugins that declare data but run no code, like ESLint rule severity overrides without custom rule implementations).
Detail on fail: Describe which lifecycle stages are missing. Example: "Plugin interface defines init() and activate() but no deactivation or cleanup hook. Plugins that open database connections, start intervals, or attach event listeners have no way to release resources when disabled or removed."
Remediation: A plugin lifecycle ensures orderly startup and shutdown. Without it, disabling or removing a plugin leaves orphaned resources — open connections, running timers, attached listeners — that accumulate until the host crashes or behaves erratically.
// Minimum viable plugin lifecycle interface:
interface Plugin {
/** Called once when plugin is first loaded. Set up state. */
init(context: PluginContext): Promise<void>;
/** Called when plugin becomes active. Start listening/processing. */
activate(): Promise<void>;
/** Called when plugin is disabled. Stop processing, keep state. */
deactivate(): Promise<void>;
/** Called when plugin is removed. Release all resources. */
destroy(): Promise<void>;
}
The host must invoke each method at the right time and handle errors from each independently.
Cross-reference: For hook type safety, see the type-safety check in the Type Safety & API Contract category.