Plugin Architecture
Extensible plugin system with Abstract Factory for component families, Adapter for third-party integration, and Facade to shield plugins from host internals. The blueprint behind VS Code extensions and Chrome add-ons.
Key Abstractions
Interface defining the plugin lifecycle: initialize, activate, deactivate
Abstract Factory that creates a family of components: commands, UI panels, event handlers
Adapter that wraps third-party APIs to conform to the Plugin interface
Facade providing a simplified API over host internals: file system, UI, events
Manages plugin lifecycle, discovery, loading, and unloading
Observer that broadcasts host events like file_opened and build_started to plugins
Prototype that clones pre-configured plugin scaffolds for rapid creation
Class Diagram
How It Works
A plugin architecture separates a host application from its extensions through a strict contract boundary. The host defines what plugins can do (the Plugin interface), how they create components (PluginFactory), and what host capabilities they can access (PluginSDK). Plugins never reach into host internals directly.
Abstract Factory ensures each plugin produces a coherent family of components. A linter plugin creates lint commands, lint panels, and lint event handlers through its LinterPluginFactory. You can't accidentally combine a theme panel with a linter command because the factory forces them to come from the same family.
The Adapter pattern handles third-party integration. Legacy tools and external libraries don't implement your Plugin interface. Instead of modifying them, you wrap them in a PluginAdapter that translates their existing API to match the expected lifecycle methods. The host sees just another Plugin. The legacy tool sees no change at all. VS Code integrates hundreds of language servers this way: servers that predate the extension API entirely.
The Facade pattern protects plugins from host complexity. The PluginSDK exposes simplified methods like read_file() and show_notification() instead of giving plugins direct access to the file system implementation or the notification queue. When the host team refactors internal storage from disk to cloud, no plugin breaks because the SDK interface stays the same.
The Observer pattern connects plugins to host events. The PluginEventBus lets plugins subscribe to events like file_saved or build_started without the host knowing which plugins care about which events. When an event fires, every subscribed handler runs.
Prototype accelerates plugin scaffolding. Instead of manually constructing a new plugin configuration from scratch, you clone a pre-configured PluginTemplate and customize the copy. Deep cloning ensures modifications to one clone never leak back to the original template.
Requirements
Functional
- Plugins implement a standard lifecycle: initialize, activate, deactivate
- Each plugin produces a family of components (commands, panels, event handlers) via its factory
- Third-party tools integrate through an adapter without source modification
- Plugins interact with the host exclusively through a simplified SDK facade
- Host events are broadcast to plugins via a publish-subscribe event bus
- New plugin scaffolds can be created by cloning a prototype template
- A registry manages plugin discovery, activation, and orderly shutdown
Non-Functional
- Adding a new plugin requires zero changes to host code
- SDK API stability: host internals can be refactored without breaking existing plugins
- Plugin isolation: a faulty plugin must not crash the host or other plugins
- Lifecycle management: plugins that leak resources on deactivation must be detectable
- Template cloning must produce deep copies so clone mutations never affect the original
Design Decisions
What does Abstract Factory give you over Simple Factory?
A simple factory creates one type of object. But a plugin is not one object. It's a family: a command, a panel, and an event handler that must work together. A linter's panel shows lint results. A linter's command runs lint checks. They share state assumptions. If you created them through separate simple factories, nothing would prevent mixing a theme panel with a linter command. Abstract Factory groups the family behind one factory interface. Call LinterPluginFactory and every component you get back is guaranteed to be from the linter family.
Why not just let plugins call host APIs directly?
If plugins call host._internal_file_manager.read() directly, every internal refactor breaks every plugin. The SDK is a stable boundary. It exposes read_file(path) and hides whether the host stores files on disk, in memory, or in a cloud bucket. The host team can replace the entire storage layer in a sprint. As long as read_file still returns the file content, no plugin notices.
Couldn't we require all tools to implement Plugin directly?
You don't control third-party code. A popular formatter library has its own API. Asking the maintainer to implement your Plugin interface is unrealistic. The Adapter wraps the existing API: it calls legacy_tool.format_code() inside the adapter's activate() method. From the host's perspective, it's just another Plugin. From the legacy tool's perspective, nothing changed. That's how VS Code integrates hundreds of language servers that predate its extension API.
How does plugin isolation work in practice?
Each plugin runs through the SDK, never touching host internals. If a plugin throws an exception during activate(), the registry catches it and marks that plugin as failed without affecting others. In production systems, this extends to process-level isolation (separate worker processes or WebAssembly sandboxes) so a rogue plugin can't corrupt host memory.
Interview Follow-ups
- "How would you handle plugin versioning?" Each plugin declares a
minHostVersionandmaxHostVersionin its manifest. The registry checks compatibility before loading. If the host is version 3.0 and a plugin requires 2.x, the registry rejects it with a clear error instead of loading broken code. - "How would you resolve dependencies between plugins?" Build a dependency graph from plugin manifests. Topological sort determines load order. If Plugin A depends on Plugin B, B loads first. Circular dependencies are detected at registration time and rejected immediately.
- "How would you sandbox plugins for security?" Run each plugin in a restricted environment: a separate process, a WebAssembly module, or a VM with limited system calls. The SDK becomes an RPC boundary. The plugin calls
sdk.read_file(), which serializes across the sandbox boundary. The host decides whether to grant the request based on the plugin's declared permissions. - "How would you support hot-reloading plugins?" The registry calls
deactivate()on the old version, unloads it, loads the new version, callsinitialize()andactivate(). The key requirement is that plugins must be stateless or persist state through the SDK. Any state held only in the plugin's memory is lost on reload, which is by design: it forces clean lifecycle management.
Code Implementation
1 from abc import ABC, abstractmethod
2 import copy
3
4
5 # ── Plugin Interface ─────────────────────────────────────────────
6
7 class Plugin(ABC):
8 """Core plugin lifecycle interface."""
9
10 @abstractmethod
11 def initialize(self, sdk: "PluginSDK") -> None: ...
12
13 @abstractmethod
14 def activate(self) -> None: ...
15
16 @abstractmethod
17 def deactivate(self) -> None: ...
18
19 @abstractmethod
20 def get_name(self) -> str: ...
21
22
23 # ── Abstract Factory: Product Interfaces ─────────────────────────
24
25 class Command(ABC):
26 @abstractmethod
27 def execute(self) -> str: ...
28
29 class Panel(ABC):
30 @abstractmethod
31 def render(self) -> str: ...
32
33 class EventHandler(ABC):
34 @abstractmethod
35 def handle(self, event: str, data: dict) -> None: ...
36
37
38 # ── Abstract Factory Interface ───────────────────────────────────
39
40 class PluginFactory(ABC):
41 """Creates a coherent family of plugin components."""
42
43 @abstractmethod
44 def create_command(self) -> Command: ...
45
46 @abstractmethod
47 def create_panel(self) -> Panel: ...
48
49 @abstractmethod
50 def create_event_handler(self) -> EventHandler: ...
51
52
53 # ── Concrete Factories ───────────────────────────────────────────
54
55 class LintCommand(Command):
56 def execute(self) -> str:
57 return "Running lint checks on current file..."
58
59 class LintPanel(Panel):
60 def render(self) -> str:
61 return "[Linter Panel: 0 errors, 2 warnings]"
62
63 class LintEventHandler(EventHandler):
64 def handle(self, event: str, data: dict) -> None:
65 print(f" [LintHandler] {event}: re-linting {data.get('file', 'unknown')}")
66
67 class LinterPluginFactory(PluginFactory):
68 def create_command(self) -> Command:
69 return LintCommand()
70 def create_panel(self) -> Panel:
71 return LintPanel()
72 def create_event_handler(self) -> EventHandler:
73 return LintEventHandler()
74
75
76 class ThemeCommand(Command):
77 def execute(self) -> str:
78 return "Switching to dark theme..."
79
80 class ThemePanel(Panel):
81 def render(self) -> str:
82 return "[Theme Panel: Dark Mode | accent=#7c3aed]"
83
84 class ThemeEventHandler(EventHandler):
85 def handle(self, event: str, data: dict) -> None:
86 print(f" [ThemeHandler] {event}: refreshing theme colors")
87
88 class ThemePluginFactory(PluginFactory):
89 def create_command(self) -> Command:
90 return ThemeCommand()
91 def create_panel(self) -> Panel:
92 return ThemePanel()
93 def create_event_handler(self) -> EventHandler:
94 return ThemeEventHandler()
95
96
97 # ── Concrete Plugins Using Factories ─────────────────────────────
98
99 class LinterPlugin(Plugin):
100 def __init__(self):
101 self.factory = LinterPluginFactory()
102 self.sdk = None
103 self.active = False
104
105 def initialize(self, sdk: "PluginSDK") -> None:
106 self.sdk = sdk
107 cmd = self.factory.create_command()
108 self.sdk.register_command("lint", cmd)
109 handler = self.factory.create_event_handler()
110 self.sdk.event_bus.subscribe("file_saved", handler)
111
112 def activate(self) -> None:
113 self.active = True
114 panel = self.factory.create_panel()
115 self.sdk.show_notification(f"Linter activated | {panel.render()}")
116
117 def deactivate(self) -> None:
118 self.active = False
119 self.sdk.show_notification("Linter deactivated")
120
121 def get_name(self) -> str:
122 return "Linter"
123
124
125 class ThemePlugin(Plugin):
126 def __init__(self):
127 self.factory = ThemePluginFactory()
128 self.sdk = None
129 self.active = False
130
131 def initialize(self, sdk: "PluginSDK") -> None:
132 self.sdk = sdk
133 cmd = self.factory.create_command()
134 self.sdk.register_command("switch_theme", cmd)
135 handler = self.factory.create_event_handler()
136 self.sdk.event_bus.subscribe("settings_changed", handler)
137
138 def activate(self) -> None:
139 self.active = True
140 panel = self.factory.create_panel()
141 self.sdk.show_notification(f"Theme activated | {panel.render()}")
142
143 def deactivate(self) -> None:
144 self.active = False
145 self.sdk.show_notification("Theme deactivated")
146
147 def get_name(self) -> str:
148 return "Theme"
149
150
151 # ── Adapter: Wrap a Legacy Tool ──────────────────────────────────
152
153 class LegacyFormatter:
154 """A third-party tool that does NOT implement Plugin."""
155 def format_code(self, source: str) -> str:
156 return source.strip().replace(" ", " ")
157
158 def get_version(self) -> str:
159 return "2.1.0"
160
161
162 class PluginAdapter(Plugin):
163 """Adapter wrapping LegacyFormatter to conform to Plugin interface."""
164
165 def __init__(self, legacy_tool: LegacyFormatter):
166 self._tool = legacy_tool
167 self.sdk = None
168
169 def initialize(self, sdk: "PluginSDK") -> None:
170 self.sdk = sdk
171 self.sdk.show_notification(
172 f"Legacy formatter v{self._tool.get_version()} adapted as plugin"
173 )
174
175 def activate(self) -> None:
176 sample = self._tool.format_code(" hello world ")
177 self.sdk.show_notification(f"Formatter active, sample: '{sample}'")
178
179 def deactivate(self) -> None:
180 self.sdk.show_notification("Formatter deactivated")
181
182 def get_name(self) -> str:
183 return "LegacyFormatterAdapter"
184
185
186 # ── Facade: PluginSDK ────────────────────────────────────────────
187
188 class PluginSDK:
189 """Simplified API over host internals. Plugins never touch internals."""
190
191 def __init__(self, event_bus: "PluginEventBus"):
192 self._file_system: dict[str, str] = {
193 "/src/main.py": "print('hello')",
194 "/src/utils.py": "def add(a, b): return a + b",
195 }
196 self._notifications: list[str] = []
197 self._commands: dict[str, Command] = {}
198 self.event_bus = event_bus
199
200 def read_file(self, path: str) -> str:
201 return self._file_system.get(path, f"[file not found: {path}]")
202
203 def show_notification(self, msg: str) -> None:
204 self._notifications.append(msg)
205 print(f" [SDK Notification] {msg}")
206
207 def register_command(self, name: str, cmd: Command) -> None:
208 self._commands[name] = cmd
209 print(f" [SDK] Registered command: {name}")
210
211 def run_command(self, name: str) -> str:
212 cmd = self._commands.get(name)
213 if cmd is None:
214 return f"Command '{name}' not found"
215 return cmd.execute()
216
217
218 # ── Observer: PluginEventBus ─────────────────────────────────────
219
220 class PluginEventBus:
221 """Publishes host events to subscribed plugin handlers."""
222
223 def __init__(self):
224 self._subscribers: dict[str, list[EventHandler]] = {}
225
226 def subscribe(self, event: str, handler: EventHandler) -> None:
227 self._subscribers.setdefault(event, []).append(handler)
228
229 def publish(self, event: str, data: dict) -> None:
230 print(f" [EventBus] Publishing '{event}'")
231 for handler in self._subscribers.get(event, []):
232 handler.handle(event, data)
233
234
235 # ── Prototype: PluginTemplate ────────────────────────────────────
236
237 class PluginTemplate:
238 """Pre-configured plugin scaffold. Clone instead of building from scratch."""
239
240 def __init__(self, name: str, config: dict):
241 self.name = name
242 self.config = config
243
244 def clone(self) -> "PluginTemplate":
245 return copy.deepcopy(self)
246
247 def __repr__(self) -> str:
248 return f"PluginTemplate(name='{self.name}', config={self.config})"
249
250
251 # ── Plugin Registry ──────────────────────────────────────────────
252
253 class PluginRegistry:
254 """Manages plugin lifecycle: register, load, unload."""
255
256 def __init__(self, sdk: PluginSDK):
257 self._plugins: dict[str, Plugin] = {}
258 self._active: set[str] = set()
259 self._sdk = sdk
260
261 def register(self, plugin: Plugin) -> None:
262 name = plugin.get_name()
263 self._plugins[name] = plugin
264 plugin.initialize(self._sdk)
265 print(f" [Registry] Registered: {name}")
266
267 def load_plugin(self, name: str) -> None:
268 plugin = self._plugins.get(name)
269 if plugin is None:
270 print(f" [Registry] Plugin '{name}' not found")
271 return
272 if name in self._active:
273 print(f" [Registry] Plugin '{name}' already active")
274 return
275 plugin.activate()
276 self._active.add(name)
277 print(f" [Registry] Loaded: {name}")
278
279 def unload_plugin(self, name: str) -> None:
280 plugin = self._plugins.get(name)
281 if plugin and name in self._active:
282 plugin.deactivate()
283 self._active.discard(name)
284 print(f" [Registry] Unloaded: {name}")
285
286 def list_plugins(self) -> list[str]:
287 return [
288 f"{n} ({'active' if n in self._active else 'inactive'})"
289 for n in self._plugins
290 ]
291
292
293 # ── Demo ─────────────────────────────────────────────────────────
294
295 if __name__ == "__main__":
296 # 1. Bootstrap host
297 print("=== 1. Bootstrap Host ===")
298 event_bus = PluginEventBus()
299 sdk = PluginSDK(event_bus)
300
301 # 2. Create registry and register plugins
302 print("\n=== 2. Register Plugins ===")
303 registry = PluginRegistry(sdk)
304 registry.register(LinterPlugin())
305 registry.register(ThemePlugin())
306
307 # 3. Adapter: wrap a legacy tool as a plugin
308 print("\n=== 3. Adapter: Legacy Formatter ===")
309 legacy = LegacyFormatter()
310 adapted = PluginAdapter(legacy)
311 registry.register(adapted)
312
313 # 4. Activate plugins
314 print("\n=== 4. Activate Plugins ===")
315 registry.load_plugin("Linter")
316 registry.load_plugin("Theme")
317 registry.load_plugin("LegacyFormatterAdapter")
318
319 # 5. Run registered commands
320 print("\n=== 5. Run Commands ===")
321 print(f" lint -> {sdk.run_command('lint')}")
322 print(f" switch_theme -> {sdk.run_command('switch_theme')}")
323
324 # 6. Fire events via the EventBus (Observer)
325 print("\n=== 6. Fire Events (Observer) ===")
326 event_bus.publish("file_saved", {"file": "/src/main.py"})
327 event_bus.publish("settings_changed", {"theme": "dark"})
328
329 # 7. Facade: read a file through the SDK
330 print("\n=== 7. Facade: SDK File Read ===")
331 content = sdk.read_file("/src/main.py")
332 print(f" /src/main.py -> {content}")
333
334 # 8. Prototype: clone plugin templates
335 print("\n=== 8. Prototype: Clone Templates ===")
336 starter = PluginTemplate("starter-plugin", {
337 "version": "1.0.0",
338 "entry": "index.py",
339 "permissions": ["fs.read", "ui.notify"],
340 })
341 clone_a = starter.clone()
342 clone_b = starter.clone()
343 clone_a.name = "my-linter-extension"
344 clone_a.config["permissions"].append("fs.write")
345 clone_b.name = "my-theme-extension"
346 print(f" Original: {starter}")
347 print(f" Clone A: {clone_a}")
348 print(f" Clone B: {clone_b}")
349 assert "fs.write" not in starter.config["permissions"], "Original must not change"
350 print(" Clones are independent of the original.")
351
352 # 9. Unload a plugin
353 print("\n=== 9. Unload Plugin ===")
354 registry.unload_plugin("Linter")
355
356 # 10. List all plugins
357 print("\n=== 10. Plugin Status ===")
358 for status in registry.list_plugins():
359 print(f" {status}")Common Mistakes
- ✗Exposing host internals directly to plugins. Any internal refactor breaks all plugins.
- ✗No plugin isolation. One broken plugin crashes the entire host.
- ✗Tight coupling between plugins. Plugin A importing Plugin B's classes directly.
- ✗Forgetting plugin lifecycle management. Plugins that leak resources when deactivated go unnoticed until production.
Key Points
- ✓Abstract Factory ensures each plugin provides a coherent family of components. You can't accidentally mix a dark-theme's UI with a linter's commands.
- ✓Adapter makes third-party code fit the Plugin interface without modifying it: wrap external APIs and the host never knows the difference.
- ✓Facade protects plugins from internal changes. The host can refactor freely as long as the SDK stays stable.
- ✓Prototype for scaffolding: clone a 'starter plugin' template rather than constructing from scratch.