Agentic AI Patterns for Enterprise Software

It is no surprise that software development is increasingly shaped by the integration of AI. As the technology matures, we are moving beyond simple, monolithic AI models or single shot API calls. Modern enterprise architectures are rapidly adopting agentic AI systems, which orchestrate multiple independent reasoning agents to accomplish complex, real-world tasks. This article explores these powerful patterns, covering the core concepts of agents, their coordination, orchestration models, and crucially how to implement them effectively in enterprise Java applications. 

Note that while there are many types of AI, this article focuses on interacting with Large Language Models (LLMs), commonly referred to as generative AI. 

It All Starts with a Single AI Service  

The first step in adopting AI in applications is typically to interact with a single LLM. This is already a nontrivial task because it requires a shift from deterministic to probabilistic thinking as LLMs inherently produce variable outputs. Many developers initially struggle with this because they are used to knowing exactly what their software will do for a given input. 

Despite this shift, AI adoption in software has steadily increased due to the many use cases where a probabilistic LLM can augment applications. Early adoption often occurred in experimental Python applications, but demand for production-ready AI in established enterprise systems has exploded over the last few years. Projects like LangChain4j and Spring AI emerged to address this need in the Java ecosystem, providing idiomatic libraries that abstract the complexity of interacting with LLMs. 

Thanks to these projects, enriching Java applications with AI capabilities has become much more straightforward. Prompting an LLM can now be almost as simple as adding an annotation. 

Of course, things become more complex in a production setting. Even a single model interaction probably requires: 

  • maintaining conversational memory
  • enforcing safety guardrails and other security features 
  • augmenting responses with proprietary data by using Retrieval Augmented Generation (RAG) 
  • enabling the LLM to call functions or tools, local or by using MCP for external actions
  • streaming capabilities to stream responses in real time
  • additional considerations like logging, tracing, (cloud-native) deployments, etc.   

These “traditional” AI integrations involve calling a single LLM within a rigid programmatic scope. This works well for many use cases but reaches its limits when tasks are complex, require specialized knowledge, or involve multiple steps and decisions. This is where agentic AI shifts the paradigm. 

The Path from a Single AI Service to Agentic Systems 

We’ve established that an AI service is essentially a specialized autonomous software entity that connects to an LLM and can execute actions. Agents are nearly identical in functionality but conceptually distinct: they are designed to participate in an agentic system composed of multiple agents orchestrated by deterministic workflows or even coordinated and supervised in a probabilistic way by another LLM.  

Each agent contributes a specialized capability, and together they work toward a shared objective. One additional distinction is that, while agents typically do interact with an LLM, they don’t have to. Some agents are simply a traditional software entity that can be executed by an agentic system. A ‘non-LLM backed’ agent would simply execute deterministic code in its body instead of relying on an LLM to provide a response.  

Tools, Function Calling, and MCP 

One way to build an agentic system is by providing a list of tools to an LLM. Tools allow the LLM to invoke external functions capable of taking action. You can add descriptions to clarify their purpose or provide parameters to supply input data. The LLM decides which tools to call and with which parameters. 

Anthropic introduced Model Context Protocol (MCP), a protocol to enable remote tool calling in a standard way. MCP allows you to create servers that expose deterministic or probabilistic functionality as tools. These MCP servers can be consumed by any MCP-compatible client, e.g. LangChain4j, Spring AI, Goose, LM Studio, or AI code assistants, without requiring knowledge of implementation details. 

Here is an example of a simple Quarkus-based weather MCP service: 

public class Weather { 
    @RestClient 
    WeatherClient weatherClient; 

    @Tool(description = "Get weather forecast for a location.") 
    String getForecast( 
        @ToolArg(description = "Latitude of the location") double latitude, 
        @ToolArg(description = "Longitude of the location") double longitude 
    ) { 
        return weatherClient.getForecast( 
            latitude, 
            longitude 
        ); 
    } 
} 

This service can be registered as an MCP server and used by any MCP-compatible client. The LLM can then discover and instruct the AI client to call the weather tool as part of its invocation. 

Tools enable a basic agentic system: the LLM receives a list of tools and autonomously decides which to call and in what order, and tools may invoke other AI services. 

This works fairly well with an advanced LLM, a well-crafted prompt, and a limited set of tools. However, as the number of tools grows, the LLM becomes increasingly less effective at determining the correct sequence of actions or which tools to call. 

