AI Engineering
Saleen Theeyons November 13, 2026

Building Cross-Language AI Agents with Spring AI, Google ADK, and A2A

Building Cross-Language AI Agents with Spring AI, Google ADK, and A2A

How Java and Python Agents Collaborate through a Standard Protocol

Enterprise AI rarely exists in a single language, making building cross-language AI agents essential for modern architectures. Java typically manages customer-facing APIs, identity, policy, and long-running business services. Python handles model experimentation, data tooling, and specialist agent workflows. Connecting the two with bespoke REST endpoints creates an unstandardized, private protocol that both teams must continuously maintain.

The Agent2Agent (A2A) protocol offers a more durable boundary. This article builds a Spring AI coordinator in Java that delegates supplier research to a Google Agent Development Kit (ADK) specialist in Python. The frameworks stay independent; the network contract becomes the integration point.

The Business Value

A standardized agent boundary transforms each new agent integration from a custom software project into a straightforward configuration task. The primary business benefits emerge in four key areas:

  • Faster delivery. One discovery and messaging contract replaces a private API for every agent pairing.
  • Independent releases. The Python team changes prompts, models, or tools without a Java release, as long as the published capability stays compatible.
  • Existing investment reused. Java keeps identity, audit, and policy; Python keeps its data tooling.
  • Less lock-in. A2A now sits under the Linux Foundation’s Agentic AI Foundation, with support from major clouds and SaaS platforms.

Use Cases

This integration pattern is suitable wherever a governed Java front door must invoke specialist reasoning owned by a separate team. When building cross-language AI agents, the primary scenario is detailed in the first row of the table below, while the additional use cases follow an identical structure.

DomainJava coordinator ownsPython specialist owns
Procurement (this example)Supplier assessment API, approval policySupplier risk research and signal scoring
BankingCustomer channels, KYC records, audit trailFraud and transaction-pattern analysis
InsuranceClaims intake and adjuster workflowDocument extraction and damage assessment
Healthcare operationsPatient scheduling and consent checksClinical-document summarization for staff review
Customer supportTicketing, entitlements, SLA rulesKnowledge-base retrieval and resolution drafting

In each case, the coordinator keeps the final decision and the human-review policy. The specialist supplies evidence, not verdicts.

Architecture

When a user requests a supplier assessment through the Java API, the Spring AI coordinator determines that external research is required and invokes the researchSupplier tool. The adapter reads the specialist’s Agent Card, dispatches an A2A message, and returns the result. Finally, the coordinator separates the remote findings from its own internal inference.

Spring AI ChatClient Model picks the tool

Only two steps involve model inference: the coordinator selecting the appropriate tool, and the specialist determining how to execute its domain tool. Discovery, endpoint selection, serialization, timeouts, and error handling remain in deterministic code, preventing the model from invoking arbitrary endpoints.

Why Building Cross-Language AI Agents Needs A2A Over Custom REST

While a standard REST endpoint eliminates runtime coupling, development teams must still define custom semantics for discovery, task states, streaming, artifacts, and error handling. The A2A protocol standardizes these core agent concepts while maintaining abstraction over remote implementations. Consequently, Spring AI only requires knowledge of the specialist’s capabilities and endpoint location, without needing visibility into its prompts, models, or state management.

Versions Used

The example was verified against the versions below as of early October 2026. Pin them in your build files; protocol SDKs and framework integrations move quickly.

ComponentVersionSource
Java21Required baseline for Spring AI 2.x
Spring Boot4.1.1Current stable, released 20 Aug 2026
Spring AI2.0.1Released 21 Aug 2026; 2.0.x designed for Spring Boot 4.0 and 4.1
A2A specification1.0Latest released version; stable since March 2026
Python3.12Any version supported by ADK
Google ADK2.9.2PyPI
A2A Python SDK1.1.4Implements A2A 1.0 with a 0.3 compatibility mode

You need an API key for the model behind Spring AI and a Google API key (or Google Cloud credentials) for Gemini. The supplier data is a local fixture, so no third-party research API is required.

Build the Python Specialist

The following implementation code is fully executable as presented. Install Google ADK alongside its A2A extension and pin the A2A SDK to ensure explicit transport versioning:

mkdir supplier-agent && cd supplier-agent
uv init --package --python 3.12
uv add "google-adk[a2a]==2.9.2" "a2a-sdk[http-server]==1.1.4" uvicorn

