Spring AI Multi-Agent Systems — Coordinator and Worker Agent Patterns
Complex tasks benefit from multiple specialized agents working together. A coordinator agent breaks down a goal into sub-tasks and delegates to specialist worker agents — each with its own tools, system prompt, and model choice. Spring AI's tool-calling capability makes it straightforward to wire coordinator-worker patterns in a standard Spring Boot application.
Multi-Agent Patterns
Pattern 1: Hierarchical (Coordinator + Workers)
Coordinator → decides task breakdown
├── Research Agent → web search, document lookup
├── Analysis Agent → data processing, calculations
└── Writing Agent → generates final report
Pattern 2: Pipeline (Sequential)
Input → Agent1 (classify) → Agent2 (enrich) → Agent3 (format) → Output
Pattern 3: Parallel (Fan-Out + Aggregator)
Question → [Agent1 | Agent2 | Agent3] → Aggregator → Answer
Specialist Agent Services
// Agent 1: Research Agent
@Service
public class ResearchAgent {
private final ChatClient chatClient;
public ResearchAgent(ChatClient.Builder builder, WebSearchTool webSearch) {
this.chatClient = builder
.defaultSystem("""
You are a research specialist. Your job is to gather relevant
facts and data from available sources. Return findings in a
structured list format with source citations.
""")
.defaultTools(webSearch)
.build();
}
public String research(String topic) {
return chatClient.prompt()
.user("Research this topic thoroughly: " + topic)
.call()
.content();
}
}
// Agent 2: Analysis Agent
@Service
public class AnalysisAgent {
private final ChatClient chatClient;
public AnalysisAgent(ChatClient.Builder builder, CalculatorTool calculator) {
this.chatClient = builder
.defaultSystem("""
You are a data analyst. You receive raw research findings
and produce structured analysis: key insights, trends,
risks, and data-backed conclusions.
""")
.defaultTools(calculator)
.build();
}
public String analyze(String researchFindings) {
return chatClient.prompt()
.user("Analyze these research findings and extract key insights:\n\n" + researchFindings)
.call()
.content();
}
}
// Agent 3: Writing Agent
@Service
public class WritingAgent {
private final ChatClient chatClient;
public WritingAgent(ChatClient.Builder builder) {
this.chatClient = builder
.defaultSystem("""
You are a technical writer. You take analysis results and
produce clear, well-structured reports suitable for
Java developers. Use headers, bullet points, and code
examples where relevant.
""")
.build();
}
public String write(String analysis, String format) {
return chatClient.prompt()
.user("Write a " + format + " based on this analysis:\n\n" + analysis)
.call()
.content();
}
}
Coordinator Agent
@Service
public class CoordinatorAgent {
private final ChatClient chatClient;
private final ResearchAgent researchAgent;
private final AnalysisAgent analysisAgent;
private final WritingAgent writingAgent;
public CoordinatorAgent(ChatClient.Builder builder,
ResearchAgent researchAgent,
AnalysisAgent analysisAgent,
WritingAgent writingAgent) {
this.researchAgent = researchAgent;
this.analysisAgent = analysisAgent;
this.writingAgent = writingAgent;
// Coordinator uses tools that call specialist agents
this.chatClient = builder
.defaultSystem("""
You are a project coordinator. Break down complex requests into tasks,
delegate to appropriate specialist agents, and synthesize results.
Always run: research → analysis → writing in sequence.
""")
.defaultTools(this) // coordinator's own @Tool methods
.build();
}
@Tool(description = "Delegate research task to the research specialist agent")
public String delegateResearch(String topic) {
System.out.println("[Coordinator] → Delegating research: " + topic);
return researchAgent.research(topic);
}
@Tool(description = "Delegate analysis of research findings to the analysis agent")
public String delegateAnalysis(String findings) {
System.out.println("[Coordinator] → Delegating analysis");
return analysisAgent.analyze(findings);
}
@Tool(description = "Delegate writing to the writing agent with specified format")
public String delegateWriting(String analysis, String format) {
System.out.println("[Coordinator] → Delegating writing: " + format);
return writingAgent.write(analysis, format);
}
public String execute(String goal) {
return chatClient.prompt()
.user("Complete this goal using the available agents: " + goal)
.call()
.content();
}
}
Parallel Fan-Out Pattern
@Service
public class ParallelAgentOrchestrator {
private final ChatClient factChecker;
private final ChatClient codeGenerator;
private final ChatClient securityReviewer;
public ParallelAgentOrchestrator(ChatClient.Builder builder) {
this.factChecker = builder
.defaultSystem("You verify technical facts. State TRUE/FALSE/UNCERTAIN for each claim.")
.build();
this.codeGenerator = builder
.defaultSystem("You generate clean Java code examples. Code only, no explanation.")
.build();
this.securityReviewer = builder
.defaultSystem("You review code for security vulnerabilities. List findings with severity.")
.build();
}
public Map<String, String> analyzeCode(String code) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newFixedThreadPool(3);
// Run all 3 agents in parallel
Future<String> factFuture = executor.submit(() ->
factChecker.prompt().user("Is this code following Java best practices?\n" + code).call().content());
Future<String> codeFuture = executor.submit(() ->
codeGenerator.prompt().user("Refactor this code:\n" + code).call().content());
Future<String> securityFuture = executor.submit(() ->
securityReviewer.prompt().user("Review for security:\n" + code).call().content());
Map<String, String> results = Map.of(
"factCheck", factFuture.get(),
"refactored", codeFuture.get(),
"securityReview", securityFuture.get()
);
executor.shutdown();
return results;
}
}
Controller
@RestController
@RequestMapping("/agents")
public class MultiAgentController {
private final CoordinatorAgent coordinator;
private final ParallelAgentOrchestrator parallel;
public MultiAgentController(CoordinatorAgent coordinator,
ParallelAgentOrchestrator parallel) {
this.coordinator = coordinator;
this.parallel = parallel;
}
@PostMapping("/execute")
public String execute(@RequestBody String goal) {
return coordinator.execute(goal);
}
@PostMapping("/analyze-code")
public Map<String, String> analyzeCode(@RequestBody String code)
throws InterruptedException, ExecutionException {
return parallel.analyzeCode(code);
}
}
Output
POST /agents/execute
Body: "Research Spring AI 1.0 features and write a 300-word blog post summary"
Console:
[Coordinator] → Delegating research: Spring AI 1.0 features
[Coordinator] → Delegating analysis
[Coordinator] → Delegating writing: 300-word blog post summary
Final output:
# Spring AI 1.0: What Java Developers Need to Know
Spring AI 1.0 GA brings production-ready AI to the Spring ecosystem...
[300-word blog post follows with key features, code examples, and takeaways]
Key Points
- Each agent has its own system prompt, tool set, and optionally its own model — specialist agents outperform a single generalist agent on complex tasks
- The coordinator pattern is most powerful when combined with tool calling — the coordinator calls specialist agents as tools
- Use parallel execution (CompletableFuture or ExecutorService) for independent sub-tasks — reduces total latency from N×latency to max(latency)
- Pass context between agents explicitly — agents don't share memory by default
- For long-running multi-agent workflows, use Spring Batch or an async job framework to handle agent failures and retries at each step
Comments