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
Section titled “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
Section titled “How it works”Wire type tags as the interop contract
Section titled “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 producerfrom aster import wire_typefrom dataclasses import dataclass
@wire_type("myapp/TaskRequest")@dataclassclass TaskRequest: task_id: str = "" priority: int = 0The 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
Section titled “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 valuesaster contract gen --service myapp.services:TaskService --out .aster/manifest.jsonThe manifest contains the contract_id for each service version. Consumers can verify compatibility before connecting.
XLANG serialization mode
Section titled “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 import service, SerializationMode
# XLANG is the default -- this is equivalent to @service@service(serialization=[SerializationMode.XLANG])class TaskService: ...NATIVE mode uses language-native Fory serialization and cannot be deserialized by other languages – use it for deployments where interoperability is second to performance. JSON mode is human-readable on the wire; it is language-agnostic but not optimized for throughput, so reach for it when you want debuggable traffic or proxy-client compatibility.
Example: Python producer, future Java consumer
Section titled “Example: Python producer, future Java consumer”Python side (available now)
Section titled “Python side (available now)”from dataclasses import dataclassfrom typing import AsyncIterator
from aster import service, rpc, server_stream, wire_type
@wire_type("billing/Invoice")@dataclassclass Invoice: invoice_id: str = "" amount_cents: int = 0 currency: str = "USD"
@wire_type("billing/InvoiceQuery")@dataclassclass InvoiceQuery: customer_id: str = "" status: str = ""
@wire_type("billing/InvoiceEvent")@dataclassclass 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)
Section titled “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 callvar 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
Section titled “Verifying compatibility”Canonical test vectors
Section titled “Canonical test vectors”To verify that two implementations are wire-compatible:
-
Generate the contract manifest from the Python service:
Terminal window 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
Section titled “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
Section titled “Current status”| Language | Status | Notes |
|---|---|---|
| Python | Shipping 0.1.2 (first-class) | Production-ready on PyPI as aster-rpc. |
| TypeScript | Shipping 0.1.2 (first-class) | Production-ready on npm as @aster-rpc/aster. Uses the same Rust core and wire protocol. |
| Java | In progress | Via C FFI + Panama Foreign Function API (JDK 22+). |
| .NET | In progress | Via C FFI + P/Invoke or NativeAOT. |
| Kotlin | In progress | JVM binding surface with Kotlin-first ergonomics. |
| Go | In progress | Via C FFI + cgo. |
| Rust | Planned | Direct access to core crate. No FFI overhead. |
All bindings are validated against shared wire output and contract identity test vectors to ensure interop.
See the Bindings section for per-language status and architecture details.