Agentic AI Systems and Workflows 

Agentic workflows are a relatively new concept in enterprise AI. Unlike the previous approaches, workflows enable much more control over which agents should run, in what sequence, and under what conditions. 

Agentic orchestration patterns range from highly controlled deterministic workflows to flexible autonomous systems. Each pattern offers different tradeoffs between control, flexibility, and complexity. 

Note: The examples below use a car rental fleet management system as a running scenario. The source code for this scenario as well as a further practical deep dive into this material can be found in the Quarkus LangChain4j Workshop

Rigid Workflows: Predictable and Controlled 

Rigid workflows offer maximum control and predictability. You define exactly which agents run, in what order, and under what conditions. This approach is especially valuable when execution sequences must be guaranteed. 

Although these patterns may seem novel in their implementation, they mirror the same familiar paradigms used in traditional software development. 

Sequential Execution 

Sequential execution runs agents one after another in a predetermined order. Each agent’s output becomes available to subsequent agents. 

Example: 

@SequenceAgent( 
    outputKey = "carConditions", 
    subAgents = { CleaningAgent.class, CarConditionFeedbackAgent.class } 
) 
CarConditions processCarReturn( 
    String carMake, 
    String carModel, 
    Integer carYear, 
    Long carNumber, 
    String carCondition, 
    String rentalFeedback, 
    String cleaningFeedback
); 

Here, CleaningAgent is invoked first, then CarConditionFeedbackAgent.  

This pattern is deterministic and easy to understand, but it lacks flexibility: every invocation follows the same sequence regardless of actual needs. 

Parallel Execution 

When tasks are independent, running agents in parallel improves efficiency. You can then potentially aggregate results and use the output for further processing. 

Example:

@ParallelAgent( 
    outputKey = "feedbackResult", 
    subAgents = { CleaningFbAgent.class, MaintenanceFbAgent.class } 
) 

String analyzeFeedback( 
    String carMake, 
    String carModel, 
    Integer carYear, 
    Long carNumber, 
    String carCondition, 
    String rentalFeedback, 
    String cleaningFeedback, 
    String maintenanceFeedback 
);

In this example, CleaningFbAgent and MaintenanceFbAgent are called at the same time, and the combined result is stored in the feedbackResult key.  

Parallel execution significantly reduces processing time for LLM-based tasks, where network latency often dominates. 

Conditional Routing 

Conditional routing adds flexibility by selecting which agent to call at runtime based on given conditions: 

Example:

@ConditionalAgent( 
    outputKey = "analysisResult", 
    subAgents = { MaintenanceAgent.class, CleaningAgent.class } 
) 
String processAction( 
    String carMake, 
    String carModel, 
    Integer carYear, 
    Long carNumber, 
    String carCondition, 
    String cleaningRequest, 
    String maintenanceRequest 
); 

@ActivationCondition(MaintenanceAgent.class) 
static boolean assignToMaintenance(Boolean needsMaintenance) { 
    return needsMaintenance; 
}  

@ActivationCondition(CleaningAgent.class) 
static boolean assignToCleaning(Boolean needsCleaning) { 
    return needsCleaning); 
} 

In this example, MaintenanceAgent is ‘activated’ if needsMaintenance is true, and otherwise CleaningAgent is activated if the needsCleaning variable is set to true.   

This pattern balances flexibility with explicit control and is ideal when clear business rules dictate actions. 

Autonomous Agentic AI Patterns for More Flexibility 

So far, we’ve seen rigid workflows where we determine which agents should be called and in what order. When the flow cannot be fully predetermined, autonomous agentic AI patterns allow an AI agent to call other agents based on its own LLM-backed determination.  

Supervisor Pattern 

The supervisor pattern strikes a balance between rigid control and autonomy. A supervisor agent receives a high-level goal and a list of worker agents, and then decides which agents to invoke, in what order, and with what parameters. In the following diagram, you can see that the decision logic needs to be more flexible to determine the flow of agents to be called and the arguments to be passed.

In code, it would look like this: 

@SystemMessage(""" 
    You are a fleet supervisor for a car rental company.
    DECISION LOGIC:     
    <<< omitted for brevity >>>  
    Explain your decision-making clearly, including which agents 
    you invoked and why. 
    """) 
