AI writes code fast, but it is also prone to a typical problem:
Every time a feature is added, more
if/elifbranches pile into the core code.
For example:
if domain == "legal":
...
elif domain == "finance":
...
elif domain == "medical":
...
With few features this is fine, but as the types keep growing, the core module becomes increasingly bloated, and every new feature risks affecting existing logic.
A more reasonable approach is to define upfront:
Plugin Extension Point
And let AI follow one principle:
New capabilities should preferably be added as new modules, not by modifying the core flow.
π Technical Profile
Plugin Architecture
Split easily-changing features into independent modules that plug into the system through a unified interface. The core system depends on interfaces, not on any specific implementation.
Among these:
Extension Point
A pre-reserved entry point for features to plug into the system.
For example:
Plugin"] B --> D["Finance
Plugin"] B --> E["Medical
Plugin"]
The core system doesn’t need to know how each plugin is implemented internally β it only needs to know:
They all follow the same interface.
π‘ A Simple Analogy
Plugin architecture is a lot like a computer’s USB port.
The computer doesn’t need a different machine for the mouse, the keyboard, and the USB drive β it just defines a unified USB interface.
Software architecture works the same way:
The core system defines “how to plug in”; the plugin decides “what to do once plugged in.”
1. Why Does AI Especially Need Extension Points?
Suppose the knowledge base currently supports:
general
legal
If you ask AI to add a finance domain, it will most likely write:
if domain == "general":
return process_general(document)
elif domain == "legal":
return process_legal(document)
elif domain == "finance":
return process_finance(document)
Next time you add a medical domain, another branch gets added.
Eventually:
Core Service
β
Lots of if / elif
β
All domain logic coupled together
A better way of thinking:
Core
β
Plugin Interface
β
Registry
β
Concrete Plugin
This follows the:
Open-Closed Principle
Put simply:
Open for extension, closed for modification.
Add features by adding new implementations, rather than repeatedly modifying already-stable core code.
2. Architectural Principles
1. Interface β Plugin β Registry
Plugin architecture can be summarized into three core roles.
Interface
First, define what capabilities a plugin must provide:
from abc import ABC, abstractmethod
class DomainPlugin(ABC):
@abstractmethod
def parse_metadata(self, raw: dict) -> dict:
pass
Here:
ABC (Abstract Base Class)
acts as the plugin contract.
Concrete plugins just implement it:
Plugin"] A --> C["Finance
Plugin"] A --> D["Medical
Plugin"]
The core system depends only on DomainPlugin, not on any specific domain.
Registry
The system also needs to know:
Which plugin should be used for a given domain?
This can be achieved with:
Registry Pattern
implemented as:
class PluginRegistry:
def __init__(self):
self._plugins = {}
def register(self, name, plugin):
self._plugins[name] = plugin
def get(self, name):
return self._plugins.get(name)
At runtime you only need:
plugin = registry.get(domain)
instead of:
if domain == ...
Loader
Plugin creation should preferably not be scattered across business code either.
Recommended:
Application Startup
β
Plugin Loader
β
Plugin Registry
At runtime:
Request
β
Registry.get()
β
Plugin
In other words:
The startup phase is responsible for discovery and registration; the runtime phase is responsible for lookup and use.
2. Going Further with Configuration: Dynamic Plugin Loading
If plugins will be added frequently in the future, you can make the concrete implementations configurable.
For example, store in the configuration:
app.plugins.legal.LegalPlugin
The system loads it dynamically via:
module = importlib.import_module(module_path)
plugin_cls = getattr(module, class_name)
This way, the core system doesn’t need to write:
from app.plugins.legal import LegalPlugin
from app.plugins.finance import FinancePlugin
from app.plugins.medical import MedicalPlugin
Adding a plugin becomes:
Implement Plugin
+
Add Configuration
β
System loads it automatically
This is:
Plug-and-Play.
3. What Features Are Suitable as Extension Points?
Not all code needs to be plugin-ized.
What genuinely deserves an extension point is usually:
A part of a stable flow that has multiple implementations and will keep changing in the future.
Common extension points in Agent systems include:
Core Runtime
β
βββ Tool Plugin
βββ Domain Plugin
βββ Retriever
βββ Reranker
βββ LLM Provider
βββ Vector Store
βββ Document Processor
Among them:
- Retriever
- Reranker
- LLM (Large Language Model) Provider
- Vector Store
may all have multiple implementations.
A very practical heuristic:
If a place starts accumulating more and more
if type == "A",elif type == "B"branches, check whether it’s missing an extension point.
But for simple, stable logic, there’s no need to force in Plugin, Registry, and Factory β otherwise you end up with:
Overengineering.
3. What Does Plugin Architecture Bring?
The most direct change:
Traditional approach
New feature
β
Modify Core
β
Re-validate core flow
becomes:
Plugin approach
New feature
β
Implement Plugin
β
Register
This brings several practical benefits:
- Lower coupling: domain logic doesn’t all pile into the core Service;
- Lower regression risk: new plugins rarely affect existing ones;
- Easier testing: each plugin can be tested independently;
- Easier swapping: implementations can be switched via configuration;
- Easier AI development: AI knows where a new feature should go.
The last point matters most.
Without extension points, AI’s development pattern tends to be:
Find old code
β
Insert new condition
β
Modify multiple Services
With extension points, it becomes:
Find the Extension Point
β
Implement Plugin
β
Register
Plugin architecture essentially draws a boundary for AI:
The correct direction for feature growth.
4. Putting Prompts into Practice: Explicitly Telling AI How to Extend
You can put the following rules into:
Project Rules
## Plugin and Extension Rules
1. Prefer extension points over adding type-based if/elif branches to core services.
2. Core modules must depend on abstractions, not concrete plugin implementations.
3. Extensible features should follow this flow: Interface β Plugin Implementation β Registry β Runtime Lookup.
4. Do not instantiate concrete plugins inside business services.
5. Plugin discovery and registration should happen during application startup.
6. Domain-specific behavior must stay inside plugins and must not leak into generic services.
7. Prefer configuration-driven registration when implementations can change independently.
8. Adding a plugin should ideally only require:
- The plugin implementation
- Registration/configuration
- Plugin tests
9. Do not introduce plugin abstractions when multiple implementations are unlikely.
10. Before adding a type-based if/elif branch, check whether that spot should be an extension point.
The core purpose is not to demand:
“Write a few more Plugin classes.”
but rather:
Stable capabilities stay in the Core; changeable capabilities go into Extension Points.
5. A Positive Example: miniagent’s Domain Plugin Mechanism
In miniagent’s knowledge base system, there is a typical extension point:
Domain Plugin
Different domains can have their own:
- Document metadata processing;
- Small-to-Big context expansion;
- Citation merging rules;
while the generic knowledge base flow doesn’t need to know the specifics of each domain.
1. DomainPlugin Defines the Plugin Contract
miniagent defines:
class DomainPlugin(ABC):
@property
@abstractmethod
def processor(self) -> SmallToBigProcessor:
...
@abstractmethod
def parse_metadata(self, raw: dict) -> dict:
...
@property
def citation_merger(self) -> CitationMerger:
return CitationMerger()
It defines several domain extension points:
DomainPlugin
β
βββ processor
βββ parse_metadata()
βββ citation_merger
A new domain only needs to implement this contract, without shoving domain-specific branching into the generic flow.
2. DomainRegistry Manages Plugins Centrally
miniagent uses:
class DomainRegistry:
def __init__(self):
self._plugins: dict[str, DomainPlugin] = {}
def register(
self,
domain: str,
plugin: DomainPlugin
) -> None:
self._plugins[domain] = plugin
def get(self, domain: str) -> DomainPlugin:
return self._plugins.get(domain)
The runtime relationship therefore becomes:
domain
β
DomainRegistry
β
DomainPlugin
instead of an ever-growing if/elif chain.
3. Loading Concrete Implementations Dynamically from the Database
Going one step further, miniagent reads the domain configuration at ServiceContainer startup:
domains = await self.domain_db.get_all_domains()
Then, based on the configuration’s:
processor_class
plugin_class
it loads them dynamically:
processor_cls = import_class(
domain_orm.processor_class
)
processor_instance = processor_cls()
plugin_cls = import_class(
domain_orm.plugin_class
)
plugin_instance = plugin_cls(
processor=processor_instance
)
Finally, it registers:
self.domain_registry.register(
domain=domain_orm.name,
plugin=plugin_instance
)
The core of the dynamic import is:
def import_class(class_path: str):
module_path, class_name = class_path.rsplit(".", 1)
module = importlib.import_module(module_path)
return getattr(module, class_name)
So the whole plugin loading process can be summarized as:
Database Configuration
β
processor_class / plugin_class
β
Dynamic Import
β
Create Plugin
β
DomainRegistry.register()
β
Runtime Lookup
The diagram below describes miniagent’s domain plugin implementation mechanism in detail:
What matters most here are two phases:
Startup phase
Discover β Create β Register plugins
Runtime phase
Lookup β Use plugins
Plugin management and business execution are therefore separated.
4. What Happens When a New Domain Is Added?
Suppose miniagent adds a finance domain in the future.
The ideal path is:
Finance Processor
β
Finance DomainPlugin
β
Add Domain configuration
β
System auto-loads at startup
β
DomainRegistry
instead of:
Modify Retrieval Service
Modify Document Service
Add finance if/elif
Modify core routes
This is exactly the effect plugin architecture hopes to achieve:
Extend the system by adding implementations, not by modifying the core flow.
Summary
AI is very good at adding more logic to existing code, but software that evolves over the long term needs:
Stable Core
β
Extension Point
β
Plugin
The plugin principle can be compressed into four statements:
Stable capabilities β stay in the Core
Changeable capabilities β go into Plugins
Plugin management β handled by the Registry
Concrete implementations β wired in via configuration
miniagent’s DomainPlugin + DomainRegistry + dynamic import + database configuration embodies this approach: the interface defines the extension boundary, the Registry manages concrete implementations, and ServiceContainer discovers and registers plugins dynamically at startup.
For AI programming, the single most valuable sentence to put in your project rules is:
When adding a new capability, first look for an extension point; if one exists, add a new implementation instead of modifying the core flow.
This way, AI isn’t endlessly “piling code” into the system, but inserting into the existing architecture a:
Module that is replaceable, testable, and independently iterable.
Open Source Code
πͺ Wishing you good luck πͺ