trendingzones
← Back to the Intermediate level

JAVA + SPRING AI — ADVANCED

Agents Across Microservices: Cross-Service Tools, Sagas & Resilience4j

Topic 6’s Advanced level closed with a specific forward-reference: every tool shown across that topic was “a local, in-process Java method call,” and the next topic would explore “what changes when a tool’s own implementation is itself a network call to a differentmicroservice.” This is that topic’s payoff — and this pillar’s last stop.

The Quick Answer

Nothing about @Tool, .tools(...), or ToolCallingAdvisor changes — Spring AI has no separate mechanism for a “remote” tool versus a “local” one. What changes is only what happens inside the tool method: instead of a database lookup or in-memory computation, the method body makes a RestClientcall to a different microservice. That one change drags in everything topic 3 already taught about inter-service calls — a service with its own database, its own latency, its own chance of failing — except now the decision to make that call, and what to do next based on the result, comes from the model, not from a developer’s fixed code path. The Saga pattern from topic 3’s Advanced level still applies to keeping the agent’s actions consistent across those services, and Resilience4j’s @CircuitBreakeris the tool this page verifies for handling a cross-service tool call that’s failing or taking too long.

A Tool Whose Body Is a Network Call

Topic 6’s getOrderStatus and getCurrentWeathertools both did their real work in-process — a repository lookup, a call to some already-injected service, all inside the same running application. Here’s the same @Toolannotation, on a method whose entire body is instead a call to the Inventory Service from topics 3 and this topic’s Intermediate level:

public record StockLevel(String sku, int quantityAvailable) {}

@Component
public class InventoryTools {

    private final RestClient restClient;

    // Same RestClient-plus-service-discovery shape topic 3's Intermediate
    // level already taught for InventoryClient — this class is that same
    // idea, just reused as a tool's implementation instead of a plain
    // service-layer dependency.
    public InventoryTools(RestClient.Builder restClientBuilder) {
        this.restClient = restClientBuilder
            .baseUrl("http://inventory-service")
            .build();
    }

    @Tool(description = "Check whether a specific SKU currently has stock available")
    public StockLevel checkStock(
            @ToolParam(description = "The item SKU, e.g. 'SKU-4471'") String sku) {
        // The tool's entire "logic" is this one network call — nothing
        // computed locally, nothing read from this service's own database.
        // Inventory Service owns that data; this method just asks it.
        return restClient.get()
            .uri("/inventory/{sku}", sku)
            .retrieve()
            .body(StockLevel.class);
    }
}

Registering InventoryTools on a ChatClient call is identical to every registration this pillar has already shown — .tools(new InventoryTools(restClientBuilder)), or constructor-injected as a @Componentthe way it’s written above. The model reads checkStock’s description and schema exactly the way it read topic 6’s tools, decides to call it when a question needs live stock data, and ToolCallingAdvisor invokes it exactly the same way. From Spring AI’s point of view, this is an ordinary tool call. From the system’s point of view, it’s a real request crossing a real network boundary, with a real chance of not coming back on time.

The Coordination Challenge This Creates

Give an agent two tools like this — one calling Inventory Service, another calling Payment Service — and a task like “reserve this item and charge the customer,” and the exact problem topic 3’s Advanced level described for database-per-service shows up again, unchanged: Inventory Service can successfully reserve the stock, in its owndatabase, at the exact moment Payment Service fails to charge the card, in a completely separate database that Inventory Service’s success has no way of knowing about. Nothing here is new about distributed systems — it’s the identical “no shared transaction across two databases” problem topic 3 already covered. What’s new is only whodecided to call these two tools in this order: an LLM, mid-conversation, rather than a fixed method body a developer wrote in advance. The agent’s tool-call sequence isa multi-service operation, in exactly topic 3’s sense — it just wasn’t written down anywhere as a single, predetermined code path before the conversation started.

Applying the Saga Pattern to an Agent’s Own Actions

Topic 3’s Advanced level already described the Saga pattern in full: a sequence of local transactions, one per service, with compensating transactions available to undo whichever steps already succeeded if a later step fails — coordinated either by choreography(services publishing and reacting to each other’s events, no central coordinator) or orchestration(a dedicated coordinator explicitly directing each step). Nothing about that description changes here — an agent’s sequence of cross-service tool calls is exactly a saga in that same sense: local operations on separate services’ own databases, needing a way to undo earlier successes if a later step fails.

What’s worth being precise about is which of the two coordination styles fits more naturally here, and why. An agent, by construction, is already making one decision at a time and reacting to each tool’s result before deciding what happens next — it is, functionally, already playing the role of a step-by-step director. That maps far more directly onto orchestrationthan choreography: the agent’s own reasoning loop (call a tool, read the result, decide the next tool) plays the part topic 3’s saga orchestrator played — explicitly deciding what runs next, and, on a failure, explicitly choosing to call a compensating tool (a releaseReservedStock tool, say, undoing the Inventory Service reservation) rather than proceeding as if the payment had succeeded. A choreographed saga, by contrast, depends on services publishing events and reacting to each other’sevents with no central coordinator — which doesn’t map cleanly onto an agent’s tool calls at all, since the agent itself, not an event bus, is what’s deciding the sequence. This page is careful not to overstate this: it’s a natural fit given how an agent already operates, not a claim that Spring AI or Spring Cloud ship a dedicated, agent-specific saga implementation — the saga logic here is still something a developer designs, using the same ordinary compensating-tool idea, on top of topic 3’s already-verified Saga description.

Timeouts, Retries & Circuit Breakers: Resilience4j

