Why Fifty-Year-Old Rules Still Apply
SOLID is a set of five principles for writing maintainable object-oriented software: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion. It was formalized in the 1990s for Java and C#. It completely applies to agentic systems — not by analogy, but structurally.
The reason is simple: SOLID was never really about classes. It was about managing complexity and change in systems that have multiple components with different concerns. Agentic systems have all of that and more.
The failure trajectory of an agentic system without SOLID is predictable. Week 1: you build a working agent. Week 3: you need to add a new capability, so you extend the system prompt. Week 6: the prompt is 8,000 tokens. Behavior regresses when you make changes. Evals start failing mysteriously. You cannot tell which part of the prompt caused a bug. Week 10: you have a God Prompt, context rot, and a test suite that is effectively useless. SOLID exists to break that trajectory before it starts.
"A class should have only one reason to change."
Applied to agents: an agent should have only one clearly defined job. An agent that handles customer service enquiries, generates marketing copy, performs data analysis, and translates documents is not a customer service agent — it is a God Prompt with a name. Each of those is a separate reason to change the prompt, and changes to one concern will degrade others through context dilution.
A monolithic agent fails for five different reasons and none of them produce a clear error signal. Decomposition by responsibility makes each failure localised, diagnosable, and fixable in isolation.
# BEFORE — Single agent with multiple responsibilities
god_prompt = """
You are a customer service agent. Handle orders, refunds,
shipping inquiries, complaints, and general FAQs. For orders,
check status. For refunds, verify eligibility based on 30-day
policy. For shipping, check carrier API. For complaints...
[continues for 4,000 more tokens]
"""
# AFTER — Pipeline: each agent has one job
order_classifier → classifies the intent
refund_specialist → handles refund logic only
shipping_specialist → handles shipping queries only
escalation_handler → handles complaints / complex issues
faq_responder → handles general questions
"Open for extension, closed for modification."
Applied to agents: the system prompt is closed for modification once validated; new capabilities should be added through tool injection (Chapter 8), not by editing the core prompt. Every time you edit a working system prompt, you risk introducing regressions in all the behaviors that were already working. The Open/Closed approach keeps the prompt stable and extends capability through the tool interface.
# CLOSED: This system prompt is tested and validated. Do not modify.
SYSTEM_PROMPT = """
You are a customer service agent. When a user makes a request,
use the tools provided to find an answer. Tools define what you
can do — do not speculate about capabilities you don't have tools for.
"""
# OPEN for extension: add new tools without touching the prompt
v1_tools = [get_order_status]
v2_tools = [get_order_status, process_refund] # New capability added
v3_tools = [get_order_status, process_refund, # Another extension
get_shipping_status]
"Subtypes must be substitutable for their base types."
Applied to agents: if you design your prompts to be model-agnostic — avoiding formatting that only works on one model, avoiding reliance on specific model behaviors — then any model that supports your tool schema is a valid substitution. This is what enables model hot-swapping (Chapter 8). The key design rule: format your prompts for the interface, not for the implementation.
# ANTI-PATTERN: Relies on GPT-4's specific behavior for *** delimiters
prompt = """
Use *** to start and end each section of your response.
***Summary***
[your summary here]
***Recommendations***
[your recommendations here]
"""
# LSP-COMPLIANT: Uses explicit XML tags that work across models
prompt = """
Structure your response as:
<summary>A 2-3 sentence overview of the situation.</summary>
<recommendations>A numbered list of 3-5 actionable recommendations.</recommendations>
"""
"Clients should not be forced to depend on interfaces they do not use."
Applied to agents: do not inject tools an agent does not need. A tool is a claim on context Window attention. Injecting 20 tools into an agent that will only ever call 2 of them pollutes the context, increases hallucination risk (the model may invoke an irrelevant tool), and increases cost.
Applied to agents: no agent should be given a tool it doesn't require. Segregated tool interfaces save context tokens and enforce security boundaries that a monolithic tool cannot provide.
INTENT_TO_TOOLS = {
"order_status": [get_order_status, get_estimated_delivery],
"process_refund":[get_order_status, check_refund_eligibility,
apply_refund, send_confirmation_email],
"shipping_query":[get_shipping_status, get_carrier_info],
"general_faq": [search_knowledge_base],
}
def get_tools_for_intent(intent: str) -> list:
return INTENT_TO_TOOLS.get(intent, [search_knowledge_base])
"Depend on abstractions, not concretions."
Applied to agents: the agent prompt depends on the tool schema — the abstraction — not on the underlying implementation. The system prompt describes what the tool does, in general terms. Whether that description maps to a Google Search API, a Bing Search API, or a custom internal search engine is invisible to the agent. The prompt is not coupled to the implementation.
# The tool schema is the abstraction the agent depends on
SEARCH_TOOL_SCHEMA = {
"name": "search_web",
"description": "Search the web for current information on a topic.",
"parameters": {
"query": {"type": "string", "description": "The search query"}
}
}
# IMPLEMENTATION A — Google (production)
def google_search_impl(query: str) -> str:
return google_api.search(query)
# IMPLEMENTATION B — Bing (fallback)
def bing_search_impl(query: str) -> str:
return bing_api.search(query)
# IMPLEMENTATION C — Mock (testing)
def mock_search_impl(query: str) -> str:
return GOLDEN_DATASET_RESPONSES.get(query, "")
| Principle | Common Agentic Violation | Consequence |
|---|---|---|
| SRP | One agent handles 12 different service categories | Context rot; evals break unpredictably on changes |
| OCP | Adding capability by editing the validated system prompt | Regressions in previously working behavior |
| LSP | Prompts use GPT-4-specific markdown formatting | Model migration requires full rewrite and re-eval |
| ISP | Injecting all 20 tools into every agent call | Hallucinated tool calls; increased cost; reduced accuracy |
| DIP | API key and endpoint hardcoded in system prompt | Security exposure; unable to change implementation without re-testing prompt |
SOLID is not a checklist for Java developers. It is a framework for reasoning about change and complexity in systems composed of multiple components with different concerns. Agentic systems — with their prompts, models, tools, evaluations, and orchestrators — exhibit all of these properties. The patterns that worked in object-oriented systems work here too: for the same reasons, in the same ways, with the same payoffs.
SOLID principles are not rules about Java. They are rules about managing complexity and change. Agentic systems are not exempt from either — they are uniquely vulnerable to both.