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?"

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.

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.

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.

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.


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.

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

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.

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.

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.






Comments