Chapter 10

Design Patterns in Multi-Agent Workflows

Gang of Four Meets the LLM Runtime

Part IV — The New Architecture 7 sections

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.


10.1   Why Patterns Survive

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.


10.2   Factory → Dynamic Agent Spawner

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.

Fig 10.1 — Dynamic Agent Spawner: Factory Pattern
Task Request type: "research" topic: "APAC risk" AgentFactory 1. Reads agent registry 2. Selects template 3. Injects dependencies 4. Returns agent instance Agent Registry research → ResearchAgentSpec ... ResearchAgent (instance) tools: [search, summarise] DataAgent (instance) tools: [sql, chart] WriterAgent (instance) tools: [formatter, export] Benefits: ✓ No new code for new type ✓ Type-safe DI ✓ Registry-driven configuration

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 Factory Pattern
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"]
    )

10.3   Observer → Event-Driven Agent Triggers

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.

Fig 10.2 — Event-Driven Agent Architecture
Data Source A API Webhook Cron Schedule User Action Event Bus Queue, filter, route events to subscribers (Kafka / Redis / SQS) AlertAgent Subscribes: threshold_breach AnalysisAgent Subscribes: new_data_available ReportAgent Subscribes: schedule_tick Action Agents are fully decoupled — no polling

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.

Event-Driven Agent Triggers
@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)

10.4   Strategy → Hot-Swap Models

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 TypeStrategy (Model)Rationale
Simple classificationgemini-flash (fast, cheap)Does not need reasoning depth; latency matters
Multi-step reasoningo1-preview (reasoning model)Complex chain-of-thought; accuracy critical
Code generationclaude-3-5-sonnetStrong code performance; supports long context
Long-document analysisgemini-1.5-pro (1M context)Document too long for other models
Real-time user-facinggpt-4o-miniFast first-token; streaming response needed
Strategy Pattern — Model Selection
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

10.5   Chain of Responsibility → Agent Pipeline

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.

Fig 10.3 — Agent Pipeline: Chain of Responsibility
Raw Document 12k tokens ExtractAgent Pulls key facts → structured JSON output: 400 tok EnrichAgent Adds external reference data output: 800 tok ScoreAgent Risk scoring → numeric output output: 100 tok NarrateAgent Human-readable summary output: 600 tok Final Report 600 tok 12,000 tokens → 600 tokens: 95% reduction through progressive specialisation

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 Stage Contracts
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"},
}

10.6   Singleton → Shared Workflow Memory

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.

Workflow Memory Singleton
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

10.7   Anti-Patterns in Multi-Agent Systems

Design patterns describe what works. Anti-patterns describe what looks like it works but reliably fails.

God Prompt Agent

A single agent trying to do everything. Violates SRP (Chapter 9). The fix: decompose into specialized agents connected through an Orchestrator or pipeline.

Chatty Agent

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.

Blind Passer

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.

Fig 10.4 — The Blind Passer Anti-Pattern
Agent A Produced a good result ✓ Agent B ⚠ BLIND PASSER Just forwards A's output without processing Agent C Receives A's output as if from B Problems: B wastes token budget for zero transformation Fix: Either remove Agent B from the pipeline, or give it a real transformation responsibility. Each agent in a chain must add specific value. Relay agents that only forward data are architectural debt.

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.

Core Principle — Chapter 10

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.