| name | apache-kafka-event-streaming-and-schema-registry |
| description | Production-grade guidance for Apache Kafka event streaming, partition strategies, consumer group management, transactional messaging, and Confluent Schema Registry integration (Avro/Protobuf/JSON Schema). Use when designing Kafka topics, configuring producers/consumers for idempotency and ordering, enforcing schema evolution policies, or implementing dead-letter queues. |
Apache Kafka Event Streaming & Schema Registry Guide
1. Overview & Core Philosophy
Apache Kafka is an append-only distributed commit log designed for high-throughput, fault-tolerant event streaming. Unlike traditional message queues (e.g., RabbitMQ) that delete messages upon consumer ACK, Kafka retains messages for a configurable retention window, allowing multiple heterogeneous consumer groups to read and re-read event streams independently.
Key Architectural Axioms
- Log Ordering Guarantee: Total order is guaranteed only within a single partition, not across partitions in a topic.
- Partitioning as Unit of Parallelism: Maximum concurrency for a consumer group is bounded by the number of partitions in a topic.
- Decoupled State via Schema Registry: Producers and consumers must share data contracts via explicit schemas (Avro, Protobuf, or JSON Schema) registered in a Schema Registry rather than untyped payloads.
- Zero-Data-Loss Default: Production setups must enforce strict durability constraints (
acks=all, min.insync.replicas=2, replication.factor=3).
2. Topic Architecture & Partitioning Strategy
2.1 Partition Key Selection & Distribution
Selecting an optimal partition key is critical for balance and ordering.
- Natural Entity Keys: Key by domain entity ID (e.g.,
order_id, customer_id) when state updates for that specific entity must arrive in order.
- Custom Partitioner: Implement custom hashing (or murmur2 default) to prevent skew when entity keys follow power-law distributions (hotspot keys).
- Null Keys (Round-Robin / Sticky): Use only for high-throughput append logs where ordering across events is irrelevant.
2.2 Topic Configuration Checklist
replication.factor=3
min.insync.replicas=2
cleanup.policy=delete
retention.ms=604800000
segment.bytes=1073741824
unclean.leader.election.enable=false
2.3 Compacted Topics
Use cleanup.policy=compact for changelog topics (e.g., Kafka Streams state stores or KSQLDB tables).
- Requires non-null message keys.
- Retains at least the latest record value for each key.
- Tombstone messages (null payload) trigger log garbage collection for key deletion.
3. Reliable Producer & Consumer Engineering
3.1 Producer Idempotency & Durability Matrix
| Setting | Value | Rationale |
|---|
acks | all (-1) | Leader waits for full ISR acknowledgment. |
enable.idempotence | true | Assigns Producer ID (PID) and sequence numbers to eliminate network retries duplicate writes. |
max.in.flight.requests.per.connection | 5 (when idempotent) | Maintains strict ordering while enabling pipeline throughput. |
retries | 2147483647 (INT_MAX) | Producer retries transient broker errors indefinitely until delivery.timeout.ms expires. |
3.2 Transactional Producer (Exactly-Once Semantics across Topics)
When consuming from topic A and producing to topic B (read-process-write loop):
producer.init_transactions()
try:
producer.begin_transaction()
producer.send("topic-B", key=key, value=val)
producer.send_offsets_to_transaction(offsets, consumer_group_id)
producer.commit_transaction()
except Exception as e:
producer.abort_transaction()
3.3 Consumer Group & Rebalance Management
- Manual Offset Commit: Disable
enable.auto.commit=false. Commit offsets synchronously (commitSync) after batch processing, or asynchronously (commitAsync) with error callbacks.
- Cooperative Sticky Assignor: Configure
partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor to minimize stop-the-world rebalances.
- Processing Timeouts: Ensure
max.poll.interval.ms exceeds maximum batch processing duration to prevent unwanted consumer group eviction.
4. Schema Governance & Schema Registry Integration
Confluent Schema Registry decouples payload structure from business code by serving as a central authority for data contracts.
4.1 Schema Compatibility Modes
[ Producer ] ---> Registers Schema Version N
|
[ Schema Registry ]
| Validates Rules
[ Consumer ] <--- Fetches Schema Version N
- BACKWARD (Default Recommended): Consumers using schema $N$ can read messages written with schema $N-1$. Fields can be deleted, or optional fields added.
- FORWARD: Consumers using schema $N-1$ can read messages written with schema $N$. New fields can be added, or optional fields deleted.
- FULL: Bidirectional compatibility. Fields can be added or deleted, but must always have default values.
4.2 Avro Schema Evolution Rules
- Adding a Field: MUST specify a
default value (e.g., {"name": "status", "type": "string", "default": "PENDING"}).
- Removing a Field: Only remove fields that previously had default values.
- Changing Types: Only promote narrow types to wider types (e.g.,
int to long). Never change string to integer directly.
5. Resilience: Retries, Backpressure & Dead Letter Queues (DLQ)
Processing failures in Kafka consumers must never block the main partition processing loop indefinitely.
5.1 Non-Blocking Retry Topic Architecture
[ Main Topic: orders ]
│ (Error)
▼
[ Retry Topic 1: orders-retry-1m ] ──► (Delay 1m) ──► Re-attempt
│ (Error)
▼
[ Retry Topic 2: orders-retry-5m ] ──► (Delay 5m) ──► Re-attempt
│ (Failed Max Attempts)
▼
[ Dead Letter Queue: orders-dlq ] ──► Manual Inspection / Alerting
- Main topic consumers immediately push unprocessable messages to a dedicated retry topic with error metadata in message headers (
x-exception-message, x-original-topic, x-retry-count).
- A dedicated retry consumer consumes from retry topics with backoff delays before republishing.
6. Critical Anti-Patterns & Production Warnings
- Auto-Committing Offsets (
enable.auto.commit=true): Causes silent data loss when application crashes midway through processing polled batches.
- Blocking Operations inside Consumer Poll Loop: Long-running HTTP/DB calls in
poll() thread cause max.poll.interval.ms timeout, triggering constant rebalance cascades.
- Partition Skew via Uniform Null Keys on High-Throughput Topics: Leads to broker disk imbalance and bottlenecking single consumer instances.
- Publishing Schemaless JSON Payloads: Results in runtime breaking changes when field types or names change downstream.
- Ignoring
min.insync.replicas: Setting acks=all with min.insync.replicas=1 provides no multi-node durability if two brokers fail simultaneously.
7. Production Code Reference
7.1 Python: Robust Producer with Schema Registry (confluent-kafka)
from confluent_kafka import SerializingProducer
from confluent_kafka.serialization import StringSerializer
from confluent_kafka.schema_registry import SchemaRegistryClient
from confluent_kafka.schema_registry.avro import AvroSerializer
schema_str = """
{
"type": "record",
"name": "OrderCreated",
"namespace": "com.events.orders",
"fields": [
{"name": "order_id", "type": "string"},
{"name": "customer_id", "type": "string"},
{"name": "amount", "type": "double"},
{"name": "created_at", "type": "long"}
]
}
"""
sr_config = {'url': 'http://schema-registry.prod.internal:8081'}
schema_registry_client = SchemaRegistryClient(sr_config)
avro_serializer = AvroSerializer(
schema_registry_client,
schema_str,
to_dict=lambda obj, ctx: obj
)
producer_config = {
'bootstrap.servers': 'kafka-1.prod.internal:9092,kafka-2.prod.internal:9092',
'key.serializer': StringSerializer('utf_8'),
'value.serializer': avro_serializer,
'acks': 'all',
'enable.idempotence': True,
'max.in.flight.requests.per.connection': 5,
'retries': 1000000,
'delivery.timeout.ms': 120000
}
producer = SerializingProducer(producer_config)
def delivery_report(err, msg):
if err is not None:
print(f"Message delivery failed for key {msg.key()}: ")
:
()
payload = {
: ,
: ,
: ,
:
}
producer.produce(
topic=,
key=payload[],
value=payload,
on_delivery=delivery_report
)
producer.flush()
7.2 Node.js / TypeScript: Resilient Consumer with Manual Ack & Retry DLQ (KafkaJS)
import { Kafka, Consumer, EachMessagePayload, PartitionAssigners } from 'kafkajs';
const kafka = new Kafka({
clientId: 'order-processing-service',
brokers: ['kafka-1.prod.internal:9092', 'kafka-2.prod.internal:9092']
});
const consumer: Consumer = kafka.consumer({
groupId: 'order-processors-v1',
partitionAssigners: [PartitionAssigners.cooperativeSticky],
sessionTimeout: 30000,
heartbeatInterval: 3000,
});
async function processOrderMessage({ topic, partition, message }: EachMessagePayload): Promise<void> {
const orderId = message.key?.toString();
const rawValue = message.value?.toString();
if (!rawValue) return;
try {
const orderData = JSON.parse(rawValue);
await (orderData);
} (: ) {
.(, err);
({
: topic,
: partition,
: message.,
: message.,
: message.,
: {
...message.,
: .(err. || ),
: .( ().())
}
});
}
}
(): <> {
consumer.();
consumer.({ : , : });
consumer.({
: ,
: (payload) => {
(payload);
{ topic, partition, message } = payload;
consumer.([
{ topic, partition, : ((message.) + ).() }
]);
}
});
}
().(.);
() {}
() {}