Apache Kafka transport
com.babelqueue:babelqueue-kafka is an Apache Kafka transport on the Java core. It sends the
canonical envelope as the record value with the
§6 header projection, and consumes by routing
each record to a handler by URN — so a record it produces is consumed by any other BabelQueue
SDK, and vice-versa. Kafka has no native ack/delay/DLQ/delivery-counter, so the binding
absorbs all four in the transport.
Install
Maven:
<dependency>
<groupId>com.babelqueue</groupId>
<artifactId>babelqueue-kafka</artifactId>
<version>1.0.0</version>
</dependency>
Requirements: Java 17+. It pulls babelqueue-core and org.apache.kafka:kafka-clients
transitively. You supply the Kafka Producer / Consumer.
Produce
import com.babelqueue.kafka.KafkaPublisher;
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.common.serialization.ByteArraySerializer;
import java.util.Map;
Map<String, Object> cfg = Map.of(
ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092",
ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class,
ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class);
try (Producer<byte[], byte[]> producer = new KafkaProducer<>(cfg)) {
String id = KafkaPublisher.create(producer, "orders")
.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 (delays require a RetryTopics topology, else they raise).
Consume
import com.babelqueue.kafka.*;
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.serialization.ByteArrayDeserializer;
import java.time.Duration;
import java.util.List;
Consumer<byte[], byte[]> consumer = new KafkaConsumer<>(Map.of(
ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092",
ConsumerConfig.GROUP_ID_CONFIG, "orders-workers",
ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false, // manual commit (process-then-commit)
ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class,
ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class));
consumer.subscribe(List.of("orders"));
RetryTopics retry = RetryTopics.forTopic("orders")
.tier(Duration.ofSeconds(5)).tier(Duration.ofMinutes(1)).build();
com.babelqueue.kafka.KafkaConsumer worker = com.babelqueue.kafka.KafkaConsumer.builder(consumer)
.producer(producer)
.retryTopics(retry)
.maxTries(3)
.handler("urn:babel:orders:created", (envelope, record) -> {
// envelope.data(), envelope.traceId(), envelope.attempts() ...
})
.build();
worker.run(() -> true); // poll → process → commit
A throwing handler republishes the envelope to the next <topic>.retry.<n> tier with
bq-attempts + 1, then commits; once maxTries is reached it goes to <topic>.dlq with a
dead_letter block. The consumer routes on the bq-job header. Unknown-URN strategy is one
of fail / delete / release / dead_letter.
Contract mapping (§6)
| Envelope | Apache Kafka |
|---|---|
| body | record value (byte-identical across SDKs) |
job (URN) |
header bq-job (consumer routes on this) |
trace_id |
header bq-trace-id |
meta.id |
header bq-message-id |
meta.schema_version |
header bq-schema-version |
meta.lang |
header bq-source-lang |
meta.created_at |
record timestamp (Unix ms) |
attempts |
header bq-attempts (authoritative; body is the fallback) |
| reserve / ack | poll → process → commit offset (manual) |
| retry / delay | republish to <topic>.retry.<n> (bq-attempts + 1) |
| dead-letter | <topic>.dlq + dead_letter block |
The Kafka Consumer is mocked with Mockito and the producer with the official MockProducer
— no Kafka, no network. The envelope is unchanged (schema_version stays 1); Apache Kafka
is purely additive.