Java SpringAI

Spring AI Multi-Agent Systems — Coordinator and Worker Agent Patterns

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
Topics: Java SpringAI
← Newer Post Older Post →