Cross-Language Interop
Aster is designed so that a service defined in one language can be consumed from any other supported language. This page explains how cross-language interop works, what guarantees it provides, and how to structure services for multi-language deployments. The reference implementation is Python; concepts apply to all bindings.
The promise
A service defined in Python can be consumed by a Java client (or Go, Rust, .NET, JavaScript) without any code generation step, provided both sides agree on the wire types. The contract_id -- a deterministic hash of the service interface -- ensures wire compatibility between producer and consumer regardless of implementation language.
How it works
Wire type tags as the interop contract
Cross-language interop is built on @wire_type tags. When a Python producer serializes a TaskRequest tagged as "myapp/TaskRequest", a Java consumer can deserialize it by registering the same tag on its equivalent class.
# Python producer
from aster.codec import wire_type
from dataclasses import dataclass
@wire_type("myapp/TaskRequest")
@dataclass
class TaskRequest:
task_id: str = ""
priority: int = 0
The tag "myapp/TaskRequest" is the contract. Any language binding that registers a type with this tag and compatible fields can deserialize the data.
Contract identity
Each @service-decorated class has a contract_id: a canonical hash computed from the service name, version, method signatures, and wire type tags. Two implementations in different languages that produce the same contract_id are wire-compatible.
# Generate the contract manifest to inspect contract_id values
aster contract gen --service myapp.services:TaskService --out .aster/manifest.json
The manifest contains the contract_id for each service version. Consumers can verify compatibility before connecting.
XLANG serialization mode
Cross-language interop requires SerializationMode.XLANG (the default). This mode uses Apache Fory's cross-language serialization, which encodes type tags and field schemas in the wire format.
from aster.types import SerializationMode
# XLANG is the default -- this is equivalent to @service
@service(serialization=[SerializationMode.XLANG])
class TaskService:
...
NATIVE mode uses Python-specific Fory serialization and cannot be deserialized by other languages. ROW mode is language-agnostic in principle but requires that all languages support Fory's row format.
Example: Python producer, future Java consumer
Python side (available now)
from dataclasses import dataclass
from typing import AsyncIterator
from aster.codec import wire_type
from aster.decorators import service, rpc, server_stream
@wire_type("billing/Invoice")
@dataclass
class Invoice:
invoice_id: str = ""
amount_cents: int = 0
currency: str = "USD"
@wire_type("billing/InvoiceQuery")
@dataclass
class InvoiceQuery:
customer_id: str = ""
status: str = ""
@wire_type("billing/InvoiceEvent")
@dataclass
class InvoiceEvent:
invoice_id: str = ""
event_type: str = ""
timestamp: int = 0
@service("BillingService", version=1)
class BillingService:
@rpc
async def get_invoice(self, req: InvoiceQuery) -> Invoice:
return Invoice(
invoice_id="INV-001",
amount_cents=9999,
currency="USD",
)
@server_stream
async def watch_events(self, req: InvoiceQuery) -> AsyncIterator[InvoiceEvent]:
yield InvoiceEvent(
invoice_id="INV-001",
event_type="created",
timestamp=1700000000,
)
Java side (planned)
When the JVM binding is available, the Java consumer will look approximately like this:
// Register types with matching wire_type tags
@WireType("billing/Invoice")
public record Invoice(String invoiceId, int amountCents, String currency) {}
@WireType("billing/InvoiceQuery")
public record InvoiceQuery(String customerId, String status) {}
// Connect and call
var client = AsterClient.connect(endpointAddr);
var billing = client.stub(BillingService.class);
Invoice invoice = billing.getInvoice(new InvoiceQuery("cust-123", "active"));
The Java client and Python producer are wire-compatible because they share the same @wire_type tags and the same contract_id.
Verifying compatibility
Canonical test vectors
To verify that two implementations are wire-compatible:
-
Generate the contract manifest from the Python service:
aster contract gen --service myapp.services:BillingService --out manifest.json -
The manifest contains the
contract_idfor each service. Compare this against the manifest generated by the other language's tooling. -
For byte-level verification, serialize a known object in one language and deserialize it in the other. The XLANG serialization format is deterministic for the same input and schema.
Field compatibility rules
For two types to be wire-compatible across languages:
- They must have the same
@wire_typetag. - They must have fields with matching names and compatible types.
- Field order does not matter (Fory XLANG uses named fields).
- Supported primitive types:
str,int,float,bool,bytes. These map to language-native equivalents. - Nested dataclasses must also have matching
@wire_typetags.
Current status
| Language | Status | Notes |
|---|---|---|
| Python | Reference implementation | Production-ready for 0.1-alpha. |
| Rust | Planned | Direct access to core crate. No FFI overhead. |
| JVM | Planned | Via C FFI + Panama Foreign Function API (JDK 22+). |
| Go | Planned | Via C FFI + cgo. |
| .NET | Planned | Via C FFI + P/Invoke or NativeAOT. |
| JavaScript | Planned | Via WASM or N-API. |
The Python binding defines the reference behavior. Other bindings will be validated against Python's wire output and contract identity computation to ensure interop.
See the Bindings section for per-language status and architecture details.