Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Topic design, partition strategies, consumer group patterns, exactly-once processing, and dead letter queue handling.
.claude/skills/kafka-patterns/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | — | — |
| case-16 | ✗→✓ | ▲ Improved | — | — |
| case-10 | ✗→✓ | ▲ Improved | — | — |
| case-14 | ✗→✓ | ▲ Improved | — | — |
| case-03 | ✗→✓ | ▲ Improved | — | — |
Event streaming patterns for Apache Kafka in distributed systems.
yaml# Topic naming convention: <domain>.<entity>.<event-type> # Examples: # orders.order.created # payments.payment.completed # inventory.stock.updated # Topic configuration topics: orders.order.created: partitions: 12 # Match expected consumer parallelism replication-factor: 3 # Survive 2 broker failures retention.ms: 604800000 # 7 days cleanup.policy: delete orders.order.changelog: partitions: 12 replication-factor: 3 retention.ms: -1 # Infinite retention (compacted) cleanup.policy: compact # Keep latest value per key min.compaction.lag.ms: 3600000 # 1h before compacting
typescriptimport { Kafka, Partitioners, CompressionTypes } from 'kafkajs' const kafka = new Kafka({ clientId: 'order-service', brokers: process.env.KAFKA_BROKERS!.split(','), }) const producer = kafka.producer({ idempotent: true, // Exactly-once producer maxInFlightRequests: 5, // Max parallel requests createPartitioner: Partitioners.DefaultPartitioner, }) await producer.connect() // Key-based partitioning: same key always goes to same partition (ordering) async function publishOrderEvent(order: Order, eventType: string): Promise<void> { await producer.send({ topic: `orders.order.${eventType}`, compression: CompressionTypes.LZ4, messages: [{ key: order.id, // Orders for same ID → same partition → ordered value: JSON.stringify({ eventId: crypto.randomUUID(), // Idempotency key eventType, timestamp: new Date().toISOString(), data: order, }), headers: { 'content-type': 'application/json', 'source': 'order-service', 'correlation-id': order.correlationId, }, }], }) } // Batch publishing for throughput async function publishBatch(events: OrderEvent[]): Promise<void> { await producer.sendBatch({ topicMessages: [{ topic: 'orders.order.created', messages: events.map(e => ({ key: e.orderId, value: JSON.stringify(e), })), }], }) }
typescriptconst consumer = kafka.consumer({ groupId: 'payment-processor', // Consumer group: shared topic consumption sessionTimeout: 30000, heartbeatInterval: 3000, maxBytesPerPartition: 1048576, // 1MB per partition per fetch retry: { retries: 5 }, }) await consumer.connect() await consumer.subscribe({ topics: ['orders.order.created'], fromBeginning: false, // Start from latest offset }) await consumer.run({ autoCommit: false, // Manual commit for exactly-once eachBatchAutoResolve: false, eachBatch: async ({ batch, resolveOffset, commitOffsetsIfNecessary, heartbeat }) => { for (const message of batch.messages) { try { const event = JSON.parse(message.value!.toString()) // Idempotency check: skip already processed events if (await isAlreadyProcessed(event.eventId)) { resolveOffset(message.offset) continue } await processOrderPayment(event.data) await markAsProcessed(event.eventId) resolveOffset(message.offset) await commitOffsetsIfNecessary() await heartbeat() } catch (err) { console.error(`Failed to process message at offset ${message.offset}:`, err) // Send to DLQ instead of blocking the partition await sendToDeadLetterQueue(message, err as Error) resolveOffset(message.offset) } } }, })
typescriptconst DLQ_TOPIC = 'orders.order.created.dlq' async function sendToDeadLetterQueue( originalMessage: KafkaMessage, error: Error ): Promise<void> { await producer.send({ topic: DLQ_TOPIC, messages: [{ key: originalMessage.key, value: originalMessage.value, headers: { ...originalMessage.headers, 'dlq-reason': error.message, 'dlq-timestamp': new Date().toISOString(), 'dlq-original-topic': 'orders.order.created', 'dlq-retry-count': '0', }, }], }) } // DLQ consumer: retry or alert async function processDLQ(): Promise<void> { const dlqConsumer = kafka.consumer({ groupId: 'dlq-processor' }) await dlqConsumer.subscribe({ topics: [DLQ_TOPIC] }) await dlqConsumer.run({ eachMessage: async ({ message }) => { const retryCount = parseInt( message.headers?.['dlq-retry-count']?.toString() ?? '0' ) if (retryCount >= 3) { // Max retries exceeded: alert ops team await alertOps({ topic: DLQ_TOPIC, key: message.key?.toString(), reason: message.headers?.['dlq-reason']?.toString(), retries: retryCount, }) return } // Retry with incremented count try { const event = JSON.parse(message.value!.toString()) await processOrderPayment(event.data) } catch (err) { // Re-enqueue with incremented retry count await producer.send({ topic: DLQ_TOPIC, messages: [{ key: message.key, value: message.value, headers: { ...message.headers, 'dlq-retry-count': String(retryCount + 1), }, }], }) } }, }) }
typescript// Custom partitioner: route by region for data locality const regionalPartitioner = () => ({ partition: ({ topic, partitionMetadata, message }) => { const region = message.headers?.['region']?.toString() ?? 'default' const regionMap: Record<string, number> = { 'us-east': 0, 'us-west': 1, 'eu-west': 2, 'eu-east': 3, 'ap-southeast': 4, } const partition = regionMap[region] if (partition !== undefined && partition < partitionMetadata.length) { return partition } // Fallback: hash the key const numPartitions = partitionMetadata.length const hash = murmurHash(message.key?.toString() ?? '') return Math.abs(hash) % numPartitions } })
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-02 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-23 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +22 percentage points is the difference between those two pass rates over the 23 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.