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.