Gang of Four Meets the LLM Runtime
The Gang of Four catalogued 23 design patterns in 1994. They did not know about LLMs. But the patterns survived because they encode solutions to structural problems — not solutions to Java problems. Multi-agent systems have the same structural problems: object creation, event handling, behavioral variation, pipeline composition, shared state management. The patterns translate directly.
A design pattern is a solution to a recurring problem in a given context. The context changes — algorithms instead of objects, probabilistic instead of deterministic — but the problems recur. The pattern abstracts over the implementation details and exposes the structural solution. That structure is what survives.
The Factory Method pattern creates objects without specifying the exact class. Applied to agents: a Factory creates the right agent for the task without the caller needing to know which agent, which model, or which tools that agent will use.
The Factory pattern decouples agent creation from agent use. New agent types are added to the registry — no calling code changes. The factory guarantees every instance is correctly wired before execution begins.
AGENT_CONFIGS = {
"research": {"model": "gemini-flash", "tools": [search_web, browse_url]},
"coding": {"model": "claude-sonnet", "tools": [run_code, read_file]},
"writing": {"model": "gemini-pro", "tools": []},
"analysis": {"model": "o1-preview", "tools": [calculate, query_database]},
}
def create_agent(task_type: str) -> Agent:
config = AGENT_CONFIGS.get(task_type, AGENT_CONFIGS["writing"])
return Agent(
system_prompt = SYSTEM_PROMPTS[task_type],
model = config["model"],
tools = config["tools"]
)
The Observer pattern defines a subscription mechanism: objects subscribe to events and are notified when those events occur. Applied to agents: agents subscribe to business events and are triggered when conditions are met — without one agent calling another directly. This is the agentic equivalent of an event-driven microservices architecture.
Event-driven agent architecture replaces polling and scheduled queries with reactive subscriptions. Agents only execute when their trigger event arrives — eliminating wasted inference calls and idle computation.
@on_event("document.uploaded")
async def summarize_document(event: Event):
agent = create_agent("summarization")
summary = await agent.run(f"Summarize: {event.payload.document_url}")
await publish("document.summarized", summary=summary)
@on_event("survey.submitted")
async def analyze_sentiment(event: Event):
agent = create_agent("analysis")
sentiment = await agent.run(f"Rate sentiment 1–10: {event.payload.responses}")
await publish("survey.analyzed", sentiment=sentiment)
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. Applied to agents: the "algorithm" is the model, and models are selected at runtime based on task characteristics — accuracy requirements, latency budget, cost constraints, and context length.
| Task Type | Strategy (Model) | Rationale |
|---|---|---|
| Simple classification | gemini-flash (fast, cheap) | Does not need reasoning depth; latency matters |
| Multi-step reasoning | o1-preview (reasoning model) | Complex chain-of-thought; accuracy critical |
| Code generation | claude-3-5-sonnet | Strong code performance; supports long context |
| Long-document analysis | gemini-1.5-pro (1M context) | Document too long for other models |
| Real-time user-facing | gpt-4o-mini | Fast first-token; streaming response needed |
def select_model(task: Task) -> str:
if task.requires_reasoning and task.accuracy_threshold > 0.9:
return "o1-preview"
elif task.context_length > 100_000:
return "gemini-1.5-pro"
elif task.task_type == "code":
return "claude-3-5-sonnet-20241022"
elif task.latency_budget < 2.0: # seconds
return "gpt-4o-mini"
else:
return "gemini-pro" # default balanced choice
The Chain of Responsibility pattern passes a request along a chain of handlers, each deciding to process or pass forward. Applied to agents: a multi-agent pipeline where each stage performs a specific transformation on the data — receiving, processing, and handing off to the next stage with a clear contract.
Chain of Responsibility applies to agents as a data transformation pipeline. Each stage receives a progressively refined intermediate artifact — enabling progressive specialisation and drastic token reduction.
PIPELINE_CONTRACTS = {
"ingestion": {"input": "raw_document", "output": "structured_text"},
"research": {"input": "structured_text", "output": "annotated_text"},
"analysis": {"input": "annotated_text", "output": "analysis_report"},
"writing": {"input": "analysis_report", "output": "draft_document"},
"quality": {"input": "draft_document", "output": "final_document"},
}
The Singleton pattern ensures a class has only one instance and provides a global access point to it. Applied to multi-agent systems: shared workflow memory that all agents in a pipeline can read from and write to — a single source of truth for the state of the current workflow, preventing the context fragmentation problem (Chapter 4) in multi-hop workflows.
class WorkflowMemory:
_instance: Optional["WorkflowMemory"] = None
def __new__(cls) -> "WorkflowMemory":
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.state = {}
return cls._instance
def set(self, key: str, value: Any) -> None:
self.state[key] = value
def get(self, key: str, default: Any = None) -> Any:
return self.state.get(key, default)
# Any agent in the pipeline accesses the same memory object:
memory = WorkflowMemory()
memory.set("user_profile", profile) # written by Agent 1
profile = memory.get("user_profile") # read by Agent 3
Design patterns describe what works. Anti-patterns describe what looks like it works but reliably fails.
A single agent trying to do everything. Violates SRP (Chapter 9). The fix: decompose into specialized agents connected through an Orchestrator or pipeline.
Agents that communicate too frequently in tiny steps — sending a message, waiting for a reply, sending a follow-up — instead of batching work. Each round trip incurs latency and token cost. The fix: batch the context payload once, get a comprehensive response, pass it to the next stage. Use parallel sub-agents (Chapter 6) for independent tasks.
An agent in a pipeline that receives context, passes it to the next agent unchanged, and adds no value — essentially a relay node. Often a symptom of a pipeline that was designed wrong: this stage does not need to be an LLM call at all.
The Blind Passer anti-pattern occurs when an agent in a chain neither transforms nor validates its input — it simply forwards. Every agent in a pipeline must have a measurable responsibility; passive relays should be removed.
Design patterns encode structural solutions to recurring problems. The structure of the problem did not change — only the medium. Factory, Observer, Strategy, Chain of Responsibility, and Singleton all translate directly from class hierarchies to agent architectures because the underlying complexities — creation, events, variation, sequencing, shared state — are the same problems in a new domain.