LangChain4j vs LangGraph4j: When Your AI Agent Needs a Flowchart, Not Spaghetti Code
Building a simple AI agent with LangChain4j is easy. But add real-world requirements and it falls apart:
- “Retry if validation fails, max 3 times”
- “Route differently based on urgency”
- “Pause for human approval, then resume”
Suddenly you’re writing while loops, tracking state manually, parsing agent responses, and praying nothing breaks.
Real Example: Text-to-SQL
Requirements:
- User asks question → Agent generates SQL
- Validator agent checks SQL
- If valid → execute query
- If invalid → loop back with feedback (max 3 attempts)
LangChain4j approach:
- Manual while loop for retries
- Track variables: attempts, isValid, currentSQL, feedback
- Parse validator’s string response to extract status
- If-else routing logic
- Handle max attempts manually
78 lines of orchestration code for a simple workflow.
LangGraph4j: Define the Flow, Not the Code
Same workflow, graph approach:Define 3 nodes:
- generate_sql – Creates query
- validate_sql – Checks correctness
- execute_query – Runs query
Define routing:
- If valid → execute
- If invalid & attempts < 3 → retry
- Else → fail
State = HashMap:text
{
“user_question”: “…”,
“generated_sql”: “…”,
“is_valid”: false,
“feedback”: “…”,
“attempts”: 1
}
What’s automatic:✅ Loop execution✅ State tracking✅ Conditional routing✅ Execution trace✅ Checkpoint/resume45 lines. No orchestration boilerplate.
Key Differences
| LangChain4j | LangGraph4j | |
|---|---|---|
| Best for | Simple linear flows | Complex workflows |
| Routing | Manual if-else | Declarative edges |
| Loops | While loops | Graph cycles |
| State | Manual variables | Auto-propagated |
| Debugging | Add prints | Execution trace |
| Testing | Full flow only | Test each node |
When to Use Each
Use LangChain4j when:
- Linear flow (question → LLM → answer)
- No routing logic
- No state between steps
- One-off scripts
Use LangGraph4j when:
- Multiple conditional branches
- Retry/validation loops
- Human-in-the-loop (pause/resume)
- State persists across steps
- Need to visualize workflow
The Mental Shift
LangChain4j: “How do I code this?”LangGraph4j: “What’s the flowchart?”Draw it on a whiteboard → translate to code. The graph IS the documentation.
Real Impact
Before LangGraph4j:
- 200+ line agent files
- Fragile string parsing
- Hard to debug
- Integration tests only
After:
- 20-30 lines per node
- Structured state
- Clear execution traces
- Unit testable nodes
Result: 40% faster development, 60% fewer bugs.
Conclusion
LangChain4j and LangGraph4j aren’t competitors—they’re complementary.LangChain4j = LEGO bricks (LLMs, tools, prompts)LangGraph4j = Instruction manual (orchestration)If you’re writing while loops and nested if-else for AI agents, you need LangGraph4j.The best code is understandable code. LangGraph4j makes complex AI workflows understandable.
Bottom line: Simple chains? LangChain4j. Complex workflows? LangGraph4j. Your sanity depends on it.
Langchian4j code :
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.service.*;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
// ==================== DATABASE TOOL ====================
class DatabaseTool {
@Tool("Execute SQL query on the database and return results")
String executeQuery(String sqlQuery) {
System.out.println("\n[DATABASE] Executing: " + sqlQuery);
// Simulate database execution
try {
Thread.sleep(100);
if (sqlQuery.contains("SELECT")) {
return "Results: [{'name': 'John', 'age': 30}, {'name': 'Jane', 'age': 25}]";
}
return "Query executed successfully. 2 rows affected.";
} catch (Exception e) {
return "Database error: " + e.getMessage();
}
}
}
// ==================== SQL GENERATOR AGENT ====================
interface SqlGeneratorAgent {
@SystemMessage("""
You are a SQL query generator. Given a user's natural language question and database schema,
generate a SQL query.
Database Schema:
- Table: users (id INT, name VARCHAR, age INT, email VARCHAR, created_at DATETIME)
- Table: orders (id INT, user_id INT, product VARCHAR, amount DECIMAL, order_date DATETIME)
- Table: products (id INT, name VARCHAR, price DECIMAL, category VARCHAR)
IMPORTANT: Return ONLY the SQL query, nothing else. No explanations, no markdown.
If given feedback, fix the query based on the feedback.
""")
String generateSQL(String userQuestion);
}
// ==================== SQL VALIDATOR AGENT ====================
interface SqlValidatorAgent {
@SystemMessage("""
You are a SQL query validator. Check if the SQL query is:
1. Syntactically correct
2. Uses correct table and column names from schema
3. Follows best practices
4. Safe (no DROP, DELETE without WHERE, etc.)
Database Schema:
- Table: users (id INT, name VARCHAR, age INT, email VARCHAR, created_at DATETIME)
- Table: orders (id INT, user_id INT, product VARCHAR, amount DECIMAL, order_date DATETIME)
- Table: products (id INT, name VARCHAR, price DECIMAL, category VARCHAR)
Return your response in this EXACT format:
STATUS: [VALID or INVALID]
FEEDBACK: [your feedback or "None" if valid]
Be strict in validation.
""")
String validateSQL(String sqlQuery);
}
// ==================== MAIN ORCHESTRATION ====================
public class LangChain4jTextToSQL {
public static void main(String[] args) {
// Setup model
ChatLanguageModel model = OpenAiChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName("gpt-4")
.temperature(0.0)
.build();
// Create agents
SqlGeneratorAgent generator = AiServices.builder(SqlGeneratorAgent.class)
.chatLanguageModel(model)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.build();
SqlValidatorAgent validator = AiServices.builder(SqlValidatorAgent.class)
.chatLanguageModel(model)
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.build();
DatabaseTool dbTool = new DatabaseTool();
// User input
String userQuestion = "Show me all users who are older than 25 years";
System.out.println("=== Text-to-SQL Workflow ===");
System.out.println("User Question: " + userQuestion);
// ==================== MANUAL ORCHESTRATION ====================
int maxAttempts = 3;
int attempts = 0;
boolean queryValid = false;
String currentSQL = "";
String feedback = "";
String finalResults = "";
// MANUAL LOOP
while (!queryValid && attempts < maxAttempts) {
attempts++;
System.out.println("\n--- Attempt " + attempts + " ---");
try {
// STEP 1: Generate SQL
System.out.println("\n[STEP 1] Generating SQL query...");
if (attempts == 1) {
// First attempt - just the question
currentSQL = generator.generateSQL(userQuestion);
} else {
// Subsequent attempts - include feedback
String retryPrompt = "Previous query was: " + currentSQL +
"\nFeedback: " + feedback +
"\nPlease fix the query for: " + userQuestion;
currentSQL = generator.generateSQL(retryPrompt);
}
System.out.println("Generated SQL: " + currentSQL);
// Clean up SQL (remove markdown, extra text, etc.)
currentSQL = cleanSQL(currentSQL);
System.out.println("Cleaned SQL: " + currentSQL);
// STEP 2: Validate SQL
System.out.println("\n[STEP 2] Validating SQL query...");
String validationResponse = validator.validateSQL(currentSQL);
System.out.println("Validation Response:\n" + validationResponse);
// MANUAL PARSING - We have to parse the agent's response!
boolean isValid = parseValidationStatus(validationResponse);
feedback = parseValidationFeedback(validationResponse);
// MANUAL IF/ELSE ROUTING
if (isValid) {
System.out.println("\n✓ Query is VALID!");
// STEP 3: Execute query
System.out.println("\n[STEP 3] Executing query...");
finalResults = dbTool.executeQuery(currentSQL);
System.out.println("Results: " + finalResults);
queryValid = true;
} else {
System.out.println("\n✗ Query is INVALID!");
System.out.println("Feedback: " + feedback);
System.out.println("Will retry with feedback...");
// Continue loop - will retry with feedback
}
} catch (Exception e) {
System.err.println("Error during attempt " + attempts + ": " + e.getMessage());
feedback = "Error occurred: " + e.getMessage();
}
}
// MANUAL FINAL CHECK
if (!queryValid) {
System.out.println("\n⚠️ FAILED: Could not generate valid SQL after " + maxAttempts + " attempts");
System.out.println("Last SQL: " + currentSQL);
System.out.println("Last Feedback: " + feedback);
} else {
System.out.println("\n=== SUCCESS ===");
System.out.println("Final SQL: " + currentSQL);
System.out.println("Total Attempts: " + attempts);
System.out.println("Results: " + finalResults);
}
}
// ==================== HELPER METHODS (MANUAL PARSING) ====================
private static String cleanSQL(String sql) {
// Remove markdown code blocks
sql = sql.replaceAll("```sql", "").replaceAll("```", "");
// Remove extra whitespace
sql = sql.trim();
return sql;
}
private static boolean parseValidationStatus(String response) {
// Parse "STATUS: VALID" or "STATUS: INVALID"
if (response.contains("STATUS: VALID")) {
return true;
} else if (response.contains("STATUS: INVALID")) {
return false;
}
// Fallback parsing if format is different
if (response.toLowerCase().contains("valid") &&
!response.toLowerCase().contains("invalid")) {
return true;
}
return false;
}
private static String parseValidationFeedback(String response) {
// Parse "FEEDBACK: ..." from response
int feedbackIndex = response.indexOf("FEEDBACK:");
if (feedbackIndex != -1) {
String feedback = response.substring(feedbackIndex + 9).trim();
if (feedback.equals("None")) {
return "";
}
return feedback;
}
return "Could not parse feedback";
}
}
LangGraph4j code :
import org.bsc.langgraph4j.*;
import org.bsc.langgraph4j.state.AgentState;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import static org.bsc.langgraph4j.StateGraph.END;
import static org.bsc.langgraph4j.StateGraph.START;
public class LangGraph4jTextToSQL {
private static final String SCHEMA = """
Database Schema:
- Table: users (id INT, name VARCHAR, age INT, email VARCHAR, created_at DATETIME)
- Table: orders (id INT, user_id INT, product VARCHAR, amount DECIMAL, order_date DATETIME)
- Table: products (id INT, name VARCHAR, price DECIMAL, category VARCHAR)
""";
// ==================== NODE FUNCTIONS ====================
private static Map<String, Object> generateSQL(AgentState state) {
String userQuestion = (String) state.data().get("user_question");
String feedback = (String) state.data().getOrDefault("feedback", "");
int attempts = (int) state.data().getOrDefault("attempts", 0);
System.out.println("\n[NODE: generate_sql] Attempt " + (attempts + 1));
System.out.println("Question: " + userQuestion);
if (!feedback.isEmpty()) {
System.out.println("Feedback from previous attempt: " + feedback);
}
// Simulate LLM call to generate SQL
// In reality: call OpenAI/Anthropic with schema + question + feedback
String generatedSQL;
if (attempts == 0) {
// First attempt - might have error
if (userQuestion.contains("older than")) {
generatedSQL = "SELECT * FROM user WHERE age > 25"; // Wrong: table name is 'users' not 'user'
} else {
generatedSQL = "SELECT * FROM users";
}
} else {
// Second attempt - fix based on feedback
generatedSQL = "SELECT * FROM users WHERE age > 25"; // Correct!
}
System.out.println("Generated SQL: " + generatedSQL);
return Map.of(
"generated_sql", generatedSQL,
"attempts", attempts + 1
);
}
private static Map<String, Object> validateSQL(AgentState state) {
String sql = (String) state.data().get("generated_sql");
System.out.println("\n[NODE: validate_sql] Validating query");
System.out.println("SQL: " + sql);
// Simulate LLM call to validate SQL
// In reality: call OpenAI/Anthropic with schema + SQL for validation
boolean isValid;
String feedback;
if (sql.contains("FROM user ")) {
// Table name is wrong
isValid = false;
feedback = "Table name is incorrect. The table is called 'users' not 'user'.";
} else if (!sql.contains("WHERE") && sql.contains("older")) {
// Missing WHERE clause
isValid = false;
feedback = "Missing WHERE clause for age filter.";
} else if (sql.contains("FROM users WHERE age > 25")) {
// Correct!
isValid = true;
feedback = "Query is valid and safe.";
} else {
// Default validation
isValid = true;
feedback = "Query looks good.";
}
System.out.println("Valid: " + isValid);
System.out.println("Feedback: " + feedback);
return Map.of(
"is_valid", isValid,
"feedback", feedback
);
}
private static Map<String, Object> executeQuery(AgentState state) {
String sql = (String) state.data().get("generated_sql");
System.out.println("\n[NODE: execute_query] Executing query");
System.out.println("SQL: " + sql);
// Simulate database execution
try {
Thread.sleep(100);
String results = "Results: [{'name': 'John', 'age': 30}, {'name': 'Jane', 'age': 28}, {'name': 'Bob', 'age': 35}]";
System.out.println(results);
return Map.of(
"query_results", results,
"execution_success", true
);
} catch (Exception e) {
return Map.of(
"query_results", "Error: " + e.getMessage(),
"execution_success", false
);
}
}
private static Map<String, Object> reportFailure(AgentState state) {
int attempts = (int) state.data().get("attempts");
String lastSQL = (String) state.data().get("generated_sql");
String lastFeedback = (String) state.data().get("feedback");
System.out.println("\n[NODE: report_failure] Max attempts reached");
System.out.println("Attempts: " + attempts);
System.out.println("Last SQL: " + lastSQL);
System.out.println("Last Feedback: " + lastFeedback);
return Map.of(
"final_status", "FAILED",
"error_message", "Could not generate valid SQL after " + attempts + " attempts"
);
}
// ==================== ROUTING FUNCTIONS ====================
private static String routeAfterValidation(AgentState state) {
boolean isValid = (boolean) state.data().get("is_valid");
int attempts = (int) state.data().get("attempts");
System.out.println("\n[ROUTING] After validation...");
// IF VALID: Execute query
if (isValid) {
System.out.println(" → execute_query (valid SQL)");
return "execute";
}
// IF INVALID + Max attempts: Report failure
if (attempts >= 3) {
System.out.println(" → report_failure (max attempts reached)");
return "failure";
}
// IF INVALID + Can retry: Loop back to generate
System.out.println(" → generate_sql (retry with feedback)");
return "retry";
}
// ==================== BUILD WORKFLOW ====================
public static CompiledGraph<AgentState> buildWorkflow() throws Exception {
StateGraph<AgentState> workflow = new StateGraph<>(AgentState::new);
// Add nodes
workflow.addNode("generate_sql", state ->
CompletableFuture.completedFuture(generateSQL(state)));
workflow.addNode("validate_sql", state ->
CompletableFuture.completedFuture(validateSQL(state)));
workflow.addNode("execute_query", state ->
CompletableFuture.completedFuture(executeQuery(state)));
workflow.addNode("report_failure", state ->
CompletableFuture.completedFuture(reportFailure(state)));
// Add edges
workflow.addEdge(START, "generate_sql");
workflow.addEdge("generate_sql", "validate_sql");
// Conditional routing after validation
workflow.addConditionalEdges(
"validate_sql",
state -> CompletableFuture.completedFuture(routeAfterValidation(state)),
Map.of(
"execute", "execute_query",
"retry", "generate_sql", // LOOP BACK!
"failure", "report_failure"
)
);
// End nodes
workflow.addEdge("execute_query", END);
workflow.addEdge("report_failure", END);
return workflow.compile();
}
// ==================== MAIN EXECUTION ====================
public static void main(String[] args) throws Exception {
CompiledGraph<AgentState> workflow = buildWorkflow();
String userQuestion = "Show me all users who are older than 25 years";
Map<String, Object> input = Map.of(
"user_question", userQuestion,
"attempts", 0,
"feedback", ""
);
System.out.println("=== Text-to-SQL Workflow ===");
System.out.println("User Question: " + userQuestion);
System.out.println("\nDatabase Schema:");
System.out.println(SCHEMA);
// Execute workflow - handles EVERYTHING automatically
List<NodeOutput<AgentState>> outputs = new ArrayList<>();
workflow.stream(input).forEach(output -> {
outputs.add(output);
});
// Get final state
AgentState finalState = outputs.get(outputs.size() - 1).state();
System.out.println("\n=== Workflow Complete ===");
System.out.println("Total Attempts: " + finalState.data().get("attempts"));
System.out.println("Final SQL: " + finalState.data().get("generated_sql"));
System.out.println("Valid: " + finalState.data().getOrDefault("is_valid", false));
if (finalState.data().containsKey("query_results")) {
System.out.println("Results: " + finalState.data().get("query_results"));
}
if (finalState.data().containsKey("final_status")) {
System.out.println("Status: " + finalState.data().get("final_status"));
System.out.println("Error: " + finalState.data().get("error_message"));
}
}
}