Multi-Strategy Code Review Agent
Build a Spring AI agent that orchestrates 5 agentic workflow patterns.
Introduction
30 Second Summary
Code reviews are one of the most time-consuming parts of software development. Every team wishes they had a senior engineer available around the clock to catch security flaws, performance bottlenecks, and code quality issues before they reach production.
In this project, you will build a Code Review Agent API that orchestrates five distinct AI workflow patterns to produce comprehensive, multi-dimensional code reviews from a single REST endpoint.
What You'll Build
A Spring Boot REST API that accepts code snippets and returns structured reviews by routing, parallelizing, chaining, orchestrating, and evaluating AI-powered analysis steps using Spring AI and OpenRouter.
By the end of this project, you'll have:
- A working REST API with a /api/review endpoint that classifies, analyzes, and reviews code using all five agentic workflow patterns (Chain, Parallelization, Routing, Orchestrator-Workers, Evaluator-Optimizer).
- Custom Tool Calling with @Tool-annotated methods that the LLM invokes autonomously during code analysis.
- Structured JSON output with typed Java records for review categories, severity scores, and actionable recommendations.
- Secret Mission: Add real-time streaming via Server-Sent Events so a frontend can show each workflow stage as it executes.
Are there any prerequisites?
This project assumes strong Java and Spring Boot knowledge. You should be comfortable creating REST controllers, working with dependency injection, and reading Maven project files.
You will also need an OpenRouter account with an API key. Basic familiarity with calling AI models from code is helpful but not required.
Set Up the Project and Configure OpenRouter
Before you can orchestrate AI workflows, you need a properly configured Spring Boot application that knows how to talk to an LLM. Spring AI provides an OpenAI-compatible client that works with any provider exposing the same API shape.
In this step, you will scaffold a Spring Boot project, wire in the Spring AI dependencies, and point everything at OpenRouter so your application can send prompts to google/gemini-2.5-flash as its LLM backbone.
In this step, get ready to:
- Generate a Spring Boot 3.5 project with Spring AI and Web dependencies.
- Configure OpenRouter as the LLM provider via application properties.
- Verify the application compiles and starts successfully.
Generate the Spring Boot project
Spring Initializr is the official project generator for Spring Boot applications. It produces a ready-to-run Maven project with the correct parent POM and dependency structure.
- Open start.spring.io in your browser.
- Set **Project** to Maven and **Language** to Java.
- Set **Spring Boot** version to 3.5.0.
- Under **Project Metadata**, set **Group** to com.example and **Artifact** to code-review-agent.
- Set **Name** to code-review-agent and **Description** to AI-Powered Multi-Strategy Code Review Agent.
- Under **Dependencies**, click Add Dependencies and add: Spring Web, Spring Reactive Web, and OpenAI (under the AI section).
- Click Generate to download the zip file.
- Extract the zip to a location on your machine (such as your Desktop).
- Open IntelliJ IDEA and select File then Open.
- Navigate to the extracted code-review-agent folder and open it as a project.
- Wait for IntelliJ to finish importing the Maven project (you will see progress indicators in the bottom bar).
Why OpenRouter instead of OpenAI directly?
OpenRouter is a unified API gateway that gives you access to dozens of models (Gemini, Claude, GPT) through a single endpoint. Spring AI's OpenAI starter works with any OpenAI-compatible API, so you just change the base URL.
This means you can swap models later without changing any code.
Now you need to verify your pom.xml matches the required configuration. Spring Initializr may generate a slightly different BOM version, so you will pin it to the exact version this project uses.
- Open pom.xml in the root of the code-review-agent project.
- Replace the entire contents of pom.xml with the following:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.0</version>
<relativeTo/>
</parent>
<groupId>com.example</groupId>
<artifactId>code-review-agent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>code-review-agent</name>
<description>AI-Powered Multi-Strategy Code Review Agent</description>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.1.7</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
What does this POM configure?
- spring-boot-starter-parent 3.5.0 sets the Spring Boot version and manages common dependency versions.
- spring-ai-bom 1.1.7 locks all Spring AI modules to the same compatible version.
- spring-ai-starter-model-openai auto-configures a ChatClient that speaks the OpenAI protocol.
- spring-boot-starter-webflux adds reactive support. You will use this later for the streaming Secret Mission.
- Save pom.xml.
- IntelliJ should show a notification to reload Maven changes. Click Load Maven Changes (or press Cmd+Shift+I on macOS, Ctrl+Shift+O on Windows).
- Wait for the dependency download to complete. You should see no red errors in the pom.xml file.
Seeing red dependency errors?
Make sure you clicked Load Maven Changes after editing the POM. If dependencies still fail to resolve, right-click the project in the Project panel, select Maven, then Reload Project.
help me resolve Maven dependency errors in my Spring AI project
Configure OpenRouter as the LLM provider
Spring AI reads its configuration from application.yml (or application.properties). By setting the base-url property, you redirect all ChatClient calls to OpenRouter instead of OpenAI.
- In IntelliJ's Project panel, navigate to src/main/resources.
- If an application.properties file exists, delete it. You will use YAML format instead.
- Right-click the resources folder, select New then File, and name it application.yml.
- Paste the following configuration into application.yml:
spring:
ai:
openai:
base-url: https://openrouter.ai/api/v1
api-key: ${OPENROUTER_API_KEY}
chat:
options:
model: google/gemini-2.5-flash
temperature: 0.3
What does each property do?
- spring.ai.openai.base-url redirects the client from OpenAI's servers to OpenRouter's endpoint.
- spring.ai.openai.api-key reads from the OPENROUTER_API_KEY environment variable. This keeps your secret out of source control.
- model: google/gemini-2.5-flash selects Gemini 2.5 Flash via OpenRouter. It supports tool calling and is cost-effective for multi-call pipelines.
- temperature: 0.3 keeps responses deterministic and focused, which is ideal for code review analysis.
- Save application.yml.
Now confirm your main application class exists. Spring Initializr generates this automatically.
- In the Project panel, navigate to src/main/java/com/example/codereviewagent.
- Open CodeReviewAgentApplication.java and confirm it looks like this:
package com.example.codereviewagent;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CodeReviewAgentApplication {
public static void main(String[] args) {
SpringApplication.run(CodeReviewAgentApplication.class, args);
}
}
What does this class do?
The @SpringBootApplication annotation triggers auto-configuration, component scanning, and Spring Boot's startup sequence. Spring AI's OpenAI starter registers a ChatClient.Builder bean automatically because spring-ai-starter-model-openai is on the classpath.
Verify the application compiles and starts
The application reads your API key from an environment variable at startup. You need to provide that variable before running.
- In IntelliJ, click the dropdown next to the green Run button in the top toolbar.
- Select Edit Configurations.
- Select your CodeReviewAgentApplication configuration (or create one if it does not exist by clicking + and selecting Spring Boot).
- Find the Environment variables field.
- Enter your API key in this format: OPENROUTER_API_KEY=your-api-key-here (replace your-api-key-here with your actual OpenRouter API key).
- Click Apply then OK.
Keep your API key safe
Never commit API keys to version control. The ${OPENROUTER_API_KEY} syntax in application.yml reads from the environment at runtime, keeping the secret out of your source files.
- Click the green Run button (or press Shift+F10 on Windows, Ctrl+R on macOS) to start the application.
- Watch the console output. You should see Spring Boot's banner followed by a line like Started CodeReviewAgentApplication and the application listening on port 8080.
- Stop the application by clicking the red Stop button in the console panel.
Application fails to start?
- If you see a placeholder resolution error mentioning OPENROUTER_API_KEY, double-check that you entered the environment variable in the run configuration exactly as shown above.
- If port 8080 is already in use, stop whatever process is using it or add server.port: 8081 to your application.yml.
- If you see compilation errors, make sure Maven finished downloading all dependencies (check the Build panel for errors).
help me troubleshoot my Spring Boot application startup failure with Spring AI and OpenRouter
✔️ Awesome, I've got everything!
Great. Double check you have saved both pom.xml and application.yml before moving on.
ⓧ I'd like to double check the full code
Here are the three files you should have at this point:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.0</version>
<relativeTo/>
</parent>
<groupId>com.example</groupId>
<artifactId>code-review-agent</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>code-review-agent</name>
<description>AI-Powered Multi-Strategy Code Review Agent</description>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.1.7</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
spring:
ai:
openai:
base-url: https://openrouter.ai/api/v1
api-key: ${OPENROUTER_API_KEY}
chat:
options:
model: google/gemini-2.5-flash
temperature: 0.3
package com.example.codereviewagent;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CodeReviewAgentApplication {
public static void main(String[] args) {
SpringApplication.run(CodeReviewAgentApplication.class, args);
}
}
Your project foundation is solid. Next up, you will define the domain model and REST controller that accept code review requests and return structured responses.
Define the Domain Model and REST Controller
Your project compiles and connects to OpenRouter. Now you need to define the data contract for the review agent.
Every API needs a clear shape for what goes in and what comes out. You will create Java records that define the request (code to review), the response (structured findings), and the supporting types that carry routing decisions and individual findings through the pipeline.
In this step, get ready to:
- Create domain model records for requests, responses, and supporting types.
- Build a REST controller that accepts code review requests at /api/review.
- Stub out the orchestrator service that will coordinate all workflow patterns.
Create the request and response records
Java records are immutable data carriers that automatically generate constructors, accessors, and equals()/hashCode(). Spring Boot's Jackson integration serializes and deserializes records to JSON without any extra configuration.
- In IntelliJ, right-click the com.example.codereviewagent package in the Project tool window.
- Select New then Package.
- Name the package model and press Enter.
- Right-click the new model package, select New then Java Class.
- Select Record from the kind dropdown, name it CodeReviewRequest, and press Enter.
- Replace the file contents with this code:
package com.example.codereviewagent.model;
public record CodeReviewRequest(
String code,
String language,
String context
) {}
What does this record do?
CodeReviewRequest defines the three pieces of information a client sends when requesting a review. code holds the source code snippet, language tells the LLM what programming language it is, and context provides optional background about what the code does.
- Create a second record in the same model package named RouteDecision with this code:
package com.example.codereviewagent.model;
public record RouteDecision(
String category,
String reasoning,
int confidenceScore
) {}
- Create a third record named ReviewFinding with this code:
package com.example.codereviewagent.model;
public record ReviewFinding(
String category,
String severity,
String title,
String description,
String suggestion
) {}
What are RouteDecision and ReviewFinding?
RouteDecision captures how the Router workflow classifies code (e.g., security-sensitive, performance-critical). The confidenceScore tells you how certain the LLM was about the classification.
ReviewFinding represents a single issue the AI discovered. Each finding has a severity level and a concrete suggestion for fixing it.
- Create the final record named CodeReviewResponse with this code:
package com.example.codereviewagent.model;
import java.util.List;
public record CodeReviewResponse(
String summary,
RouteDecision route,
List<ReviewFinding> findings,
List<String> recommendations,
int overallScore,
String refinementNotes
) {}
What does this response contain?
CodeReviewResponse is the complete output a client receives. It bundles together the routing decision, all findings from the parallel analysis, the refined recommendations from the chain workflow, a quality score from the evaluator, and any refinement notes from the optimizer loop.
- Save all four files.
- Confirm there are no red underlines in any of the model files in the editor.
Seeing red underlines in CodeReviewResponse?
Make sure the import java.util.List; statement is present at the top of CodeReviewResponse.java. Also check that RouteDecision and ReviewFinding are in the same com.example.codereviewagent.model package.
Help me fix red underlines in my Java records
Create the REST controller
The controller is the entry point for HTTP requests. It stays thin by delegating all orchestration logic to a service class.
- Right-click the com.example.codereviewagent package, select New then Package, and name it controller.
- Create a new Java class named ReviewController inside the controller package.
- Replace the file contents with this code:
package com.example.codereviewagent.controller;
import com.example.codereviewagent.model.CodeReviewRequest;
import com.example.codereviewagent.model.CodeReviewResponse;
import com.example.codereviewagent.service.CodeReviewOrchestrator;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/review")
public class ReviewController {
private final CodeReviewOrchestrator orchestrator;
// Spring injects the orchestrator via constructor injection
public ReviewController(CodeReviewOrchestrator orchestrator) {
this.orchestrator = orchestrator;
}
// POST /api/review accepts a JSON body and returns a structured review
@PostMapping
public CodeReviewResponse review(@RequestBody CodeReviewRequest request) {
return orchestrator.executeReview(request);
}
}
What does this controller do?
@RestController tells Spring this class handles HTTP requests and automatically serializes return values to JSON. @RequestMapping("/api/review") maps all endpoints in this class to the /api/review path.
The controller has one job: accept the incoming CodeReviewRequest JSON and hand it to the orchestrator. All the complex workflow logic lives elsewhere.
- Save ReviewController.java.
You will see a red underline on the CodeReviewOrchestrator import. That is expected because the service class does not exist yet. You will create it next.
Create the orchestrator service stub
The CodeReviewOrchestrator is the heart of the application. It will eventually coordinate all five workflow patterns. For now, you will create a stub that returns a placeholder response so the project compiles.
- Right-click the com.example.codereviewagent package, select New then Package, and name it service.
- Create a new Java class named CodeReviewOrchestrator inside the service package.
- Replace the file contents with this code:
package com.example.codereviewagent.service;
import com.example.codereviewagent.model.*;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CodeReviewOrchestrator {
// Placeholder implementation - will be replaced with full workflow orchestration
public CodeReviewResponse executeReview(CodeReviewRequest request) {
return new CodeReviewResponse(
"Review pending",
new RouteDecision("GENERAL", "Not yet implemented", 0),
List.of(),
List.of(),
0,
""
);
}
}
Why a placeholder return?
The @Service annotation registers this class as a Spring bean so it can be injected into the controller. The placeholder return lets the project compile and respond to HTTP requests right now.
You will replace this stub with the full pipeline (Router, Parallel Analysis, Chain, Orchestrator-Workers, Evaluator-Optimizer) over the next several steps.
- Save CodeReviewOrchestrator.java.
- Check that the red underline in ReviewController.java is now gone.
- Run the application by clicking the green play button next to the main method in CodeReviewAgentApplication.java.
- Open a terminal and test the endpoint by running:
curl -X POST http://localhost:8080/api/review -H "Content-Type: application/json" -d '{"code": "int x = 1;", "language": "java", "context": "test"}'
- You should see a JSON response with "summary":"Review pending" and empty findings. This confirms the controller, model records, and service are wired correctly.
Connection refused or 404?
If you see "connection refused", make sure the application started successfully. Check the console for a line saying the app started on port 8080. If you see a 404, verify the endpoint path is exactly /api/review and you are sending a POST request, not a GET.
Help me debug my Spring Boot application not responding to requests
✔️ Awesome, I've got everything!
Great. Double check you have saved all files and stop the running application before moving on.
ⓧ I'd like to double check the full code
Here are the complete files you should have at this point.
package com.example.codereviewagent.model;
public record CodeReviewRequest(
String code,
String language,
String context
) {}
package com.example.codereviewagent.model;
public record RouteDecision(
String category,
String reasoning,
int confidenceScore
) {}
package com.example.codereviewagent.model;
public record ReviewFinding(
String category,
String severity,
String title,
String description,
String suggestion
) {}
package com.example.codereviewagent.model;
import java.util.List;
public record CodeReviewResponse(
String summary,
RouteDecision route,
List<ReviewFinding> findings,
List<String> recommendations,
int overallScore,
String refinementNotes
) {}
package com.example.codereviewagent.controller;
import com.example.codereviewagent.model.CodeReviewRequest;
import com.example.codereviewagent.model.CodeReviewResponse;
import com.example.codereviewagent.service.CodeReviewOrchestrator;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/review")
public class ReviewController {
private final CodeReviewOrchestrator orchestrator;
public ReviewController(CodeReviewOrchestrator orchestrator) {
this.orchestrator = orchestrator;
}
@PostMapping
public CodeReviewResponse review(@RequestBody CodeReviewRequest request) {
return orchestrator.executeReview(request);
}
}
package com.example.codereviewagent.service;
import com.example.codereviewagent.model.*;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CodeReviewOrchestrator {
public CodeReviewResponse executeReview(CodeReviewRequest request) {
return new CodeReviewResponse(
"Review pending",
new RouteDecision("GENERAL", "Not yet implemented", 0),
List.of(),
List.of(),
0,
""
);
}
}
Your API skeleton is in place. The controller accepts requests, the model records define the data contract, and the orchestrator is ready to be filled with real workflow logic. Next up, you will build the Router that classifies incoming code so each snippet gets the right review strategy.
Implement the Routing and Parallelization Workflows
Your domain model and REST controller are ready to accept code review requests. Now you need to build the intelligence behind the agent.
In this step, you'll implement two of the five agentic workflow patterns: Routing and Parallelization. The router classifies incoming code so the agent knows which review strategy to apply. The parallel workflow then fires off multiple specialized reviewers at the same time, dramatically cutting response latency.
In this step, get ready to:
- Build the ReviewRouter service that classifies code using structured output.
- Create custom tool methods the LLM can call autonomously during analysis.
- Build the ParallelReviewWorkflow that runs three analysis dimensions concurrently.
Build the ReviewRouter service
The Routing pattern is the agent's first decision point. Before analyzing code in detail, the router asks the LLM to classify the snippet into a category like SECURITY, PERFORMANCE, DATA_PROCESSING, or GENERAL. This classification determines how downstream workflows prioritize their analysis.
- Create a new file called ReviewRouter.java in the src/main/java/com/example/codereviewagent/service/ directory.
- Add the following code to build the routing service:
package com.example.codereviewagent.service;
import com.example.codereviewagent.model.RouteDecision;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class ReviewRouter {
private final ChatClient chatClient;
public ReviewRouter(ChatClient.Builder chatClientBuilder) {
// Build a ChatClient instance from the auto-configured builder
this.chatClient = chatClientBuilder.build();
}
public RouteDecision route(String code, String language) {
// Use structured output to map the LLM response directly to a RouteDecision record
return chatClient.prompt()
.system("""
You are a code classification expert. Analyze the given code and classify it into exactly one category.
Categories:
- SECURITY: Code that handles authentication, authorization, encryption, user input validation, SQL queries, or file access
- PERFORMANCE: Code with algorithms, loops, data structures, caching, database queries, or concurrent processing
- DATA_PROCESSING: Code that transforms, maps, filters, or aggregates data
- GENERAL: Code that doesn't strongly fit the above categories
Respond with the category, your reasoning, and a confidence score from 1-100.
""")
.user("Classify this %s code:\n```\n%s\n```".formatted(language, code))
.call()
.entity(RouteDecision.class);
}
}
What does this code do?
The ReviewRouter uses Spring AI's ChatClient to send a classification prompt to the LLM and receive a typed Java object back.
- The .system() method sets the system prompt that defines the LLM's role and the available categories.
- The .user() method passes the actual code to classify.
- The .entity(RouteDecision.class) call tells Spring AI to parse the LLM's response directly into your RouteDecision record. Spring AI handles the JSON schema generation and parsing automatically.
- Save ReviewRouter.java.
Why structured output instead of parsing strings?
Without structured output, you'd need to parse the LLM's free-text response manually. Spring AI's .entity() method automatically generates a JSON schema from your Java record and instructs the model to return data matching that schema. The response maps directly into your RouteDecision fields: category, reasoning, and confidenceScore.
Create the code analysis tools
Tool calling lets the LLM autonomously invoke helper methods during its reasoning process. Instead of you deciding when to run a complexity check, the model decides for itself based on what it sees in the code.
You'll create three tools the LLM can call: one for measuring complexity, one for detecting design patterns, and one for checking naming conventions.
- Create a new package directory src/main/java/com/example/codereviewagent/tools/.
- Create a new file called CodeAnalysisTools.java inside this tools package.
- Add the class skeleton and the first tool method by pasting this code:
package com.example.codereviewagent.tools;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
@Component
public class CodeAnalysisTools {
@Tool(description = "Calculate the cyclomatic complexity score of a code snippet. Returns an integer score where 1-10 is low complexity, 11-20 is moderate, and 21+ is high complexity.")
public int calculateCyclomaticComplexity(@ToolParam(description = "The source code to analyze") String code) {
// Count decision points to approximate cyclomatic complexity
int complexity = 1;
String[] keywords = {"if", "else if", "for", "while", "case", "catch", "&&", "||", "?"};
for (String keyword : keywords) {
int index = 0;
while ((index = code.indexOf(keyword, index)) != -1) {
complexity++;
index += keyword.length();
}
}
return complexity;
}
}
What is tool calling?
The @Tool annotation marks a method as callable by the LLM. Spring AI automatically generates a JSON schema from the method signature and sends it to the model. When the model decides it needs complexity data, it requests a tool call. Spring AI executes the method and feeds the result back to the model.
The @ToolParam annotation describes each parameter so the model knows what data to pass in. Detailed descriptions are critical for the model to use tools correctly.
- Now add the remaining two tool methods below calculateCyclomaticComplexity, still inside the CodeAnalysisTools class:
@Tool(description = "Detect common design patterns present in the code. Returns a comma-separated list of detected patterns such as Singleton, Factory, Observer, Builder, Strategy.")
public String detectDesignPatterns(@ToolParam(description = "The source code to analyze for design patterns") String code) {
// Scan for common pattern indicators in the code
StringBuilder patterns = new StringBuilder();
if (code.contains("getInstance") || code.contains("private static")) {
patterns.append("Singleton, ");
}
if (code.contains("create") && code.contains("interface")) {
patterns.append("Factory, ");
}
if (code.contains("subscribe") || code.contains("notify") || code.contains("listener")) {
patterns.append("Observer, ");
}
if (code.contains(".builder()") || code.contains("Builder")) {
patterns.append("Builder, ");
}
if (patterns.isEmpty()) {
return "No common patterns detected";
}
return patterns.substring(0, patterns.length() - 2);
}
@Tool(description = "Check naming conventions in the code. Returns a report of naming convention violations found, such as non-camelCase variables or uppercase constants without underscores.")
public String checkNamingConventions(@ToolParam(description = "The source code to check for naming convention violations") String code) {
// Look for lines with uppercase sequences followed by lowercase that aren't final constants
StringBuilder violations = new StringBuilder();
String[] lines = code.split("\n");
for (String line : lines) {
String trimmed = line.trim();
if (trimmed.matches(".*\\b[A-Z]{2,}[a-z].*=.*") && !trimmed.contains("final")) {
violations.append("Possible naming violation: ").append(trimmed.substring(0, Math.min(50, trimmed.length()))).append("\n");
}
}
if (violations.isEmpty()) {
return "No naming convention violations detected";
}
return violations.toString();
}
What do these tools do?
- detectDesignPatterns scans the code for common pattern indicators (like getInstance for Singleton or .builder() for Builder) and returns what it finds.
- checkNamingConventions looks for naming violations like uppercase sequences that aren't final constants.
- These are heuristic-based helpers. The LLM uses their output to supplement its own analysis with concrete data points.
- Save CodeAnalysisTools.java.
Import errors on @Tool or @ToolParam?
Make sure your imports include org.springframework.ai.tool.annotation.Tool and org.springframework.ai.tool.annotation.ToolParam. These come from the spring-ai-starter-model-openai dependency already in your pom.xml.
help me fix Spring AI @Tool import errors
Build the ParallelReviewWorkflow
The Parallelization pattern runs multiple review dimensions at the same time instead of sequentially. This cuts latency dramatically because security analysis, performance analysis, and code quality analysis can all happen concurrently.
- Create a new file called ParallelReviewWorkflow.java in the src/main/java/com/example/codereviewagent/service/ directory.
- Add the class structure and the parallel orchestration method:
package com.example.codereviewagent.service;
import com.example.codereviewagent.model.ReviewFinding;
import com.example.codereviewagent.tools.CodeAnalysisTools;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Service
public class ParallelReviewWorkflow {
private final ChatClient chatClient;
private final CodeAnalysisTools codeAnalysisTools;
// Fixed thread pool with 3 threads — one per analysis dimension
private final ExecutorService executor = Executors.newFixedThreadPool(3);
public ParallelReviewWorkflow(ChatClient.Builder chatClientBuilder, CodeAnalysisTools codeAnalysisTools) {
this.chatClient = chatClientBuilder.build();
this.codeAnalysisTools = codeAnalysisTools;
}
public List<ReviewFinding> analyzeInParallel(String code, String language, String category) {
// Launch all three analysis dimensions concurrently
CompletableFuture<List<ReviewFinding>> securityFuture = CompletableFuture.supplyAsync(
() -> analyzeSecurityDimension(code, language), executor);
CompletableFuture<List<ReviewFinding>> performanceFuture = CompletableFuture.supplyAsync(
() -> analyzePerformanceDimension(code, language), executor);
CompletableFuture<List<ReviewFinding>> qualityFuture = CompletableFuture.supplyAsync(
() -> analyzeQualityDimension(code, language), executor);
// Wait for all futures and aggregate results
List<ReviewFinding> allFindings = new ArrayList<>();
allFindings.addAll(securityFuture.join());
allFindings.addAll(performanceFuture.join());
allFindings.addAll(qualityFuture.join());
return allFindings;
}
}
What does analyzeInParallel do?
- CompletableFuture.supplyAsync() submits each analysis dimension as an asynchronous task to the thread pool.
- All three LLM calls fire simultaneously instead of waiting for one to finish before starting the next.
- .join() blocks until each future completes, then the results are merged into a single list.
- The CodeAnalysisTools dependency is injected so each dimension method can pass the tools to the ChatClient.
- Now add the three private dimension methods below analyzeInParallel, still inside the ParallelReviewWorkflow class:
private List<ReviewFinding> analyzeSecurityDimension(String code, String language) {
return chatClient.prompt()
.system("""
You are a security-focused code reviewer. Analyze the code for:
- SQL injection vulnerabilities
- Cross-site scripting (XSS) risks
- Insecure data handling
- Authentication/authorization flaws
- Input validation issues
Return findings as a JSON array.
""")
.user("Review this %s code for security issues:\n```\n%s\n```".formatted(language, code))
.tools(codeAnalysisTools)
.call()
.entity(new org.springframework.core.ParameterizedTypeReference<List<ReviewFinding>>() {});
}
private List<ReviewFinding> analyzePerformanceDimension(String code, String language) {
return chatClient.prompt()
.system("""
You are a performance-focused code reviewer. Analyze the code for:
- Algorithm efficiency (time/space complexity)
- Unnecessary object creation
- N+1 query patterns
- Missing caching opportunities
- Resource leaks
Return findings as a JSON array.
""")
.user("Review this %s code for performance issues:\n```\n%s\n```".formatted(language, code))
.tools(codeAnalysisTools)
.call()
.entity(new org.springframework.core.ParameterizedTypeReference<List<ReviewFinding>>() {});
}
private List<ReviewFinding> analyzeQualityDimension(String code, String language) {
return chatClient.prompt()
.system("""
You are a code quality reviewer. Analyze the code for:
- SOLID principle violations
- Code duplication
- Poor naming conventions
- Missing error handling
- Readability issues
Return findings as a JSON array.
""")
.user("Review this %s code for quality issues:\n```\n%s\n```".formatted(language, code))
.tools(codeAnalysisTools)
.call()
.entity(new org.springframework.core.ParameterizedTypeReference<List<ReviewFinding>>() {});
}
How does tool calling integrate here?
Each dimension method calls .tools(codeAnalysisTools) to make the @Tool-annotated methods available to the LLM. The model can autonomously decide to call calculateCyclomaticComplexity or detectDesignPatterns if it needs that data to form its findings.
The ParameterizedTypeReference<List<ReviewFinding>> is needed because Java's type erasure would otherwise prevent Spring AI from knowing the generic type at runtime. This ensures the response is deserialized into a list of ReviewFinding records.
- Save ParallelReviewWorkflow.java.
- Verify the project compiles by running:
./mvnw compile
You should see BUILD SUCCESS in the terminal output.
Compilation errors?
- If you see an error about ParameterizedTypeReference, make sure the import is org.springframework.core.ParameterizedTypeReference (from Spring Core, not Spring Web).
- If your IDE shows unresolved references to CodeAnalysisTools, check that the package declaration is com.example.codereviewagent.tools and that the file is in the matching directory path.
- If ChatClient.Builder cannot be resolved, ensure your pom.xml includes spring-ai-starter-model-openai and run ./mvnw dependency:resolve to download all dependencies.
help me fix compilation errors in my ParallelReviewWorkflow
✔️ Awesome, I've got everything!
Great. Double check you've saved all three files.
ⓧ I'd like to double check the full code
Here is the complete code for each file you created in this step.
ReviewRouter.java:
package com.example.codereviewagent.service;
import com.example.codereviewagent.model.RouteDecision;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class ReviewRouter {
private final ChatClient chatClient;
public ReviewRouter(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
public RouteDecision route(String code, String language) {
return chatClient.prompt()
.system("""
You are a code classification expert. Analyze the given code and classify it into exactly one category.
Categories:
- SECURITY: Code that handles authentication, authorization, encryption, user input validation, SQL queries, or file access
- PERFORMANCE: Code with algorithms, loops, data structures, caching, database queries, or concurrent processing
- DATA_PROCESSING: Code that transforms, maps, filters, or aggregates data
- GENERAL: Code that doesn't strongly fit the above categories
Respond with the category, your reasoning, and a confidence score from 1-100.
""")
.user("Classify this %s code:\n```\n%s\n```".formatted(language, code))
.call()
.entity(RouteDecision.class);
}
}
CodeAnalysisTools.java:
package com.example.codereviewagent.tools;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
@Component
public class CodeAnalysisTools {
@Tool(description = "Calculate the cyclomatic complexity score of a code snippet. Returns an integer score where 1-10 is low complexity, 11-20 is moderate, and 21+ is high complexity.")
public int calculateCyclomaticComplexity(@ToolParam(description = "The source code to analyze") String code) {
int complexity = 1;
String[] keywords = {"if", "else if", "for", "while", "case", "catch", "&&", "||", "?"};
for (String keyword : keywords) {
int index = 0;
while ((index = code.indexOf(keyword, index)) != -1) {
complexity++;
index += keyword.length();
}
}
return complexity;
}
@Tool(description = "Detect common design patterns present in the code. Returns a comma-separated list of detected patterns such as Singleton, Factory, Observer, Builder, Strategy.")
public String detectDesignPatterns(@ToolParam(description = "The source code to analyze for design patterns") String code) {
StringBuilder patterns = new StringBuilder();
if (code.contains("getInstance") || code.contains("private static")) {
patterns.append("Singleton, ");
}
if (code.contains("create") && code.contains("interface")) {
patterns.append("Factory, ");
}
if (code.contains("subscribe") || code.contains("notify") || code.contains("listener")) {
patterns.append("Observer, ");
}
if (code.contains(".builder()") || code.contains("Builder")) {
patterns.append("Builder, ");
}
if (patterns.isEmpty()) {
return "No common patterns detected";
}
return patterns.substring(0, patterns.length() - 2);
}
@Tool(description = "Check naming conventions in the code. Returns a report of naming convention violations found, such as non-camelCase variables or uppercase constants without underscores.")
public String checkNamingConventions(@ToolParam(description = "The source code to check for naming convention violations") String code) {
StringBuilder violations = new StringBuilder();
String[] lines = code.split("\n");
for (String line : lines) {
String trimmed = line.trim();
if (trimmed.matches(".*\\b[A-Z]{2,}[a-z].*=.*") && !trimmed.contains("final")) {
violations.append("Possible naming violation: ").append(trimmed.substring(0, Math.min(50, trimmed.length()))).append("\n");
}
}
if (violations.isEmpty()) {
return "No naming convention violations detected";
}
return violations.toString();
}
}
ParallelReviewWorkflow.java:
package com.example.codereviewagent.service;
import com.example.codereviewagent.model.ReviewFinding;
import com.example.codereviewagent.tools.CodeAnalysisTools;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Service
public class ParallelReviewWorkflow {
private final ChatClient chatClient;
private final CodeAnalysisTools codeAnalysisTools;
private final ExecutorService executor = Executors.newFixedThreadPool(3);
public ParallelReviewWorkflow(ChatClient.Builder chatClientBuilder, CodeAnalysisTools codeAnalysisTools) {
this.chatClient = chatClientBuilder.build();
this.codeAnalysisTools = codeAnalysisTools;
}
public List<ReviewFinding> analyzeInParallel(String code, String language, String category) {
CompletableFuture<List<ReviewFinding>> securityFuture = CompletableFuture.supplyAsync(
() -> analyzeSecurityDimension(code, language), executor);
CompletableFuture<List<ReviewFinding>> performanceFuture = CompletableFuture.supplyAsync(
() -> analyzePerformanceDimension(code, language), executor);
CompletableFuture<List<ReviewFinding>> qualityFuture = CompletableFuture.supplyAsync(
() -> analyzeQualityDimension(code, language), executor);
List<ReviewFinding> allFindings = new ArrayList<>();
allFindings.addAll(securityFuture.join());
allFindings.addAll(performanceFuture.join());
allFindings.addAll(qualityFuture.join());
return allFindings;
}
private List<ReviewFinding> analyzeSecurityDimension(String code, String language) {
return chatClient.prompt()
.system("""
You are a security-focused code reviewer. Analyze the code for:
- SQL injection vulnerabilities
- Cross-site scripting (XSS) risks
- Insecure data handling
- Authentication/authorization flaws
- Input validation issues
Return findings as a JSON array.
""")
.user("Review this %s code for security issues:\n```\n%s\n```".formatted(language, code))
.tools(codeAnalysisTools)
.call()
.entity(new org.springframework.core.ParameterizedTypeReference<List<ReviewFinding>>() {});
}
private List<ReviewFinding> analyzePerformanceDimension(String code, String language) {
return chatClient.prompt()
.system("""
You are a performance-focused code reviewer. Analyze the code for:
- Algorithm efficiency (time/space complexity)
- Unnecessary object creation
- N+1 query patterns
- Missing caching opportunities
- Resource leaks
Return findings as a JSON array.
""")
.user("Review this %s code for performance issues:\n```\n%s\n```".formatted(language, code))
.tools(codeAnalysisTools)
.call()
.entity(new org.springframework.core.ParameterizedTypeReference<List<ReviewFinding>>() {});
}
private List<ReviewFinding> analyzeQualityDimension(String code, String language) {
return chatClient.prompt()
.system("""
You are a code quality reviewer. Analyze the code for:
- SOLID principle violations
- Code duplication
- Poor naming conventions
- Missing error handling
- Readability issues
Return findings as a JSON array.
""")
.user("Review this %s code for quality issues:\n```\n%s\n```".formatted(language, code))
.tools(codeAnalysisTools)
.call()
.entity(new org.springframework.core.ParameterizedTypeReference<List<ReviewFinding>>() {});
}
}
You now have the first two agentic patterns running. The router classifies code in a single LLM call, and the parallel workflow fires three specialized reviewers simultaneously with tool-calling capabilities. Next up, you'll build the Chain workflow that transforms scattered findings into a coherent, prioritized report.
Implement the Chain, Orchestrator-Workers, and Evaluator-Optimizer Workflows
Your agent can now route code and analyze it in parallel across three dimensions. The raw findings from those parallel workers are useful, but they arrive as an unstructured pile of issues with no priority or polish.
In this step, you'll build the three remaining workflow patterns that turn raw findings into a refined, professional review. A Chain sequences findings through prioritization and formatting. An Orchestrator-Workers pattern discovers gaps the initial analysis missed. An Evaluator-Optimizer loop refines the output until it meets a quality threshold.
In this step, get ready to:
- Build the Chain workflow that sequences findings into prioritized recommendations.
- Build the Orchestrator-Workers pattern that dynamically discovers missed review areas.
- Build the Evaluator-Optimizer loop that refines review quality with a feedback cycle.
Build the ReviewChainWorkflow service
The Chain pattern passes data through a sequence of transformations where each step's output becomes the next step's input. Your chain will take raw findings, prioritize them by severity, convert them into actionable recommendations, and format the result as structured JSON.
- Create a new file ReviewChainWorkflow.java in the src/main/java/com/example/codereviewagent/service/ package.
- Add the following code to define the chain workflow:
package com.example.codereviewagent.service;
import com.example.codereviewagent.model.ReviewFinding;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ReviewChainWorkflow {
private final ChatClient chatClient;
public ReviewChainWorkflow(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
public List<String> chain(List<ReviewFinding> findings, String code) {
// Step 1: Prioritize findings
String prioritized = chatClient.prompt()
.system("You are a senior engineer. Prioritize these code review findings by impact and urgency. Group them into Critical, High, Medium, Low.")
.user("Findings to prioritize:\n%s".formatted(findings.toString()))
.call()
.content();
// Step 2: Generate actionable recommendations
String recommendations = chatClient.prompt()
.system("You are a mentor. Convert prioritized findings into specific, actionable recommendations. Each recommendation should include what to change and why.")
.user("Prioritized findings:\n%s\n\nOriginal code:\n```\n%s\n```".formatted(prioritized, code))
.call()
.content();
// Step 3: Format as structured list
return chatClient.prompt()
.system("Extract the recommendations as a clean JSON array of strings. Each string should be one actionable recommendation.")
.user(recommendations)
.call()
.entity(new org.springframework.core.ParameterizedTypeReference<List<String>>() {});
}
}
What does this code do?
The chain() method makes three sequential ChatClient calls, each building on the previous one's output:
- Step 1 uses .content() to get raw text from the LLM. It groups findings into Critical, High, Medium, and Low priority.
- Step 2 takes that prioritized text and the original code, then generates concrete recommendations with reasons.
- Step 3 uses .entity() with a ParameterizedTypeReference to parse the final output into a typed List<String>. This is Spring AI's structured output feature converting freeform text into usable data.
- Save ReviewChainWorkflow.java.
Why use .content() for intermediate steps?
Only the final step needs structured output (.entity()). Intermediate steps use .content() because they produce free-form reasoning text that feeds into the next prompt. Forcing structure on intermediate reasoning would constrain the LLM's ability to think through the problem.
Seeing a compilation error?
Make sure you have the import com.example.codereviewagent.model.ReviewFinding; at the top. The ParameterizedTypeReference import comes from org.springframework.core, not org.springframework.ai.
Help me fix a compilation error in ReviewChainWorkflow
Build the DynamicReviewOrchestrator service
The Orchestrator-Workers pattern is different from fixed parallelization. Instead of running pre-defined tasks, the orchestrator asks the LLM to identify what was missed and dynamically spawns worker tasks to fill those gaps.
- Create a new file DynamicReviewOrchestrator.java in the src/main/java/com/example/codereviewagent/service/ package.
- Add the following code to define the orchestrator-workers pattern:
package com.example.codereviewagent.service;
import com.example.codereviewagent.model.ReviewFinding;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class DynamicReviewOrchestrator {
private final ChatClient chatClient;
public DynamicReviewOrchestrator(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
public List<ReviewFinding> discoverAndReview(String code, String language, List<ReviewFinding> existingFindings) {
// Step 1: Ask the LLM to identify what the initial analysis missed
List<String> subtasks = chatClient.prompt()
.system("""
You are a thorough code review architect. Given existing findings, identify what areas
were NOT covered. Return a JSON array of strings, each describing a review subtask
that should still be performed. Focus on: concurrency issues, API design flaws,
error propagation patterns, or domain-specific concerns. Return at most 3 subtasks.
If nothing is missing, return an empty array.
""")
.user("Code:\n```%s\n%s\n```\n\nExisting findings:\n%s".formatted(language, code, existingFindings.toString()))
.call()
.entity(new org.springframework.core.ParameterizedTypeReference<List<String>>() {});
if (subtasks == null || subtasks.isEmpty()) {
return List.of();
}
// Step 2: Execute each subtask as a worker
List<ReviewFinding> additionalFindings = new ArrayList<>();
for (String subtask : subtasks) {
List<ReviewFinding> workerResult = chatClient.prompt()
.system("You are a specialist code reviewer. Perform the following review task and return findings as a JSON array.")
.user("Task: %s\n\nCode:\n```%s\n%s\n```".formatted(subtask, language, code))
.call()
.entity(new org.springframework.core.ParameterizedTypeReference<List<ReviewFinding>>() {});
if (workerResult != null) {
additionalFindings.addAll(workerResult);
}
}
return additionalFindings;
}
}
What does this code do?
The discoverAndReview() method has two phases:
- The orchestrator sends the existing findings to the LLM and asks what was missed. The LLM returns up to 3 subtask descriptions as strings. If nothing is missing, it returns an empty array and the method exits early.
- For each discovered subtask, a worker prompt runs a focused review and returns typed ReviewFinding objects. The null check on workerResult guards against the LLM occasionally returning malformed responses.
- Save DynamicReviewOrchestrator.java.
Why cap subtasks at 3?
Each subtask triggers a separate LLM call. Without a cap, the orchestrator could generate unlimited work. The system prompt instructs the LLM to return at most 3 subtasks, keeping latency and API costs predictable.
Getting a NullPointerException at runtime?
The structured output call can return null if the LLM's response doesn't parse into the target type. The if (subtasks == null || subtasks.isEmpty()) guard and the if (workerResult != null) check handle these cases. Make sure you have both in place.
Help me debug a NullPointerException in my DynamicReviewOrchestrator
Build the ReviewEvaluator service
The Evaluator-Optimizer pattern creates a feedback loop. It scores the current output against quality criteria, and if the score is too low, it feeds the critique back into the LLM to regenerate an improved version. This gives the agent a form of self-correction.
- Create a new file ReviewEvaluator.java in the src/main/java/com/example/codereviewagent/service/ package.
- Add the following code to define the evaluator-optimizer loop:
package com.example.codereviewagent.service;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ReviewEvaluator {
private final ChatClient chatClient;
private static final int MAX_ITERATIONS = 2;
private static final int QUALITY_THRESHOLD = 7;
public ReviewEvaluator(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
public EvaluationResult evaluate(List<String> recommendations, String code) {
List<String> currentRecommendations = recommendations;
String refinementNotes = "";
for (int i = 0; i < MAX_ITERATIONS; i++) {
EvaluationScore score = chatClient.prompt()
.system("""
You are a code review quality assessor. Evaluate the recommendations against these criteria:
- Completeness (covers all major issues): 1-10
- Actionability (specific enough to implement): 1-10
- Accuracy (technically correct): 1-10
Return your overall score (1-10) and a critique of what's missing or wrong.
""")
.user("Code:\n```\n%s\n```\n\nRecommendations:\n%s".formatted(code, currentRecommendations.toString()))
.call()
.entity(EvaluationScore.class);
if (score != null && score.overallScore() >= QUALITY_THRESHOLD) {
return new EvaluationResult(currentRecommendations, score.overallScore(), refinementNotes);
}
// Refine based on critique
String critique = score != null ? score.critique() : "Improve specificity and completeness";
refinementNotes = critique;
currentRecommendations = chatClient.prompt()
.system("Improve these code review recommendations based on the following critique. Return as a JSON array of strings.")
.user("Critique: %s\n\nCurrent recommendations:\n%s\n\nOriginal code:\n```\n%s\n```".formatted(critique, currentRecommendations.toString(), code))
.call()
.entity(new org.springframework.core.ParameterizedTypeReference<List<String>>() {});
}
return new EvaluationResult(currentRecommendations, 0, refinementNotes);
}
public record EvaluationScore(int overallScore, String critique) {}
public record EvaluationResult(List<String> recommendations, int score, String refinementNotes) {}
}
What does this code do?
The evaluate() method runs a loop that scores recommendations and refines them if they fall short:
- MAX_ITERATIONS = 2 prevents infinite loops and keeps API costs bounded. The loop exits early if quality is good enough.
- QUALITY_THRESHOLD = 7 is the minimum acceptable score. If the LLM rates the recommendations at 7 or above, the result is returned immediately.
- The inner records EvaluationScore and EvaluationResult give the structured output parser a shape to deserialize into. The LLM returns JSON that Spring AI maps directly to these records.
- Save ReviewEvaluator.java.
- Confirm your project still compiles by running this in the terminal from the code-review-agent project root:
./mvnw compile
You should see BUILD SUCCESS in the terminal output. All three new service classes should compile without errors.
Seeing BUILD FAILURE?
Check that all three files are in the correct package path: src/main/java/com/example/codereviewagent/service/. A misplaced file causes package declaration errors.
Also verify that the ReviewFinding record in your model package matches what the services import. The DynamicReviewOrchestrator and ReviewChainWorkflow both import it.
Help me fix a Maven compilation error in my Spring AI project
✔️ Awesome, I've got everything!
Great. Make sure all three files are saved before moving on.
ⓧ I'd like to double check the full code
Here is the full code for each of the three new files. Compare yours against these:
package com.example.codereviewagent.service;
import com.example.codereviewagent.model.ReviewFinding;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ReviewChainWorkflow {
private final ChatClient chatClient;
public ReviewChainWorkflow(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
public List<String> chain(List<ReviewFinding> findings, String code) {
// Step 1: Prioritize findings
String prioritized = chatClient.prompt()
.system("You are a senior engineer. Prioritize these code review findings by impact and urgency. Group them into Critical, High, Medium, Low.")
.user("Findings to prioritize:\n%s".formatted(findings.toString()))
.call()
.content();
// Step 2: Generate actionable recommendations
String recommendations = chatClient.prompt()
.system("You are a mentor. Convert prioritized findings into specific, actionable recommendations. Each recommendation should include what to change and why.")
.user("Prioritized findings:\n%s\n\nOriginal code:\n```\n%s\n```".formatted(prioritized, code))
.call()
.content();
// Step 3: Format as structured list
return chatClient.prompt()
.system("Extract the recommendations as a clean JSON array of strings. Each string should be one actionable recommendation.")
.user(recommendations)
.call()
.entity(new org.springframework.core.ParameterizedTypeReference<List<String>>() {});
}
}
package com.example.codereviewagent.service;
import com.example.codereviewagent.model.ReviewFinding;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class DynamicReviewOrchestrator {
private final ChatClient chatClient;
public DynamicReviewOrchestrator(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
public List<ReviewFinding> discoverAndReview(String code, String language, List<ReviewFinding> existingFindings) {
// Step 1: Ask the LLM to identify what the initial analysis missed
List<String> subtasks = chatClient.prompt()
.system("""
You are a thorough code review architect. Given existing findings, identify what areas
were NOT covered. Return a JSON array of strings, each describing a review subtask
that should still be performed. Focus on: concurrency issues, API design flaws,
error propagation patterns, or domain-specific concerns. Return at most 3 subtasks.
If nothing is missing, return an empty array.
""")
.user("Code:\n```%s\n%s\n```\n\nExisting findings:\n%s".formatted(language, code, existingFindings.toString()))
.call()
.entity(new org.springframework.core.ParameterizedTypeReference<List<String>>() {});
if (subtasks == null || subtasks.isEmpty()) {
return List.of();
}
// Step 2: Execute each subtask as a worker
List<ReviewFinding> additionalFindings = new ArrayList<>();
for (String subtask : subtasks) {
List<ReviewFinding> workerResult = chatClient.prompt()
.system("You are a specialist code reviewer. Perform the following review task and return findings as a JSON array.")
.user("Task: %s\n\nCode:\n```%s\n%s\n```".formatted(subtask, language, code))
.call()
.entity(new org.springframework.core.ParameterizedTypeReference<List<ReviewFinding>>() {});
if (workerResult != null) {
additionalFindings.addAll(workerResult);
}
}
return additionalFindings;
}
}
package com.example.codereviewagent.service;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class ReviewEvaluator {
private final ChatClient chatClient;
private static final int MAX_ITERATIONS = 2;
private static final int QUALITY_THRESHOLD = 7;
public ReviewEvaluator(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
public EvaluationResult evaluate(List<String> recommendations, String code) {
List<String> currentRecommendations = recommendations;
String refinementNotes = "";
for (int i = 0; i < MAX_ITERATIONS; i++) {
EvaluationScore score = chatClient.prompt()
.system("""
You are a code review quality assessor. Evaluate the recommendations against these criteria:
- Completeness (covers all major issues): 1-10
- Actionability (specific enough to implement): 1-10
- Accuracy (technically correct): 1-10
Return your overall score (1-10) and a critique of what's missing or wrong.
""")
.user("Code:\n```\n%s\n```\n\nRecommendations:\n%s".formatted(code, currentRecommendations.toString()))
.call()
.entity(EvaluationScore.class);
if (score != null && score.overallScore() >= QUALITY_THRESHOLD) {
return new EvaluationResult(currentRecommendations, score.overallScore(), refinementNotes);
}
// Refine based on critique
String critique = score != null ? score.critique() : "Improve specificity and completeness";
refinementNotes = critique;
currentRecommendations = chatClient.prompt()
.system("Improve these code review recommendations based on the following critique. Return as a JSON array of strings.")
.user("Critique: %s\n\nCurrent recommendations:\n%s\n\nOriginal code:\n```\n%s\n```".formatted(critique, currentRecommendations.toString(), code))
.call()
.entity(new org.springframework.core.ParameterizedTypeReference<List<String>>() {});
}
return new EvaluationResult(currentRecommendations, 0, refinementNotes);
}
public record EvaluationScore(int overallScore, String critique) {}
public record EvaluationResult(List<String> recommendations, int score, String refinementNotes) {}
}
You now have all five agentic workflow patterns implemented as independent, composable services. Next up, you'll wire them all together in the main orchestrator to produce complete end-to-end code reviews.
Wire Everything Together and Test End-to-End
You have five workflow services sitting side by side, each handling one piece of the review puzzle. On their own, they do nothing useful for a caller hitting your API.
In this step, you will implement the CodeReviewOrchestrator that wires all five patterns into a single pipeline. Then you will start the application and fire real code samples at it to see structured reviews come back.
In this step, get ready to:
- Connect all workflow patterns in the orchestrator: Route, Parallel Analysis, Orchestrator-Workers, Chain, Evaluator-Optimizer.
- Start the application and test with curl using different code samples.
- Verify the complete pipeline produces structured, high-quality JSON reviews.
Implement the CodeReviewOrchestrator
The orchestrator is the conductor of your agent. It calls each workflow service in sequence, passing outputs from one stage as inputs to the next.
- Open CodeReviewOrchestrator.java in the service package.
- Replace its contents with the full orchestrator implementation:
package com.example.codereviewagent.service;
import com.example.codereviewagent.model.*;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class CodeReviewOrchestrator {
private final ReviewRouter router;
private final ParallelReviewWorkflow parallelWorkflow;
private final ReviewChainWorkflow chainWorkflow;
private final DynamicReviewOrchestrator dynamicOrchestrator;
private final ReviewEvaluator evaluator;
public CodeReviewOrchestrator(ReviewRouter router,
ParallelReviewWorkflow parallelWorkflow,
ReviewChainWorkflow chainWorkflow,
DynamicReviewOrchestrator dynamicOrchestrator,
ReviewEvaluator evaluator) {
this.router = router;
this.parallelWorkflow = parallelWorkflow;
this.chainWorkflow = chainWorkflow;
this.dynamicOrchestrator = dynamicOrchestrator;
this.evaluator = evaluator;
}
public CodeReviewResponse executeReview(CodeReviewRequest request) {
// 1. Routing: Classify the code
RouteDecision route = router.route(request.code(), request.language());
// 2. Parallelization: Run multi-dimensional analysis
List<ReviewFinding> findings = parallelWorkflow.analyzeInParallel(
request.code(), request.language(), route.category());
// 3. Orchestrator-Workers: Discover and fill gaps
List<ReviewFinding> additionalFindings = dynamicOrchestrator.discoverAndReview(
request.code(), request.language(), findings);
List<ReviewFinding> allFindings = new ArrayList<>(findings);
allFindings.addAll(additionalFindings);
// 4. Chain: Transform findings into recommendations
List<String> recommendations = chainWorkflow.chain(allFindings, request.code());
// 5. Evaluator-Optimizer: Refine until quality threshold met
ReviewEvaluator.EvaluationResult evaluationResult = evaluator.evaluate(recommendations, request.code());
// Build summary
String summary = "Reviewed %s code classified as %s. Found %d issues across %d dimensions."
.formatted(request.language(), route.category(), allFindings.size(),
allFindings.stream().map(ReviewFinding::category).distinct().count());
return new CodeReviewResponse(
summary,
route,
allFindings,
evaluationResult.recommendations(),
evaluationResult.score(),
evaluationResult.refinementNotes()
);
}
}
What does this code do?
The executeReview() method runs all 5 agentic patterns in sequence:
- Routing classifies the code into a category (SECURITY, PERFORMANCE, DATA_PROCESSING, or GENERAL).
- Parallelization runs security, performance, and quality analysis concurrently.
- Orchestrator-Workers asks the LLM what the parallel analysis missed, then dispatches workers to fill those gaps.
- Chain sequences findings through prioritization, recommendation generation, and formatting.
- Evaluator-Optimizer scores the final output and refines it if quality is below threshold.
- Save CodeReviewOrchestrator.java.
Seeing compilation errors?
- Check that your import uses the wildcard com.example.codereviewagent.model.* to bring in all model records at once.
- Make sure your ReviewEvaluator class has the public inner record EvaluationResult with fields recommendations, score, and refinementNotes.
- Verify that all five service classes (ReviewRouter, ParallelReviewWorkflow, ReviewChainWorkflow, DynamicReviewOrchestrator, ReviewEvaluator) are annotated with @Service.
Help me fix compile errors in my CodeReviewOrchestrator
✔️ Awesome, I've got everything!
Great. Make sure you have saved the file before moving on.
ⓧ I'd like to double check the full code
package com.example.codereviewagent.service;
import com.example.codereviewagent.model.*;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class CodeReviewOrchestrator {
private final ReviewRouter router;
private final ParallelReviewWorkflow parallelWorkflow;
private final ReviewChainWorkflow chainWorkflow;
private final DynamicReviewOrchestrator dynamicOrchestrator;
private final ReviewEvaluator evaluator;
public CodeReviewOrchestrator(ReviewRouter router,
ParallelReviewWorkflow parallelWorkflow,
ReviewChainWorkflow chainWorkflow,
DynamicReviewOrchestrator dynamicOrchestrator,
ReviewEvaluator evaluator) {
this.router = router;
this.parallelWorkflow = parallelWorkflow;
this.chainWorkflow = chainWorkflow;
this.dynamicOrchestrator = dynamicOrchestrator;
this.evaluator = evaluator;
}
public CodeReviewResponse executeReview(CodeReviewRequest request) {
// 1. Routing: Classify the code
RouteDecision route = router.route(request.code(), request.language());
// 2. Parallelization: Run multi-dimensional analysis
List<ReviewFinding> findings = parallelWorkflow.analyzeInParallel(
request.code(), request.language(), route.category());
// 3. Orchestrator-Workers: Discover and fill gaps
List<ReviewFinding> additionalFindings = dynamicOrchestrator.discoverAndReview(
request.code(), request.language(), findings);
List<ReviewFinding> allFindings = new ArrayList<>(findings);
allFindings.addAll(additionalFindings);
// 4. Chain: Transform findings into recommendations
List<String> recommendations = chainWorkflow.chain(allFindings, request.code());
// 5. Evaluator-Optimizer: Refine until quality threshold met
ReviewEvaluator.EvaluationResult evaluationResult = evaluator.evaluate(recommendations, request.code());
// Build summary
String summary = "Reviewed %s code classified as %s. Found %d issues across %d dimensions."
.formatted(request.language(), route.category(), allFindings.size(),
allFindings.stream().map(ReviewFinding::category).distinct().count());
return new CodeReviewResponse(
summary,
route,
allFindings,
evaluationResult.recommendations(),
evaluationResult.score(),
evaluationResult.refinementNotes()
);
}
}
Start the application
Before Spring Boot can connect to OpenRouter, it needs your API key available as an environment variable. The application.yml already references ${OPENROUTER_API_KEY}, so you just need to export it in your terminal.
- Set your OpenRouter API key as an environment variable by running this command:
export OPENROUTER_API_KEY=your-api-key-here
- Replace your-api-key-here with your actual OpenRouter API key.
- Start the Spring Boot application by running:
./mvnw spring-boot:run
- Wait for the startup log to show Started CodeReviewAgentApplication and the port number (default 8080).
Windows users
On Windows, use set OPENROUTER_API_KEY=your-api-key-here in Command Prompt, or $env:OPENROUTER_API_KEY="your-api-key-here" in PowerShell. Use mvnw.cmd spring-boot:run instead of ./mvnw.
Application fails to start?
- If you see a bean creation error, check that every service class has the @Service annotation and accepts ChatClient.Builder in its constructor (not ChatClient directly).
- If you see an API key error, confirm you exported OPENROUTER_API_KEY in the same terminal session where you ran the Maven command.
Help me debug my Spring Boot startup failure
Test with real code samples
With the application running, you can send code snippets to the /api/review endpoint and watch the entire 5-pattern pipeline produce a structured review.
- Open a new terminal window (keep the application running in the first one).
- Test with a security-vulnerable code snippet by running this command:
curl -s -X POST http://localhost:8080/api/review \
-H "Content-Type: application/json" \
-d '{
"code": "public User findUser(String name) { String sql = \"SELECT * FROM users WHERE name = \u0027\" + name + \"\u0027\"; return jdbcTemplate.queryForObject(sql, new UserRowMapper()); }",
"language": "java",
"context": "User lookup service"
}' | python3 -m json.tool
What does this test do?
This snippet contains a classic SQL injection vulnerability. The router should classify it as SECURITY, and the parallel analysis should flag the string concatenation in the SQL query. The python3 -m json.tool at the end pretty-prints the JSON response so you can read it easily.
- You should see a JSON response containing a summary, route (with category SECURITY), findings (an array of issues), recommendations (actionable steps), and an overallScore.
Why does this request take a while?
The full pipeline makes 10-12 LLM calls in total (routing, 3 parallel analyses, gap discovery, workers, 3 chain steps, evaluation, and possible refinement). Expect the first request to take 30-60 seconds depending on model response times.
- Now test with a performance-heavy algorithm by running:
curl -s -X POST http://localhost:8080/api/review \
-H "Content-Type: application/json" \
-d '{
"code": "public List<Integer> findDuplicates(List<Integer> items) { List<Integer> dupes = new ArrayList<>(); for (int i = 0; i < items.size(); i++) { for (int j = i + 1; j < items.size(); j++) { if (items.get(i).equals(items.get(j)) && !dupes.contains(items.get(i))) { dupes.add(items.get(i)); } } } return dupes; }",
"language": "java",
"context": "Duplicate detection utility"
}' | python3 -m json.tool
- This snippet has O(n^2) nested loops and repeated contains() checks. The router should classify it as PERFORMANCE, and findings should flag the inefficient algorithm.
- Finally, test with a general utility snippet by running:
curl -s -X POST http://localhost:8080/api/review \
-H "Content-Type: application/json" \
-d '{
"code": "public String formatName(String first, String last) { return first.substring(0, 1).toUpperCase() + first.substring(1).toLowerCase() + \" \" + last.substring(0, 1).toUpperCase() + last.substring(1).toLowerCase(); }",
"language": "java",
"context": "Name formatting helper"
}' | python3 -m json.tool
- This simpler snippet should route as GENERAL. The review will still produce findings (missing null checks, no empty-string handling) but the severity should be lower overall.
Getting a 500 error or empty response?
- Check the Spring Boot terminal for the full stack trace. A common cause is the LLM returning a response that does not match the expected record structure for structured output.
- Verify your OpenRouter account has available credits. If your balance is zero, the API returns a 402 status.
- If you see a timeout, the model might be overloaded. Wait a moment and retry the curl command.
Help me debug my /api/review endpoint returning errors
Your Code Review Agent is fully operational. Every request now flows through all five agentic workflow patterns, producing reviews that are classified, analyzed in parallel, gap-filled, sequenced into recommendations, and quality-checked before reaching the caller. Next up, the Secret Mission adds real-time streaming so a frontend could show each stage as it executes.
Secret mission
Real-Time Streaming with Server-Sent Events
Your review agent works, but the client waits in the dark while the pipeline runs. What if you could stream each stage in real-time? Add a streaming endpoint that emits Server-Sent Events as each workflow pattern executes, showing progress from 0% to 100%.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine. OpenRouter only charges per API call, so there are no ongoing costs from leaving the project files on your computer. You only spend money when you actively send requests to the API.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The code-review-agent project folder on your Desktop.
- The running Spring Boot application (if still running in your terminal).
- Your OpenRouter API key.
Keep everything running
No action needed. Choose this if you're still actively building or want to continue experimenting with the code review agent.
- Your project folder stays on your Desktop. You can restart the application any time with mvn spring-boot:run.
- Your OpenRouter API key remains active. It only costs money when you send requests.
- Your OPENROUTER_API_KEY environment variable stays set in your terminal session.
Pause - I'll come back to this later
Shut down running processes to free up system resources while keeping your files intact.
- If the Spring Boot application is still running, press Ctrl+C in the terminal to stop it.
- Your project folder and all source files remain on your Desktop for later.
- Your OpenRouter API key stays active but costs nothing while idle.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- If the Spring Boot application is still running, press Ctrl+C in the terminal to stop it.
- Delete the project folder by running the command for your operating system.
macOS
cd ~/Desktop
rm -rf code-review-agent
Windows
cd ~\Desktop
rmdir /s /q code-review-agent
- Delete your OpenRouter API key by visiting your key settings page at openrouter.ai/settings/keys. Find the key you created for this project and delete it.
- Unset the environment variable in your current terminal session.
macOS
unset OPENROUTER_API_KEY
Windows
Remove-Item Env:OPENROUTER_API_KEY
Nice Work!
Nice Work!
You just built a production-grade AI orchestration system with Spring AI. This is not a simple chatbot or a single-prompt wrapper. It is a multi-strategy agent that classifies, analyzes, discovers, refines, and delivers structured code reviews through composable workflow patterns.
You learned how to:
- Built a Code Review Agent implementing all 5 agentic workflow patterns (Chain, Parallelization, Routing, Orchestrator-Workers, Evaluator-Optimizer) as composable Spring services.
- Implemented @Tool-annotated methods that the LLM calls autonomously during analysis, giving the model access to cyclomatic complexity scoring, pattern detection, and naming convention checks.
- Configured OpenRouter as an LLM provider through Spring AI's OpenAI-compatible client, producing structured JSON output with typed Java records.
- Secret Mission: Added a real-time streaming endpoint with Server-Sent Events that broadcasts each workflow stage as it executes.
Ready to quiz yourself?