Working with Message Headers

1. Understanding Headers Concept

Aspect Description Detail
Header Key (String) + value (byte[]) pair Outside payload
Multiple Repeated keys allowed Iterable
Use Metadata without touching value Cross-cutting

Example: Headers interface

Headers headers = record.headers();
for (Header h : headers) System.out.println(h.key());

2. Adding Headers to Producer

Method Purpose Detail
add(key, val) Append a header byte[] value
Constructor Pass header list to record At creation
Encoding App serializes value UTF-8 common

Example: Add producer headers

ProducerRecord<String, String> r = new ProducerRecord<>("orders", "k", "v");
r.headers().add("event-type", "created".getBytes(StandardCharsets.UTF_8));
producer.send(r);

3. Reading Headers in Consumer

Method Returns Detail
headers() All headers Iterable
lastHeader(key) Most recent value Or null
headers(key) All values for key Iterable

Example: Read a header

Header h = record.headers().lastHeader("event-type");
String type = h != null ? new String(h.value(), StandardCharsets.UTF_8) : null;

4. Setting Custom Headers

Header Purpose Example
correlation-id Request correlation UUID
content-type Payload format application/json
schema-version Payload version v2

Example: Multiple custom headers

r.headers()
 .add("content-type", "application/json".getBytes())
 .add("schema-version", "v2".getBytes());

5. Using Headers for Routing

Aspect Description Detail
Routing Key Header drives downstream branch No payload parse
Connect SMT Route by header transform Declarative
Filter Skip records by header Cheap check

Example: Route by header

String region = new String(record.headers().lastHeader("region").value());
if ("eu".equals(region)) forwardToEu(record);

6. Storing Metadata in Headers

Metadata Use Detail
source-app Origin service Audit
timestamp Producer time Latency calc
retry-count Attempt number DLQ logic

Example: Retry count header

int attempts = headerInt(record, "retry-count", 0);
r.headers().add("retry-count", String.valueOf(attempts + 1).getBytes());

7. Filtering by Headers

Aspect Description Detail
Pre-filter Check header before processing Skip early
No deserialize Avoid value parse cost Efficient
Streams filter() on header predicate Declarative

Example: Filter by header

for (var r : records) {
    if (r.headers().lastHeader("skip") != null) continue;
    process(r);
}

8. Understanding Header Serialization

Aspect Description Detail
Value Type Always byte[] App encodes
No Schema Headers are schemaless Convention-based
Size Counted in message size Keep small

Example: Encode/decode header value

byte[] val = ByteBuffer.allocate(8).putLong(System.currentTimeMillis()).array();
r.headers().add("ts", val);
long ts = ByteBuffer.wrap(r.headers().lastHeader("ts").value()).getLong();

9. Propagating Headers in Streams

Aspect Description Detail
Default Headers carried to output map preserves
Processor Access via context().headers() Read/modify
Joins Header propagation varies Verify behavior

Example: Headers in processor

public void process(Record<String, String> record) {
    record.headers().add("processed-by", "agg".getBytes());
    context.forward(record);
}

10. Adding Trace IDs

Aspect Description Detail
traceparent W3C trace context OpenTelemetry
Propagation Carried producer→consumer End-to-end trace
Auto-instrument OTel Kafka interceptors Zero-code

Example: Inject trace id

r.headers().add("traceparent",
    currentSpan.traceParent().getBytes(StandardCharsets.UTF_8));