top of page

Design Patterns for Agentic Applications

  • Jun 29
  • 6 min read

A practitioner's guide to the architectural patterns that separate production-grade AI agents from experimental demos — with diagrams, code, and hard-won trade-offs.




What makes an app "agentic"?


An agentic application is not simply "an app that calls an LLM." It is a system where the model drives a multi-step process, making decisions about what tools to invoke, when to stop, and how to handle failures — autonomously.


The industry crossed a threshold when LLMs gained reliable tool-calling. Suddenly, you weren't just prompting for text — you were giving a model a set of verbs it could execute in the world. Agentic design patterns emerged from the practical question: "How do we structure these loops reliably?"


Fig 1 — the agentic execution loop (vs. standard LLM call)
Fig 1 — the agentic execution loop (vs. standard LLM call)

The fundamental difference: agentic apps run N LLM inferences in a loop, with observations from the environment feeding back in. This unlocks powerful capabilities — and introduces a new class of engineering challenges.




ReAct — Reasoning + Acting


The foundational pattern. ReAct (Yao et al., 2023) interleaves thinking and doing in a structured loop, giving the model a scratchpad for explicit reasoning before each action.


Before ReAct, models either reasoned (chain-of-thought) or acted (output tool calls). ReAct's insight was that you could do both in one pass — the model thinks, acts, observes, then thinks again. This dramatically improves performance on multi-hop tasks.


Fig 2 — react loop: thought → action → observation cycle
Fig 2 — react loop: thought → action → observation cycle



def react_loop(task: str, tools: list[Tool], max_steps: int = 10) -> str:
    messages = [{"role": "user", "content": task}]

    for step in range(max_steps):
        response = llm.complete(
            messages=messages,
            tools=tools,
            system="""
            Think step by step using this format:
            Thought: [your reasoning]
            Action: [tool_name(args)]
            --- OR if done ---
            Final Answer: [your answer]
            """
        )

        # Parse thought + action from response
        thought, action = parse_react_response(response)

        if action.name == "finish":
            return action.args["answer"]

        # Execute tool, get observation
        observation = execute_tool(action)

        # Feed observation back into context
        messages.extend([
            {"role": "assistant", "content": response},
            {"role": "user",      "content": f"Observation: {observation}"},
        ])

    raise Runtime Error("Max steps exceeded")

Engineering note


ReAct shines for tasks requiring dynamic information retrieval. The explicit "Thought:" prefix dramatically improves trace-ability — you can log reasoning chains and debug why an agent made a bad decision.


Watch out for


ReAct agents can "overthink" — spinning in reasoning loops without making progress. Always implement a max_steps guard and token budget. Monitor step counts in production as a health signal.


Tool Use & Function Calling


Tools are the hands of an agent. How you design, register, and execute them determines the reliability ceiling of your entire system.


Fig 3 — tool registry and execution pipeline
Fig 3 — tool registry and execution pipeline

The Tool Specification Contract

Every tool exposed to an LLM must have a precise JSON Schema definition. The quality of this schema directly impacts how reliably the model invokes the tool.


{

"name": "search_database",

"description": "Query the customer database. Use for any request requiring

customer records, order history, or account status. Returns JSON.",

"parameters": {

"type": "object",

"properties": {

"query": {

"type": "string",

"description": "SQL WHERE clause. E.g.: email = 'user@example.com'"

},

"limit": {

"type": "integer",

"default": 10,

"maximum": 100,

"description": "Max rows to return. Default 10."

}

},

"required": ["query"]

}

}


Pro tip — tool design principles


1. Idempotent read tools first, write tools with confirmation. Search is safe to retry; sending an email is not.

2. Return structured output (JSON) not prose — LLMs parse JSON from tool results more reliably.

3. Include an "error" field schema — the agent needs to handle failures gracefully.

4. Inject context like current_user_id server-side, never from the model output.



Planning & Task Decomposition


Complex goals need to be broken down. Planning patterns determine how an agent converts a high-level objective into a sequence of executable sub-tasks.


Fig 4 — three planning strategies compared
Fig 4 — three planning strategies compared

When to use which planning strategy

Linear

Sequential Plan Simple, predictable tasks. Customer support scripts. Report generation with known steps. Low complexity, high reliability.

Recommended for most agents

DAG Decomposition Tasks with parallelizable subtasks. Research + write + review. Independent sub-problems that can run concurrently. Best latency/quality tradeoff.

Advanced

Tree of Thought Open-ended problems, creative tasks, mathematical proofs. High token cost. Use when you need exploration over exploitation.



Memory Architecture Patterns


An agent without memory is an agent that starts over every conversation. Production agents need multiple memory tiers — each optimized for a different access pattern.


Fig 5 — memory architecture: four tiers
Fig 5 — memory architecture: four tiers



Memory compression pattern

