Skip to content

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.

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.

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 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.

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.

Terminal window
# 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.

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”
from dataclasses import dataclass
from typing import AsyncIterator
from aster import service, rpc, server_stream, wire_type
@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,
)

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.

To verify that two implementations are wire-compatible:

  1. Generate the contract manifest from the Python service:

    Terminal window
    aster contract gen --service myapp.services:BillingService --out manifest.json
  2. The manifest contains the contract_id for each service. Compare this against the manifest generated by the other language’s tooling.

  3. 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.

For two types to be wire-compatible across languages:

  • They must have the same @wire_type tag.
  • 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_type tags.
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.