PatternsArchitecture Patterns
Plugin Architecture
Build extensible systems that users can customize with plugins
Plugin Architecture
A plugin architecture allows users to extend your application without modifying the core. Used by VS Code, Figma, WordPress, and Stripe.
🎯 The Concept
Core Application (Your code)
↓
Plugin System (Hook points)
↓
Plugins (User code)
Core stays stable, plugins add features📊 Benefits
| Benefit | Description |
|---|---|
| Extensibility | Users add features without core changes |
| Modularity | Features are isolated and optional |
| Maintainability | Core remains simple and stable |
| Community | Users build and share plugins |
| Customization | Each user gets what they need |
🔧 Basic Plugin System
Simple Plugin Interface
interface Plugin {
name: string;
version: string;
init(): void;
destroy?(): void;
}
class PluginManager {
private plugins: Map<string, Plugin> = new Map();
register(plugin: Plugin): void {
if (this.plugins.has(plugin.name)) {
throw new Error(`Plugin "${plugin.name}" already registered`);
}
this.plugins.set(plugin.name, plugin);
plugin.init();
console.log(`Plugin "${plugin.name}" v${plugin.version} loaded`);
}
unregister(name: string): void {
const plugin = this.plugins.get(name);
if (plugin) {
plugin.destroy?.();
this.plugins.delete(name);
console.log(`Plugin "${name}" unloaded`);
}
}
getPlugin(name: string): Plugin | undefined {
return this.plugins.get(name);
}
getAllPlugins(): Plugin[] {
return Array.from(this.plugins.values());
}
}
// Usage
const manager = new PluginManager();
const themePlugin: Plugin = {
name: 'dark-theme',
version: '1.0.0',
init() {
document.body.classList.add('dark-theme');
},
destroy() {
document.body.classList.remove('dark-theme');
}
};
manager.register(themePlugin);🪝 Hook System (VS Code Style)
Event-Based Hooks
type HookCallback = (...args: unknown[]) => void;
class HookSystem {
private hooks = new Map<string, HookCallback[]>();
// Register hook
registerHook(name: string): void {
if (!this.hooks.has(name)) {
this.hooks.set(name, []);
}
}
// Add listener to hook
on(name: string, callback: HookCallback): () => void {
if (!this.hooks.has(name)) {
this.registerHook(name);
}
this.hooks.get(name)!.push(callback);
// Return unsubscribe function
return () => {
const callbacks = this.hooks.get(name);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
};
}
// Trigger hook
emit(name: string, ...args: unknown[]): void {
const callbacks = this.hooks.get(name);
if (callbacks) {
callbacks.forEach(callback => callback(...args));
}
}
// Get all hooks
getHooks(): string[] {
return Array.from(this.hooks.keys());
}
}
// Core application
class Application {
private hooks = new HookSystem();
constructor() {
// Register available hooks
this.hooks.registerHook('app:init');
this.hooks.registerHook('app:ready');
this.hooks.registerHook('file:open');
this.hooks.registerHook('file:save');
}
init(): void {
this.hooks.emit('app:init');
// ... initialization logic
this.hooks.emit('app:ready');
}
openFile(path: string): void {
this.hooks.emit('file:open', path);
// ... file opening logic
}
saveFile(path: string, content: string): void {
this.hooks.emit('file:save', path, content);
// ... file saving logic
}
// Expose hook system to plugins
getHooks(): HookSystem {
return this.hooks;
}
}
// Plugin using hooks
class AutoSavePlugin implements Plugin {
name = 'auto-save';
version = '1.0.0';
private unsubscribe?: () => void;
init(app: Application): void {
const hooks = app.getHooks();
// Listen to file changes
this.unsubscribe = hooks.on('file:save', (path, content) => {
console.log(`Auto-save: Saving ${path}`);
localStorage.setItem(path, content);
});
}
destroy(): void {
this.unsubscribe?.();
}
}🔌 Advanced: Middleware Pattern (Redux/Stripe Style)
type Middleware<T = unknown> = (
context: T,
next: () => Promise<void>
) => Promise<void>;
class MiddlewareChain<T = unknown> {
private middlewares: Middleware<T>[] = [];
use(middleware: Middleware<T>): void {
this.middlewares.push(middleware);
}
async execute(context: T): Promise<void> {
let index = 0;
const next = async (): Promise<void> => {
if (index >= this.middlewares.length) {
return;
}
const middleware = this.middlewares[index++];
await middleware(context, next);
};
await next();
}
}
// Usage in payment system
interface PaymentContext {
amount: number;
currency: string;
customerId: string;
metadata?: Record<string, unknown>;
}
const paymentPipeline = new MiddlewareChain<PaymentContext>();
// Plugin 1: Logging
paymentPipeline.use(async (context, next) => {
console.log(`Processing payment: $${context.amount}`);
await next();
console.log('Payment processed');
});
// Plugin 2: Fraud detection
paymentPipeline.use(async (context, next) => {
if (context.amount > 10000) {
console.log('Flagging for fraud review');
context.metadata = { ...context.metadata, flagged: true };
}
await next();
});
// Plugin 3: Currency conversion
paymentPipeline.use(async (context, next) => {
if (context.currency !== 'USD') {
console.log(`Converting ${context.currency} to USD`);
context.amount = convertToUSD(context.amount, context.currency);
context.currency = 'USD';
}
await next();
});
// Execute pipeline
await paymentPipeline.execute({
amount: 15000,
currency: 'EUR',
customerId: '123'
});🎨 Plugin API Design
Well-Designed Plugin API
interface PluginAPI {
// App info
version: string;
// UI extensions
registerCommand(command: Command): void;
registerMenuItem(item: MenuItem): void;
registerPanel(panel: Panel): void;
// Data access
getState(): AppState;
setState(state: Partial<AppState>): void;
// Events
on(event: string, callback: Function): () => void;
emit(event: string, data?: unknown): void;
// Storage
storage: {
get<T>(key: string): T | null;
set<T>(key: string, value: T): void;
remove(key: string): void;
};
// Notifications
showNotification(message: string, type: 'info' | 'error' | 'success'): void;
}
// Plugin using the API
class TodoPlugin implements Plugin {
name = 'todo-list';
version = '1.0.0';
init(api: PluginAPI): void {
// Register command
api.registerCommand({
id: 'todo.add',
name: 'Add Todo',
execute: () => {
const todo = prompt('Enter todo:');
if (todo) {
this.addTodo(api, todo);
}
}
});
// Register menu item
api.registerMenuItem({
id: 'todo-menu',
label: 'Todos',
location: 'sidebar',
onClick: () => this.showTodoPanel(api)
});
// Listen to events
api.on('app:ready', () => {
this.loadTodos(api);
});
}
private addTodo(api: PluginAPI, text: string): void {
const todos = api.storage.get('todos') || [];
todos.push({ id: Date.now(), text, done: false });
api.storage.set('todos', todos);
api.showNotification('Todo added', 'success');
api.emit('todos:changed', todos);
}
private showTodoPanel(api: PluginAPI): void {
const todos = api.storage.get('todos') || [];
api.registerPanel({
id: 'todos',
title: 'Todos',
content: this.renderTodos(todos)
});
}
private renderTodos(todos: Array<{ id: string; text: string; done: boolean }>): string {
return todos.map(todo => `
<div>
<input type="checkbox" ${todo.done ? 'checked' : ''} />
<span>${todo.text}</span>
</div>
`).join('');
}
private loadTodos(api: PluginAPI): void {
const todos = api.storage.get('todos') || [];
console.log(`Loaded ${todos.length} todos`);
}
}🔐 Sandboxed Plugins (Security)
// Safe plugin execution
class SandboxedPluginManager {
private iframe?: HTMLIFrameElement;
async loadPlugin(url: string): Promise<void> {
// Create isolated iframe
this.iframe = document.createElement('iframe');
this.iframe.sandbox.add('allow-scripts');
this.iframe.style.display = 'none';
document.body.appendChild(this.iframe);
// Load plugin code
const response = await fetch(url);
const code = await response.text();
// Execute in sandbox
const script = this.iframe.contentDocument!.createElement('script');
script.textContent = code;
this.iframe.contentDocument!.body.appendChild(script);
}
// Message passing for communication
sendMessage(message: unknown): void {
this.iframe?.contentWindow?.postMessage(message, '*');
}
onMessage(callback: (message: unknown) => void): void {
window.addEventListener('message', (event) => {
if (event.source === this.iframe?.contentWindow) {
callback(event.data);
}
});
}
}🏗️ Real-World Examples
Figma Plugin System
// Figma provides restricted API to plugins
interface FigmaAPI {
// Can access and modify canvas
currentPage: {
selection: Node[];
findAll(filter?: (node: Node) => boolean): Node[];
};
// Can create UI
showUI(html: string, options?: UIOptions): void;
// Sandboxed communication
ui: {
postMessage(message: unknown): void;
onmessage: (message: unknown) => void;
};
// No network access (security)
// No file system access
// No external APIs
}
// Plugin code
figma.showUI(__html__);
figma.ui.onmessage = (msg) => {
if (msg.type === 'create-rectangle') {
const rect = figma.createRectangle();
rect.x = msg.x;
rect.y = msg.y;
figma.currentPage.appendChild(rect);
}
};VS Code Extension API
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
// Register command
const disposable = vscode.commands.registerCommand(
'extension.helloWorld',
() => {
vscode.window.showInformationMessage('Hello World!');
}
);
context.subscriptions.push(disposable);
// Register code action provider
vscode.languages.registerCodeActionsProvider('typescript', {
provideCodeActions() {
return [/* code actions */];
}
});
}Stripe Plugin System
// Stripe middleware for payment processing
stripe.use((req, next) => {
console.log('Payment request:', req);
return next();
});
stripe.use(fraudDetectionPlugin);
stripe.use(taxCalculationPlugin);
stripe.use(receiptEmailPlugin);📚 Key Takeaways
- Define clear API - What plugins can and can't do
- Use hooks/events - Let plugins react to app events
- Middleware pattern - Chain plugins in order
- Sandbox for security - Limit plugin capabilities
- Version your API - Breaking changes = new version
- Document everything - Plugin developers need docs
- Test plugins - Provide testing utilities
When to use: Building platforms (VS Code, Figma), frameworks (Gatsby, Next.js), or products that need extensibility.