---
title: "Apache Pulsar transport"
description: "The babelqueue-pulsar transport — a canonical-envelope publisher and a URN-routed consumer over pulsar-client, on the framework-agnostic core."
source: https://babelqueue.com/docs/babelqueue-java/1.x/apache-pulsar/
updated: 2026-06-14T00:00:00.000Z
---
# Apache Pulsar transport
`com.babelqueue:babelqueue-pulsar` is an Apache Pulsar transport on the Java core. It sends
the [canonical envelope](/docs/spec/1.x/envelope/) as the message payload with the
[§5 property projection](/docs/spec/1.x/broker-bindings/#apache-pulsar), and consumes by
routing each message to a handler by URN — so a message it produces is consumed by any
other BabelQueue SDK, and vice-versa.
## Install
Maven:
```xml
com.babelqueue
babelqueue-pulsar
1.0.0
```
Requirements: **Java 17+**. It pulls `babelqueue-core` and `org.apache.pulsar:pulsar-client`
transitively. You supply the Pulsar `Producer` / `Consumer`.
## Produce
```java
import com.babelqueue.pulsar.PulsarPublisher;
import org.apache.pulsar.client.api.*;
import java.util.Map;
PulsarClient client = PulsarClient.builder().serviceUrl("pulsar://localhost:6650").build();
Producer producer = client.newProducer().topic("orders").create();
String id = PulsarPublisher.create(producer)
.publish("urn:babel:orders:created", Map.of("order_id", 1042L));
```
`publish(urn, data)` returns the message `meta.id`; overloads add a `traceId` and a relative
`Duration delay` (native `deliverAfter`).
## Consume
```java
import com.babelqueue.pulsar.PulsarConsumer;
import org.apache.pulsar.client.api.*;
Consumer sub = client.newConsumer()
.topic("orders").subscriptionName("babelqueue")
.subscriptionType(SubscriptionType.Shared).subscribe();
PulsarConsumer consumer = PulsarConsumer.builder(sub)
.handler("urn:babel:orders:created", (envelope, message) -> {
// envelope.data(), envelope.traceId(), envelope.attempts() ...
})
.onError((error, envelope, message) -> error.printStackTrace())
.build();
while (running) {
consumer.poll();
}
```
A throwing handler `negativeAcknowledge`s the message — the broker redelivers it and
increments `getRedeliveryCount()` (at-least-once); with a native `DeadLetterPolicy` it
eventually moves to the cross-language `.dlq` topic. The consumer routes purely on the
`bq-job` property, so it never decodes a message it cannot handle.
## Contract mapping (§5)
| Envelope | Apache Pulsar |
| :--- | :--- |
| body | message payload (byte-identical across SDKs) |
| `job` (URN) | property `bq-job` (consumer routes on this) |
| `trace_id` | property `bq-trace-id` |
| `meta.id` | property `bq-message-id` |
| `meta.schema_version` | property `bq-schema-version` |
| `meta.lang` | property `bq-source-lang` |
| `meta.created_at` | publish time (mirror; body authoritative) |
| `attempts` | property `bq-attempts`, reconciled to `max(body, getRedeliveryCount())` |
| reserve / ack / retry | `acknowledge` / `negativeAcknowledge` |
Pulsar properties are string→string, so `bq-attempts` carries the contract `attempts` and is
**authoritative**; `getRedeliveryCount()` is 0-based, so the reconciliation maps it directly
with **no −1**. The Pulsar `Producer` / `Consumer` / `Message` interfaces are mocked with
Mockito 5 — no Pulsar, no network. The envelope is unchanged (`schema_version` stays `1`);
Apache Pulsar is purely additive.