A cross-service tool call needs the same protection any other inter-service call needs — a slow or failing Inventory Service shouldn’t be able to hang the whole conversation, or get hammered with repeated calls once it’s already struggling. Resilience4j— already introduced in topic 3’s Intermediate-level Fun Fact as the modern replacement for the now-superseded Hystrix — is the library this pillar reaches for here too, applied to a tool method’s body exactly the way it would apply to any other RestClient call.

Verified directly against Resilience4j’s own documentation and the Spring Cloud Circuit Breaker reference docs, there are genuinely two separate integration paths worth telling apart, since conflating them is an easy mistake:

This page teaches the first path — Resilience4j’s own annotation — since a single, method-level annotation on a tool’s implementation is the more direct fit for what an agent’s cross-service tool call needs:

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;

@Component
public class InventoryTools {

    private final RestClient restClient;

    public InventoryTools(RestClient.Builder restClientBuilder) {
        this.restClient = restClientBuilder
            .baseUrl("http://inventory-service")
            .build();
    }

    @Tool(description = "Check whether a specific SKU currently has stock available")
    @CircuitBreaker(name = "inventoryService", fallbackMethod = "stockCheckUnavailable")
    public StockLevel checkStock(
            @ToolParam(description = "The item SKU, e.g. 'SKU-4471'") String sku) {
        return restClient.get()
            .uri("/inventory/{sku}", sku)
            .retrieve()
            .body(StockLevel.class);
    }

    // Called instead of checkStock(...) once the circuit is open, or when
    // the underlying call fails — its result flows back to the model the
    // same way any @Tool return value does, so the agent can explain the
    // situation rather than the request simply hanging or crashing.
    public StockLevel stockCheckUnavailable(String sku, Throwable cause) {
        return new StockLevel(sku, -1); // agent-visible signal: "unknown, service unavailable"
    }
}

The failing-fast behavior itself — how many recent calls it takes before the circuit opens, how long it stays open before testing the service again — is configured the same way for any method carrying this annotation, tool or not: resilience4j.circuitbreaker.instances.inventoryService.slidingWindowSize and resilience4j.timelimiter.instances.inventoryService.timeoutDuration are both real, current configuration properties, verified against Resilience4j’s own reference docs. Retries stack the same way Resilience4j’s own documentation describes for any annotated method — a @Retry annotation alongside @CircuitBreaker, with Resilience4j applying them in a fixed, documented order (Retry wraps CircuitBreaker, which wraps RateLimiter, which wraps TimeLimiter, which wraps Bulkhead) — nothing about that ordering changes because the method happens to be a @Tool.

One detail is worth flagging honestly rather than glossing over, in the same spirit this pillar has verified version-specific details throughout: at the time of writing, resilience4j-spring-boot3 is the long-established, fully documented starter, and everything shown above works exactly as written on Spring Boot 3. A newer resilience4j-spring-boot4 artifact also exists on Maven Central (version 2.4.0), clearly intended for this pillar’s own Spring Boot 4.x baseline — but Resilience4j’s own primary getting-started documentation had not yet been updated to describe it at the time this page was verified, meaning the artifact has shipped somewhat ahead of its own reference docs catching up. The @CircuitBreaker annotation itself, its package, and the configuration property shapes above are not in question — only which exact starter artifact ID a Spring Boot 4 project should declare is worth a fresh check against Resilience4j’s own current docs at build time, rather than trusting either artifact name as permanently settled.

The Shape, Visually

One tool call, end to end — the agent’s decision loop from topic 6, now crossing a real service boundary partway through:

Agentdecides: call tool@Tool methodcheckStock(sku)NETWORK HOPInventoryServiceRestClient targetOwn databaseresponse flows back through the network hop, then to the agentFinalanswer

Try It: Step Through an Agent Acting Across Services

Pick a task below and step through an agent deciding to call a tool, that tool making a real network hop to a different microservice (with its own simulated latency), that service’s response flowing back, and the agent deciding its next step — then toggle “the second service times out” to see exactly where a cross-service tool call can leave earlier work stranded, the same failure this section’s Resilience4j content exists to handle:

Where This Pillar Ends

That’s the full arc this pillar set out to cover: Spring fundamentals (dependency injection and beans), building a real REST API (auto-configuration and @RestController), splitting a system into independently deployable microservices (service discovery, API gateways, database-per-service, and the Saga pattern), and Spring AI’s chat, RAG, and tool-calling capabilities — ending here, with how all of it combines once an agent’s own decisions start crossing service boundaries instead of staying inside one running application. Every idea in this final topic was already taught somewhere earlier in the pillar; this topic’s only real job was showing how they fit together when used at the same time.

This pillar’s own plan document leaves one related idea explicitly open, not confirmed: a possible future topic on building a standalone AI agent with Spring AI’s Advisor API and memory, independent of any microservices angle — the agent-building content covered so far has been specifically the microservices-spanning version in this topic, not a general-purpose treatment of agent-building on its own. Whether that gets built is, as of this writing, still an open possibility, not a scheduled next step.

Fun Fact

Every architectural idea this topic leaned on — microservices, service discovery, API gateways, database-per-service, the Saga pattern, circuit breakers — predates general-purpose LLM tool calling by years, most of them by over a decade. Nothing about adding an AI agent into a microservices system required inventing new distributed-systems theory; it required recognizing that an agent’s tool calls are just another kind of request crossing a service boundary, and that the tools already built for that problem still apply.

Test Yourself

What is genuinely new about a tool method whose implementation calls another microservice, compared to the local Java methods topic 6 taught?

Why does an agent’s tool-call step depending on a network call to another service raise the same concerns as topic 3’s database-per-service content?

How does this page describe the Saga pattern’s relevance to an agent’s actions across services?

What annotation and artifact does this page identify for adding a circuit breaker to an agent’s cross-service tool call, and what did verification turn up?

How does this page close out the pillar?