Long-running agents hit token limits. Implement "memory consolidation": when context hits 80% capacity, trigger a summarisation pass — llm.summarise (old_messages) → compressed_episodic. Store the summary in episodic memory, evict old messages. This is the agentic equivalent of paging in an OS.



Multi-Agent Orchestration


Some tasks exceed what a single agent can handle — either due to context length, specialisation, or parallelism requirements. Multi-agent architectures distribute the work.


Fig 6 — orchestrator + specialist agent topology
Fig 6 — orchestrator + specialist agent topology

Critical: agent communication contracts


Agents must communicate via structured schemas, not free-text. Define a Task and Result Pydantic model. Free-text agent-to-agent communication creates cascading hallucinations — each agent inherits and amplifies errors from the previous.



// Models — using records for immutability


public record AgentTask(

string TaskId,

string AgentRole, // "research" | "coder" | "critic"

string Instruction,

Dictionary<string, object> Context,

List<string> Dependencies = null // task_ids to wait for

)

{

// Default value workaround for records

public List<string> Dependencies { get; init; } = Dependencies ?? [];

}


public record AgentResult(

string TaskId,

bool Success,

string Output,

Dictionary<string, object> Metadata, // tokens_used, latency_ms, model

string? Error = null // nullable = str | None

);


// Orchestrator


public class Orchestrator

{

private readonly ILlmClient _llm;


public Orchestrator(ILlmClient llm) => _llm = llm;


public async Task<string> RunAsync(string goal)

{

List<AgentTask> plan = await LlmPlanAsync(goal);

List<AgentResult> results = await ExecuteDagAsync(plan);

return await LlmSynthesizeAsync(results);

}


private async Task<List<AgentTask>> LlmPlanAsync(string goal)

{

// Call LLM, deserialize response into List<AgentTask>


}


// Parallel execution respecting dependency order

private async Task<List<AgentResult>> ExecuteDagAsync(List<AgentTask> plan)

{

var completed = new ConcurrentDictionary<string, AgentResult>();

var remaining = new List<AgentTask>(plan);


while (remaining.Count > 0)

{

// Tasks whose dependencies are all satisfied

var ready = remaining

.Where(t => t.Dependencies.All(dep => completed.ContainsKey(dep)))

.ToList();


if (ready.Count == 0)

throw new InvalidOperationException("Circular dependency detected");


// Run all ready tasks in parallel

var results = await Task.WhenAll(

ready.Select(task => ExecuteTaskAsync(task, completed))

);


foreach (var result in results)

{

completed[result.TaskId] = result;

remaining.RemoveAll(t => t.TaskId == result.TaskId);

}

}


return [.. completed.Values];

}


Reflection & Self-Critique


Fig 7 — reflection loop: generate → critique → revise
Fig 7 — reflection loop: generate → critique → revise

Agents make mistakes. Reflection patterns add a self-evaluation loop, where the agent (or a separate critic LLM) scores its own output and triggers revision if quality falls below a threshold.


Self-Consistency: A Cheaper Alternative


For factual tasks, self-consistency is more cost-effective than iterative reflection: generate N independent solutions, then pick the one the majority agree on. No critic LLM needed.


Reflection pattern trade-offs


Pattern A

Self-reflection

Same LLM critiques itself. Fast, cheap. Blind to its own systematic biases — can't catch what it can't see.


Most robust

Separate critic LLM

Different model (or same model with critic system prompt) reviews output. Catches more errors. 2× token cost but worth it for high-stakes outputs.



High quality

Human-in-the-loop

Human reviews before critical actions. Slowest but most reliable. Use for anything irreversible.



Human-in-the-Loop (HITL)


Full autonomy isn't always the goal. HITL patterns define the checkpoints where human judgment supersedes the model — protecting against catastrophic or irreversible actions.


Fig 8 — HITL escalation decision tree
Fig 8 — HITL escalation decision tree

Define your HITL thresholds explicitly in code, not as vibes:


HITL trigger taxonomy

Irreversible delete, send, publish, pay → always human high-impact >$X cost, >Y users affected → human approval low-confidence model confidence < 0.7 → clarify with user routine read-only, reversible, <$10 → execute autonomously


Event-Driven Agentic Architecture


The most scalable production agentic systems are event-driven. Agents don't block waiting for responses — they publish to queues and react to events, enabling thousands of concurrent agent runs.


Fig 9 — event-driven agent architecture with message bus
Fig 9 — event-driven agent architecture with message bus

Stateless Workers = Horizontal Scale


Each agent worker is completely stateless — it reads its context from the state store at the start of every invocation and writes results back. This means you can run 1 or 1,000 workers with no coordination logic. All state lives in the store, all progress lives in the queue.


Pattern Quick Reference


A condensed guide for picking the right pattern at the right phase of your build.


Fig 10 — Pattern selection matrix (complexity vs. reliability requirement)
Fig 10 — Pattern selection matrix (complexity vs. reliability requirement)

 
 
 

Comments


bottom of page