CloudEvents over Kafka
The Kafka protocol binding defines two ways of carrying a CloudEvent on a Kafka record:
- Structured mode. The whole event is encoded — typically as JSON — in the record value, with
content-typeset toapplication/cloudevents+json; charset=UTF-8. - Binary mode. The context attributes are mapped to record headers prefixed
ce_, and the event data alone occupies the record value.
The two modes below are implemented in one class so the difference is visible in the producer code rather than described in prose.
import org.apache.kafka.clients.producer.ProducerRecord;import org.apache.kafka.common.header.internals.RecordHeader;import com.google.gson.Gson;import java.nio.charset.StandardCharsets;import java.time.OffsetDateTime;import java.util.UUID;
public class CloudEventsKafkaExample { // CloudEvent data class public static class CloudEvent<T> { String id; String source; String specversion; String type; OffsetDateTime time; T data;
public CloudEvent(String source, String type, T data) { this.id = UUID.randomUUID().toString(); this.source = source; this.specversion = "1.0"; this.type = type; this.time = OffsetDateTime.now(); this.data = data; } }
// Example event data public static class OrderCreated { String orderId; double amount;
public OrderCreated(String orderId, double amount) { this.orderId = orderId; this.amount = amount; } }
// Structured mode - entire CloudEvent as JSON in the record value public static ProducerRecord<String, String> createStructuredCloudEvent( String topic, OrderCreated orderData) {
CloudEvent<OrderCreated> event = new CloudEvent<>( "/orders", "com.example.order.created", orderData );
String jsonValue = new Gson().toJson(event);
return new ProducerRecord<>( topic, null, // key jsonValue ); }
// Binary mode - CloudEvent attributes in headers, data as the value public static ProducerRecord<String, String> createBinaryCloudEvent( String topic, OrderCreated orderData) {
CloudEvent<OrderCreated> event = new CloudEvent<>( "/orders", "com.example.order.created", orderData );
String jsonData = new Gson().toJson(orderData);
ProducerRecord<String, String> record = new ProducerRecord<>( topic, null, // key jsonData );
// Add CloudEvents attributes as headers record.headers().add( new RecordHeader("ce_id", event.id.getBytes(StandardCharsets.UTF_8))); record.headers().add( new RecordHeader("ce_source", event.source.getBytes(StandardCharsets.UTF_8))); record.headers().add( new RecordHeader("ce_specversion", event.specversion.getBytes(StandardCharsets.UTF_8))); record.headers().add( new RecordHeader("ce_type", event.type.getBytes(StandardCharsets.UTF_8))); record.headers().add( new RecordHeader("ce_time", event.time.toString().getBytes(StandardCharsets.UTF_8)));
return record; }
// Example usage public static void main(String[] args) { OrderCreated orderData = new OrderCreated("12345", 99.99);
ProducerRecord<String, String> structuredRecord = createStructuredCloudEvent("orders", orderData);
ProducerRecord<String, String> binaryRecord = createBinaryCloudEvent("orders", orderData);
// Use these records with a Kafka producer... }}Points worth holding on to:
- In structured mode the only obligations are that the CloudEvent JSON is valid and that
content-typeidentifies it asapplication/cloudevents+json. The record survives being bridged to another transport unchanged. - In binary mode the
ce_prefix makes the context attributes easy to identify, and a consumer or a stream processor can route and filter on them without deserialising the payload. - Binary mode is usually the more efficient of the two, because the event data is not encoded twice.
An official CloudEvents Java SDK provides a Kafka module that performs this mapping, which is preferable to hand-rolling the headers in production code; the example above is written out longhand to show what the binding actually does.