The underlying tool returns a deterministic fixture to allow testing of the bridge without external dependencies. The system prompt explicitly identifies this data, ensuring the model does not present demonstration fixtures as live operational research.

# src/supplier_agent/agent.py
from google.adk.agents import Agent
from google.adk.a2a.utils.agent_to_a2a import to_a2a

def lookup_supplier_signals(company: str, country: str) -> dict:
    """Return demonstration-only supplier signals."""
    return {
        "company": company, "country": country,
        "source": "demonstration fixture",
        "signals": [
            {"category": "operations", "finding": "Single regional warehouse"},
            {"category": "finance", "finding": "Payment terms not verified"},
            {"category": "compliance", "finding": "Human review required"},
        ],
    }

root_agent = Agent(
    name="supplier_research_agent",
    model="gemini-flash-latest",
    description="Researches supplier operating and compliance risk",
    instruction="""You are a supplier research specialist.
Use lookup_supplier_signals for every assessment.
State that demonstration fixtures are not live evidence.
Return findings, assumptions, and follow-up checks.
Do not make a procurement decision.""",
    tools=[lookup_supplier_signals],
)

# ADK builds the A2A server, task store, and Agent Card.
a2a_app = to_a2a(root_agent, port=8001)

Google ADK’s to_a2a() function encapsulates an existing agent as an A2A service and automatically generates its Agent Card. Because ADK 2.9.2 designates its A2A integration as experimental, explicit dependency versioning should be maintained. Start the service and verify discovery as follows:

export GOOGLE_API_KEY="replace-me"
uv run uvicorn supplier_agent.agent:a2a_app --host 0.0.0.0 --port 8001

# In another terminal
curl -H "A2A-Version: 1.0" http://localhost:8001/.well-known/agent-card.json

The card lists the agent’s skills, input and output modes, and its endpoints. Under A2A 1.0, endpoints appear in a supportedInterfaces array, each declaring its own protocol binding and version.

Build the Spring AI Coordinator

The Java implementation below is fully executable (with package declarations and imports omitted from the secondary snippet for brevity). To maintain clear architecture boundaries, the table below outlines framework-provided elements versus custom application code.

NameComes from
ChatClient, @Tool, @ToolParam, defaultTools, ToolCallingAdvisorSpring AI 2.0.1 (tool calling)
RestClient, JdkClientHttpRequestFactory, ParameterizedTypeReferenceSpring Framework 7 (via Spring Boot 4.1.1)
A2aResearchTool, resolveEndpoint, the JSON-RPC request mapApplication code in this article
SendMessage, ROLE_USER, supportedInterfaces, A2A-VersionWire-format names from the A2A 1.0 specification

The example does not depend on any A2A support inside Spring AI. It uses plain HTTP so every protocol detail is visible.

pom.xml (dependencies)

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>4.1.1</version>
</parent>
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>2.0.1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webmvc</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.ai</groupId>
        <artifactId>spring-ai-starter-model-openai</artifactId>
    </dependency>
</dependencies>

application.yml

spring.ai.openai:
  api-key: ${OPENAI_API_KEY}
  chat.model: ${OPENAI_MODEL}
agents.supplier:
  card-url: http://localhost:8001/.well-known/agent-card.json
  allowed-hosts: localhost

The OpenAI starter is only an example. Any Spring AI chat model that supports tools can drive the adapter.

The A2A Adapter Tool for Building Cross-Language AI Agents

Annotation-based tools in Spring AI are synchronous, so the adapter blocks at the tool boundary with fixed timeouts. Every request carries the A2A-Version: 1.0 header, which the A2A 1.0 specification (section 3.6.1) requires clients to send; a server that receives no header must assume version 0.3.

package com.example.coordinator;

import java.net.URI;
import java.net.http.HttpClient;
import java.time.Duration;
import java.util.*;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;

@Component
public class A2aResearchTool {
    private static final ParameterizedTypeReference<Map<String, Object>> JSON =
        new ParameterizedTypeReference<>() {};
    private final RestClient http;
    private final URI cardUri;
    private final Set allowedHosts;

