Cross-language: Python or PHP → Go or Java over Redis

Last updated:

The whole point of BabelQueue: a message produced in one language is consumed natively in another, over the broker you already run, using one strict JSON envelope. This demo proves it end-to-end — a Python or PHP producer and a Go or Java consumer sharing a Redis queue. The consumer never knows which language wrote the message; it only sees the canonical envelope and meta.lang.

Every SDK uses the identical reliable-queue pattern (RPUSH to produce, an atomic (B)LMOVE to a :processing list to reserve, LREM to ack), so they interoperate on the plain orders Redis list. Full runnable sources: github.com/BabelQueue/babelqueue-examples.

Producer — Python

# pip install "babelqueue[redis]"
from babelqueue import BabelQueue

app = BabelQueue("redis://localhost:6379/0", queue="orders")
app.publish("urn:babel:orders:created", {"order_id": 1042, "amount": 99.90, "currency": "USD"})
app.publish("urn:babel:catalog:item.indexed", {"sku": "WIDGET-1", "title": "Café Widget ☕"})

Producer — PHP (same queue, no framework)

The framework-less core builds the envelope and the bundled RedisTransport does a plain RPUSH onto the identical orders list — so the Go consumer below reads PHP-produced messages without changing a line.

// composer require babelqueue/php-sdk predis/predis
use BabelQueue\Codec\EnvelopeCodec;
use BabelQueue\Transport\RedisTransport;

$transport = new RedisTransport(new Predis\Client("redis://localhost:6379/0"), "orders");

$envelope = EnvelopeCodec::fromJob($job, "orders"); // $job: a PolyglotJob (URN + toPayload())
$transport->publish(EnvelopeCodec::encode($envelope), "orders");

Consumer — Go

// go get github.com/babelqueue/babelqueue-go/redis
import (
    babelqueue "github.com/babelqueue/babelqueue-go"
    bqredis "github.com/babelqueue/babelqueue-go/redis"
)

tr, _ := bqredis.New("redis://localhost:6379/0")
app := babelqueue.NewApp(tr, babelqueue.WithDefaultQueue("orders"))

app.Handle("urn:babel:orders:created", func(ctx context.Context, env babelqueue.Envelope) error {
    fmt.Printf("order %v (produced by %q)\n", env.Data["order_id"], env.Meta.Lang)
    return nil
})
app.Consume(context.Background()) // routes by URN, forever

Consumer — Java

The Java Redis transport (com.babelqueue:babelqueue-redis) reserves with the same reliable-queue pattern, so it reads the identical orders list. The full project lives in consumer-java/ of the examples repository.

// Maven: com.babelqueue:babelqueue-redis (pulls babelqueue-core + Lettuce)
import com.babelqueue.redis.RedisConsumer;
import io.lettuce.core.RedisClient;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;

RedisClient client = RedisClient.create("redis://localhost:6379/0");
try (StatefulRedisConnection<String, String> connection = client.connect()) {
    RedisCommands<String, String> redis = connection.sync();

    RedisConsumer consumer = RedisConsumer.builder(redis, "orders")
        .blockTimeoutSeconds(2)
        .handler("urn:babel:orders:created", (env, body) ->
            System.out.printf("[java] order created  id=%s  (produced by %s)%n",
                env.data().get("order_id"), env.meta().lang()))
        .handler("urn:babel:catalog:item.indexed", (env, body) ->
            System.out.printf("[java] item indexed   sku=%s  (produced by %s)%n",
                env.data().get("sku"), env.meta().lang()))
        // A URN with no handler is acked and skipped here; without this callback the
        // message is reported to onError and dead-lettered to orders.dlq (reason unknown_urn).
        .onUnknownUrn((env, body) ->
            System.out.printf("[java] (skipped unrouted urn=%s)%n", env.job()))
        .onError((error, env, body) -> System.err.println(error.getMessage()))
        .build();

    int processed = 0;
    while (true) {
        int reserved = consumer.poll(); // blocks up to blockTimeoutSeconds, routes by URN
        if (reserved == 0) {
            break; // queue drained
        }
        processed += reserved;
    }
    System.out.printf("[java] processed %d message(s)%n", processed);
} finally {
    client.shutdown();
}

The proof

Run either producer, then either consumer (Go or Java; the Java snippet above prints the same facts with a [java] prefix and the exact wording shown in its printf calls, and ends when the queue is drained). The Go program prints messages it never produced — produced by "php" (or "python") — with the data, trace_id and even unicode intact:

[go] order created  id=1042 amount=99.9 USD  trace=1d3b…  (produced by "php")
[go] order created  id=1043 amount=12.5 EUR  trace=79c3…  (produced by "php")
[go] item indexed   sku=WIDGET-1 title="Café Widget ☕"  (produced by "php")
[go] processed 3 message(s) — same envelope, different language.

The envelope on the wire is the canonical shape every SDK emits:

{
  "job": "urn:babel:orders:created",
  "trace_id": "…",
  "data": { "order_id": 1042, "amount": 99.9, "currency": "USD" },
  "meta": { "id": "…", "queue": "orders", "lang": "php", "schema_version": 1, "created_at": 1749132727000 },
  "attempts": 0
}

Swap the ends

The queue carries the canonical envelope, so any SDK can be on either side — a Go producer (app.Publish(...)) and a Python consumer (@app.handler(...) + app.run()) work the same way. PHP (framework-less RedisTransport, or Laravel/Symfony on a babelqueue-* connection), Node, Java and .NET read and write the identical envelope on their own transports.