@SupervisorAgent( 
    outputKey = "supervisorDecision", 
    subAgents = { 
        PricingAgent.class, 
        DispositionAgent.class, 
        MaintenanceAgent.class, 
        CleaningAgent.class 
    } 
) 
String superviseCarProcessing( 
    String carMake, 
    String carModel, 
    Integer carYear, 
    Long carNumber, 
    String carCondition, 
    String rentalFeedback, 
    String cleaningFeedback, 
    String maintenanceFeedback, 
    String cleaningRequest, 
    String maintenanceRequest, 
    String dispositionRequest 
); 

In this example SupervisorAgent receives feedback analysis results and decides which action agents to invoke. For a car with severe damage, it might: 

  1. Invoke PricingAgent to estimate the vehicle’s value 
  2. Invoke DispositionAgent to decide whether to scrap, sell, donate, or keep the car 
  3. If keeping the car, invoke MaintenanceAgent and/or CleaningAgent as needed

A detailed system message defines the decision logic. The supervisor can explain its reasoning, providing transparency. New worker agents can be added without modifying orchestration logic. 

However, this pattern is less deterministic, and prompt engineering becomes more important because the instructions in the system and/or user message are critical for the agent to determine which actions should be taken and in what order. 

Custom Agentic Patterns: Goal-Oriented Workflows 

While earlier patterns rely on LangChain4j annotations, sometimes a more custom or flexible flow is needed. In that case, you can create custom planners that compute actions dynamically based on goals and state. 

Goal-oriented workflows flip the traditional model of defining exact steps. Instead, you define the goal and let the system figure out how to achieve it. This approach is particularly powerful when dealing with complex state spaces where manually defining all paths becomes impractical. 

The Planner Interface 

A core building block for creating custom orchestration patterns in LangChain4j is the Planner interface. It allows you to decide at runtime how agents should behave, which ones to invoke, and when the workflow should conclude. By implementing methods such as nextAction(), which selects the appropriate agent based on the current state, and done(), which signals when the objective has been achieved, developers gain fine‑grained control over complex interactions. This low‑level approach opens the door to advanced techniques such as goal‑oriented planning and peer‑to‑peer agent communication, enabling highly adaptable agentic systems. 

Think of the planner as the conductor of an orchestra: it doesn’t play the instruments (agents) itself but decides when each should perform and how they should work together. This separation of orchestration logic from agent logic makes complex workflows manageable and testable. 

GOAP (Goal-Oriented Action Planning) 

GOAP is a planning technique where you define the current state, the desired goal state, and a set of available actions. The planner then determines the sequence of actions needed to reach the goal using a dependency graph-based approach. 

Using the Planner interface, each action’s preconditions and effects are evaluated to construct this graph, and then the graph search is used to determine the most efficient sequence of actions to achieve the goal. 

PeerToPeer (P2P) Agent Communication 

Another goal-oriented approach is peer-to-peer agent communication, where agents can directly invoke other agents as needed. Rather than following a predetermined workflow, agents make autonomous decisions about which other agents to consult. 

This pattern is a custom implementation that requires you to design how agents discover and communicate with each other. It’s particularly useful for complex domains where the required expertise depends on the specific situation. An agent might start working on a task, realize it needs specialized knowledge, and delegate to an appropriate peer agent. 

The key difference from rigid workflows is that the routing decisions happen at runtime based on the agents’ reasoning, not based on predefined rules. This provides maximum flexibility but requires careful design to prevent infinite loops or inefficient agent chains. 

Additional Agentic Features  

So far in this article we have covered quite a few patterns to control the flow of which agents should be called, and when they should be called. In addition to these agentic workflow patterns, there are a few more patterns that are important to keep in mind as you build your agentic AI system. Two of these are the AgentToAgent protocol to create distributed agents, and the HumanInTheLoop pattern, allowing for user input during the execution of the workflow. 

A2A Communication: Distributed Intelligence 

As systems grow, especially in an enterprise context, distributing agents into separate services becomes useful. For this, Google introduced the Agent2Agent (A2A) protocol. Just like in ‘classic’ distributed architectures, it allows for running agents remotely that can be developed and scaled independently, with advantages such as better isolation, reduced coupling, and reusability across different agentic systems.  

Example: 