    public A2aResearchTool(
        @Value("${agents.supplier.card-url}") URI cardUri,
        @Value("${agents.supplier.allowed-hosts}") Set allowedHosts) 
{ var factory = new JdkClientHttpRequestFactory( HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .build()); factory.setReadTimeout(Duration.ofSeconds(45)); this.http = RestClient.builder() .requestFactory(factory) .defaultHeader("A2A-Version", "1.0") .build(); this.cardUri = cardUri; this.allowedHosts = allowedHosts; } @Tool(description = "Research a supplier through the remote specialist agent") public String researchSupplier( @ToolParam(description = "Legal or trading company name") String company, @ToolParam(description = "Country of operation") String country) { Map<String, Object> request = Map.of( "jsonrpc", "2.0", "id", UUID.randomUUID().toString(), "method", "SendMessage", "params", Map.of("message", Map.of( "messageId", UUID.randomUUID().toString(), "role", "ROLE_USER", "parts", List.of(Map.of("text", "Assess supplier " + company + " in " + country))))); Map<String, Object> response = http.post().uri(resolveEndpoint()) .contentType(MediaType.APPLICATION_JSON) .body(request).retrieve().body(JSON);
if (response == null || response.containsKey("error")) { throw new IllegalStateException("A2A call failed: " + response); } return String.valueOf(response.get("result")); } /** Reads the Agent Card and returns a JSON-RPC endpoint on an allowed host. */ @SuppressWarnings("unchecked") private URI resolveEndpoint() { Map<String, Object> card = http.get().uri(cardUri).retrieve().body(JSON); var interfaces = (List<Map<String, Object>>) card.getOrDefault("supportedInterfaces", List.of()); return interfaces.stream() .filter(i -> "JSONRPC".equals(i.get("protocolBinding"))) .map(i -> URI.create((String) i.get("url"))) .filter(u -> allowedHosts.contains(u.getHost())) .findFirst() .orElseThrow(() ->new IllegalStateException("No allowed JSON-RPC endpoint")); } }

The host allowlist provides necessary security control: the model determines business parameters while deterministic code enforces target endpoints. For production implementations, this custom client should be replaced with the official A2A Java SDK, which provides strongly typed domain models, Agent Card caching, and comprehensive task state management.

Register the coordinator

@Configuration
class ChatConfiguration {
    @Bean
    ChatClient coordinator(ChatClient.Builder builder, A2aResearchTool researchTool) {
        return builder.defaultSystem("""
                You assess supplier risk for a human reviewer.
                Use researchSupplier before writing an assessment.
                Keep remote findings separate from your own inference.
                Never approve or reject a supplier.""")
            .defaultTools(researchTool)
            .build();
    }
}

@RestController
class AssessmentController {
    private final ChatClient coordinator;
    AssessmentController(ChatClient coordinator) {
        this.coordinator = coordinator;
    }

    record AssessmentRequest(String company, String country) {}

    @PostMapping("/api/assessments")
    Map<String, String> assess(@RequestBody AssessmentRequest req) {
        String answer = coordinator.prompt()
            .user("Assess " + req.company() + " in " + req.country())
            .call().content();
         return Map.of("assessment", answer);
    }
}

Spring AI’s auto-registered ToolCallingAdvisor runs the model-and-tool loop until the model returns a final answer. Spring AI 2.0.1 also adds a configurable cap on tool calls per request, worth setting in production.

Run and Debug

With the Python service running, start Spring and send one request:

export OPENAI_API_KEY="replace-me"
export OPENAI_MODEL="a-tool-capable-model"
./mvnw spring-boot:run

curl -X POST http://localhost:8080/api/assessments \
  -H "Content-Type: application/json" \
  -d '{"company":"Northwind Components","country":"India"}'

Although response phrasing varies by model, the final output should cite the demonstration data source, summarize the three fixture signals, and request human verification. If the coordinator issues a final procurement decision, refine the system instructions and implement automated policy checks.

During troubleshooting, invoke the Python agent directly to isolate transport and specialist errors from Spring AI tool selection issues. Target the endpoint URL specified in the Agent Card using the updated A2A 1.0 specification naming, where message/send has been renamed to SendMessage and the redundant kind field has been removed:

curl -X POST <JSONRPC-url-from-agent-card> \
  -H "Content-Type: application/json" \
  -H "A2A-Version: 1.0" \
  -d '{"jsonrpc":"2.0","id":"debug-1","method":"SendMessage",
       "params":{"message":{"messageId":"msg-1","role":"ROLE_USER",
       "parts":[{"text":"Assess Northwind in India"}]}}}'

The open-source A2A Inspector and Technology Compatibility Kit (TCK) offer dedicated tools for interactive protocol inspection and compliance validation during SDK upgrades.

Prepare for Production

