Multi-Tenancy
CDI-based multitenancy support for A2A Java SDK servers — per-tenant AgentExecutor and AgentCard routing with the @Tenant qualifier.
Multi-Tenancy
Lets a single A2A server serve multiple tenants with different agent behavior — each tenant gets its own AgentExecutor and AgentCard, while requests without a recognized tenant automatically fall back to the default beans.
Module
| Artifact ID | Description |
|---|---|
a2a-java-extras-multitenancy |
@Tenant qualifier, CdiAgentExecutorRouter, CdiAgentCardRouter |
Add Dependency
<dependency>
<groupId>org.a2aproject.sdk</groupId>
<artifactId>a2a-java-extras-multitenancy</artifactId>
</dependency>
Tip: Use the extras BOM to manage the version without specifying it explicitly.
The module activates automatically via CDI when present on the classpath — no additional server configuration is required.
Architecture
flowchart TD
A["Incoming Request\ntenant field"] --> B["CdiAgentExecutorRouter"]
B -->|"@Tenant match found"| C["@Tenant AgentExecutor"]
B -->|"null / blank / unknown"| D["Default AgentExecutor"]
C --> E["RequestContext.getTenant()"]
D --> E
F["getExtendedAgentCard\npublic card URL"] --> G["CdiAgentCardRouter"]
G -->|"@Tenant match found"| H["@Tenant AgentCard"]
G -->|"null / blank / unknown"| I["Default AgentCard"]
Two CDI routers are registered automatically:
CdiAgentExecutorRouter— looks up anAgentExecutorbean qualified with@Tenant(value)for each request. Falls back to the unqualified default executor when no match is found.CdiAgentCardRouter— resolves the appropriateAgentCardforgetExtendedAgentCardand tenant-specific public card endpoints.
Declaring Per-Tenant Beans
Use the @Tenant qualifier on CDI producer methods:
AgentExecutor
@ApplicationScoped
public class MyExecutors {
@Produces
public AgentExecutor defaultExecutor() {
return new DefaultAgentExecutor();
}
@Produces
@Tenant("acme")
public AgentExecutor acmeExecutor() {
return new AcmeAgentExecutor();
}
@Produces
@Tenant("beta")
public AgentExecutor betaExecutor() {
return new BetaAgentExecutor();
}
}
AgentCard
@ApplicationScoped
public class MyAgentCards {
// Default public card — used by /.well-known/agent-card.json
@Produces
@PublicAgentCard
public AgentCard publicCard() {
return AgentCard.builder().name("My Agent")...build();
}
// Default extended card — used when no tenant match
@Produces
@ExtendedAgentCard
public AgentCard defaultExtendedCard() {
return AgentCard.builder().name("My Agent (extended)")...build();
}
// Tenant-specific extended card
@Produces
@Tenant("acme")
@ExtendedAgentCard
public AgentCard acmeExtendedCard() {
return AgentCard.builder().name("Acme Agent")...build();
}
// Tenant-specific public card — no @PublicAgentCard qualifier (see note below)
@Produces
@Tenant("acme")
public AgentCard acmePublicCard() {
return AgentCard.builder().name("Acme Agent")...build();
}
}
Note: Tenant-specific public cards must not carry
@PublicAgentCard— that qualifier is reserved for the single default public card. Adding it to a@Tenantbean causes CDI ambiguity.
Routing Rules
AgentExecutor routing
| Request tenant | Result |
|---|---|
null or blank |
Default (unqualified) executor |
Known tenant (e.g. "acme") |
@Tenant("acme") executor |
| Unknown tenant | Default (unqualified) executor |
AgentCard routing
| Request / URL | Card returned |
|---|---|
getExtendedAgentCard with no tenant |
Default @ExtendedAgentCard |
getExtendedAgentCard with tenant: "acme" |
@Tenant("acme") @ExtendedAgentCard, or default |
GET /.well-known/agent-card.json |
Default @PublicAgentCard |
GET /.well-known/acme/agent-card.json |
@Tenant("acme") card (no @PublicAgentCard), or default |
Accessing the Tenant in AgentExecutor
The resolved tenant is available in RequestContext:
@Override
public void execute(RequestContext context, AgentEmitter emitter) throws A2AError {
String tenant = context.getTenant(); // null for the default tenant
// ...
}
Tenant Source
The tenant is read from the tenant field in the request payload (e.g. MessageSendParams.tenant(), CancelTaskParams.tenant()).
For the public agent card, the tenant is encoded in the URL path — /.well-known/{tenant}/agent-card.json — and is served by all transports (JSON-RPC and REST).
For the REST transport, task-operation URLs also encode the tenant as a path prefix (e.g. /{tenant}/message:send, /{tenant}/extendedAgentCard); the payload tenant field takes precedence when both are present.
Tenant identifiers are restricted to a-zA-Z0-9_-. characters — path segments containing / or ? are rejected.
Limitations
- TaskStore and QueueManager are shared across all tenants — tasks are keyed by UUID, not partitioned per tenant.
- Per-tenant TaskAuthorizationProvider is not yet supported — a single provider applies to all tenants.
Without the Module
When a2a-java-extras-multitenancy is not on the classpath, the server behaves as a single-tenant deployment: the default AgentExecutor handles all requests, the default cards are returned, and the tenant field in request payloads is silently ignored. Existing single-tenant code requires no changes when the module is added.
See Also
- Multi-Tenancy concept page — conceptual overview and setup guide
- Extras BOM — version management for all extras modules