@A2AClientAgent( 
    a2aServerUrl = "http://localhost:8888", 
    outputKey = "dispositionAction", 
    description = "Car disposition specialist." 
) 
String processDisposition( 
    String carMake, 
    String carModel, 
    Integer carYear, 
    Integer carNumber, 
    String carCondition, 
    String carValue, 
    String rentalFeedback 
);

In this case, the remote agent simply runs locally on a different port but behaves exactly the same as if it were a regular agent.  

Human-In-The-Loop: Keeping USERS in Control 

Human oversight is essential for high stakes decisions, regulatory compliance, or simply to get user input to determine what to do next. LangChain4j’s @HumanInTheLoop annotation allows workflows to receive user input during their invocation. These can be either synchronous/blocking, or asynchronous. 

Example: 

@HumanInTheLoop( 
    description = "Request human approval for vehicle disposition", 
    outputKey = "humanDecision", 
    async = false 
) 
static void requestApproval( 
    Long carNumber, 
    String carMake, 
    String carModel, 
    Integer carYear, 
    String proposedDisposition,
    String dispositionReason, 
    String carValue 
) { 
    request.set(String.format( 
        """ 
        DISPOSITION APPROVAL REQUIRED 
        Vehicle: %d %s %s (Car #%d) 
        Estimated Value: %s 
        Proposed Action: %s 
        Reason: %s 
        Do you approve this disposition? (APPROVED/REJECTED) 
        """, 
        carYear, carMake, carModel, carNumber, 
        carValue, proposedDisposition, dispositionReason 
    )); 
}

The workflow pauses until the user has decided whether to approve or reject the disposal of a car based on its value and the recommendation from the AI model. 

LangChain4j and Quarkus 

LangChain4j provides a comprehensive framework for implementing these agentic AI patterns in Java applications. The library offers two approaches: a declarative API using annotations and a programmatic API using builders. Both approaches are effective, and you can choose based on your preferences and use case. 

While LangChain4j already provides a powerful and flexible toolkit for building AI-powered applications, it requires manual wiring, configuration, and lifecycle management. This is where the Quarkus LangChain4j extension is useful for enterprise Java developers. The extension transforms LangChain4j from a library into a first-class Quarkus citizen, providing enterprise-grade integration that dramatically improves developer productivity. 

The Quarkus extension offers declarative AI services through CDI and annotations, eliminating boilerplate code and making AI integration as simple as defining an interface. It provides native support for multiple model providers including OpenAI, Gemini, Hugging Face, Ollama, and others, with centralized configuration through the application.properties configuration file. Build-time optimization validates your AI service definitions and configurations, catching errors early in the development cycle.

Built-in observability features provide comprehensive logs, metrics, and distributed traces out of the box, while Dev Services automatically provision LLM infrastructure during development. The Dev UI offers real-time insights into your AI interactions, and automatic chat memory management handles conversation context without manual intervention. 

Importantly, you’re not locked into a purely declarative approach. The extension allows you to seamlessly mix declarative @RegisterAiService annotations with low-level LangChain4j APIs when you need fine-grained control. This gives you the best of both worlds: high productivity through conventions and declarative configuration, with the flexibility to drop down to programmatic APIs when your use case demands it.  

Conclusion 

Agentic AI represents a fundamental shift in how we build intelligent systems. Instead of relying on a single monolithic model, we orchestrate specialized agents working collectively toward shared goals. This approach offers flexibility, maintainability, and the capability to solve complex real-world problems. 

LangChain4j makes these patterns accessible to Java developers, enabling enterprise-grade agentic applications with familiar tools and strong ecosystem support. 

Start simple with rigid workflows. As requirements evolve, introduce more autonomous patterns. The goal is not maximum autonomy but the right level for your use case. 

The future of enterprise software will increasingly involve collaborative AI systems—and by understanding these patterns today, you’re preparing your applications for that future. 

Interested in AI-Powered Java Performance Diagnostics?
Kevin Dubois is speakers at JCON.
This article introduces the key concepts and patterns behind agentic AI systems in enterprise Java — and his JCON talk shows how to apply these ideas to build AI-infused applications with LangChain4j and Quarkus.
Couldn’t join live? The session video will be available after the conference – worth checking out!

Total
0
Shares
Previous Post

The Inconvenient Truth About Code, 30 Years of Java & Sovereign AI – The Keynotes of #JCON2026

Next Post

Architect Your Own Experience: Creating Your Individual JCON 2026 Journey

Related Posts