A successful demonstration confirms baseline connectivity but does not guarantee operational resilience. Before introducing the adapter into production workflows, address and validate the following operational requirements:

ConcernPracticeTest that proves it
TrustTLS everywhere; read security schemes from the Agent Card but enforce a local allowlist; keep credentials out of prompts and message partsA card with an unlisted host is rejected
Timeouts and retriesSeparate deadlines for discovery and execution; retry discovery only; treat SendMessage as non-idempotent after a timeoutA retried message creates no duplicate business action
Long-running workStore the task ID behind a durable job; collect results with GetTask, SubscribeToTask, or push notificationsA TASK_STATE_WORKING result is persisted and resumed
Interrupted tasksSurface TASK_STATE_INPUT_REQUIRED questions to the userNo value is invented to fill the gap
ObservabilityPropagate trace context across Java, A2A, Python, and model calls; log agent name, task ID, latency, terminal stateOne trace spans both services
Untrusted outputTreat remote text as evidence, never as policy; validate structured fields before storing themInjected instructions in a reply are ignored
VersioningSend A2A-Version, pin SDKs, run a cross-language contract suite on every upgradeAn unsupported version returns a controlled error

When to Choose A2A

Adopt A2A when organisational independence is as critical as technical decoupling: distinct engineering teams manage each agent, specialists scale and deploy independently, and internal tools or prompts remain encapsulated. A2A complements the Model Context Protocol (MCP), which standardises agent-to-tool connections rather than inter-agent communication.

Skip it when the operation is deterministic with a stable API, or when both parts share a release cycle. A single lookup is a tool, not an agent.

Future Trends

Agent interoperability is moving from experiment to infrastructure. Five shifts are worth planning for:

  • Neutral governance. A2A joined the Agentic AI Foundation in August 2026, alongside MCP, reducing single-vendor risk.
  • Platform-native agents. The same announcement reports native support in Google Cloud, AWS Bedrock AgentCore, and Azure AI Foundry, and adoption by ServiceNow, Salesforce, Atlassian, and SAP.
  • Verifiable identity. A2A 1.0 defines Agent Card signing; signed cards will likely replace hand-kept allowlists as the first trust check.
  • Typed SDKs and tooling. Official SDKs cover six languages, and an A2A CLI shipped in October 2026. Hand-written adapters like this one become teaching tools.
  • Larger tool surfaces. Sending every tool definition on every request stops scaling as coordinators gain remote agents. Spring AI 2.0 already ships a tool-search advisor that exposes tools on demand.

Conclusion

Java and Python agents can effectively collaborate without sharing a common runtime or framework ecosystem. Spring AI manages orchestration, policy enforcement, and client delivery; Google ADK governs specialist reasoning and domain tools; and A2A provides the standardized protocol for discovery, messaging, task lifecycle management, and artifact transfer.

The adapter pattern demonstrated in this article establishes a baseline integration in approximately 60 lines of code. Enterprise deployments should extend this foundation with the official A2A Java SDK, mutual authentication, persistent task state, distributed tracing, strict schema validation, and contract testing suites. With these controls established, independent engineering teams can safely evolve cross-language agent systems against a stable specification.

This article was supported by AI-based research and writing, with Claude 5 assisting in the creation of text and images.

Author

Saleen-Theeyons

Saleen Theeyons is a Senior Software Engineer at CapeStart with 11+ years of experience in software engineering, specializing in Java, Spring Boot, microservices, and backend development. Passionate about Artificial Intelligence (AI) and Generative AI, he explores emerging technologies and how intelligent tools can enhance software development practices, improve productivity, and drive innovation. He enjoys experimenting with modern technologies and sharing practical insights through technical articles, focusing on the evolving landscape of software engineering and the growing impact of AI on application development.

FAQ

What is the Agent2Agent (A2A) protocol?

An open standard for communication between independent AI agents. It defines how agents publish capabilities, exchange messages and artifacts, and track tasks without exposing prompts, tools, or memory.

 No. The adapter uses plain Spring RestClient calls so every protocol detail is visible. For production, use the official A2A Java SDK.

The A2A 1.0 specification requires clients to send it. A server that receives no header treats the request as version 0.3, with older method names and field shapes.

A2A connects agents to other agents; MCP connects an agent to its tools and data. They are complementary: a specialist can use MCP internally while exposing one A2A capability.

Do not hold a public HTTP request open. Store the task ID behind a durable job, then collect the result with GetTask, SubscribeToTask, or push notifications.