Definition
- Partition key -- the value Kafka hashes to decide which partition a record is written to; records with the same key always land in the same partition.
- murmur2 hash -- the default hashing algorithm used by Kafka's partitioner:
partition = murmur2(key) % numPartitions. - Per-key ordering -- because the same key always maps to the same partition and order is guaranteed within a partition, all events for one key are ordered.
- High cardinality -- a key with many distinct values (such as account id) that distributes load evenly across partitions.
- Hot partition -- a single overloaded partition caused by a skewed key, bottlenecking one consumer and one broker while others sit idle.
- Composite key -- a technique like
accountId + bucketNumberto spread a dominant key across a few partitions while keeping partial ordering.
Picture National Bank of India with twelve teller windows (partitions). The key is the rule that decides which window a customer goes to. If you send customers by account id, which is unique and spread out, all twelve windows stay evenly busy. But if you send them by branch city and ninety percent of customers are from Mumbai, one window has a giant queue while eleven windows are empty. That one jammed window is the hot partition -- the whole bank feels slow even though it has twelve windows.
Understanding the Concepts
What is a partition key in Kafka?
A partition key is the value attached to a Kafka record that determines which partition it lands in. When a key is present, the default partitioner computes murmur2(keyBytes) % numPartitions to pick the partition. Because the same key always produces the same hash, all records with that key go to the same partition, which guarantees per-key ordering.
package com.bank.producer;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import java.util.Properties;
public class KeyedProducer {
public static void main(String[] args) {
Properties props = new Properties();
props.put("bootstrap.servers", "broker1:9092");
props.put("key.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
try (KafkaProducer<String, String> producer = new KafkaProducer<>(props)) {
// Key is the account id -- all events for SAV2001 go to the same partition
producer.send(new ProducerRecord<>("transfers", "SAV2001",
"{\"amount\":50000,\"type\":\"DEBIT\"}"));
producer.send(new ProducerRecord<>("transfers", "SAV2001",
"{\"amount\":50000,\"type\":\"CREDIT\"}"));
System.out.println("Both events for SAV2001 land on the same partition");
}
}
}
Both events for SAV2001 land on the same partition
Why does the partition key matter for banking?
In a bank, the sequence of debits and credits for an account must be preserved. If a debit for Priya's account lands on partition 3 and the matching credit lands on partition 7, two different consumers process them, and the credit might be applied before the debit, causing a temporary negative balance or a failed transaction. Keying by account id guarantees that both events land on the same partition and are consumed in order.
package com.bank.ordering;
public class OrderingGuarantee {
public static void main(String[] args) {
System.out.println("With key = accountId:");
System.out.println(" DEBIT SAV2001 -> partition 3 (offset 100)");
System.out.println(" CREDIT SAV2001 -> partition 3 (offset 101)");
System.out.println(" Consumer processes DEBIT first, then CREDIT -- correct!");
System.out.println();
System.out.println("Without key (round-robin):");
System.out.println(" DEBIT SAV2001 -> partition 3");
System.out.println(" CREDIT SAV2001 -> partition 7");
System.out.println(" Two consumers -- order not guaranteed -- WRONG!");
}
}
With key = accountId:
DEBIT SAV2001 -> partition 3 (offset 100)
CREDIT SAV2001 -> partition 3 (offset 101)
Consumer processes DEBIT first, then CREDIT -- correct!
Without key (round-robin):
DEBIT SAV2001 -> partition 3
CREDIT SAV2001 -> partition 7
Two consumers -- order not guaranteed -- WRONG!
How do you choose a good partition key?
A good key balances two goals: ordering and distribution. You want per-entity ordering to be correct and enough spread to use all partitions. The rule is high cardinality and even distribution.
- Account id -- millions of distinct values, roughly even traffic. Excellent.
- Customer id -- similar cardinality to account id. Good.
- Transaction id -- unique per event, perfect spread but no ordering between related events.
- Region or country -- low cardinality, heavy skew. Bad.
- Status (ACTIVE/INACTIVE) -- two values. Terrible.
package com.bank.key;
public class KeyChoiceGuide {
public static void main(String[] args) {
System.out.println("Key choice for National Bank of India:");
System.out.println(" transfers topic -> accountId (ordering + spread)");
System.out.println(" audit log topic -> no key (no ordering needed)");
System.out.println(" statement topic -> customerId (per-customer order)");
System.out.println(" clickstream -> sessionId (per-session order)");
}
}
Key choice for National Bank of India:
transfers topic -> accountId (ordering + spread)
audit log topic -> no key (no ordering needed)
statement topic -> customerId (per-customer order)
clickstream -> sessionId (per-session order)
What happens with a bad key (hot partition)?
A hot partition shows several symptoms. The consumer lag for that one partition climbs while others stay near zero. The broker leading the hot partition shows high CPU and disk use. Throughput for the whole topic is capped by that one partition because a partition is processed by exactly one consumer in a group. Adding more consumers does not help because they cannot subdivide a single partition.
package com.bank.hotpartition;
public class HotPartitionSymptoms {
public static void main(String[] args) {
// Simulating lag distribution with bad key (region)
int[] lagByPartition = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 95000};
int total = 0;
for (int i = 0; i < lagByPartition.length; i++) {
total += lagByPartition[i];
System.out.printf(" Partition %2d : lag = %6d%s%n", i, lagByPartition[i],
lagByPartition[i] > 50000 ? " <-- HOT PARTITION" : "");
}
System.out.println(" Aggregate lag: " + total);
System.out.println(" 11 consumers idle, 1 overloaded");
}
}
Partition 0 : lag = 0
Partition 1 : lag = 0
Partition 2 : lag = 0
Partition 3 : lag = 0
Partition 4 : lag = 0
Partition 5 : lag = 0
Partition 6 : lag = 0
Partition 7 : lag = 0
Partition 8 : lag = 0
Partition 9 : lag = 0
Partition 10 : lag = 0
Partition 11 : lag = 95000 <-- HOT PARTITION
Aggregate lag: 95000
11 consumers idle, 1 overloaded
How do you fix a hot partition?
When the key must stay for ordering but one key is naturally huge, use a composite key to spread the load.
package com.bank.partitioner;
import org.apache.kafka.clients.producer.ProducerRecord;
public class CompositeKeyStrategy {
private static final int BUCKETS = 4;
public static String compositeKey(String accountId, String transactionId) {
int bucket = Math.abs(transactionId.hashCode()) % BUCKETS;
return accountId + "-" + bucket;
}
public static void main(String[] args) {
// High-volume corporate account spread across 4 partitions
String corpAccount = "CORP0001";
for (int i = 1; i <= 8; i++) {
String txnId = "TXN" + i;
String key = compositeKey(corpAccount, txnId);
System.out.println(" " + txnId + " -> key: " + key);
}
System.out.println(" One account spread across up to " + BUCKETS + " partitions");
}
}
TXN1 -> key: CORP0001-1
TXN2 -> key: CORP0001-2
TXN3 -> key: CORP0001-3
TXN4 -> key: CORP0001-0
TXN5 -> key: CORP0001-1
TXN6 -> key: CORP0001-2
TXN7 -> key: CORP0001-3
TXN8 -> key: CORP0001-0
One account spread across up to 4 partitions
How do you write a custom Partitioner?
package com.bank.partitioner;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.utils.Utils;
import java.util.Map;
public class TierPartitioner implements Partitioner {
private static final int PREMIUM_PARTITIONS = 2;
@Override
public int partition(String topic, Object key, byte[] keyBytes,
Object value, byte[] valueBytes, Cluster cluster) {
int numPartitions = cluster.partitionCountForTopic(topic);
if (key != null && key.toString().startsWith("PREMIUM-")) {
// Route premium accounts to first 2 partitions
return Math.abs(Utils.murmur2(keyBytes)) % PREMIUM_PARTITIONS;
}
// Route regular accounts to remaining partitions
return PREMIUM_PARTITIONS +
(Math.abs(Utils.murmur2(keyBytes)) % (numPartitions - PREMIUM_PARTITIONS));
}
@Override public void close() { }
@Override public void configure(Map<String, ?> configs) { }
}
// Configure in producer:
// props.put(ProducerConfig.PARTITIONER_CLASS_CONFIG, TierPartitioner.class.getName());
// PREMIUM-SAV2001 -> partition 0 or 1 (dedicated capacity)
// SAV2001 -> partition 2..11 (regular pool)
What are the rules and constraints?
- Ordering is only within a partition, never across partitions. Global ordering requires a single partition, which destroys parallelism.
- Changing the partition count rehashes keys, so records for a key can move to a different partition.
- A null key means round-robin (or sticky) partitioning with no ordering guarantee.
- The partition count is a capacity decision made early because increasing it later disrupts key mapping.
What should a senior engineer know?
A senior engineer recognizes that the key is a design decision that simultaneously controls correctness through ordering and performance through distribution. Key choice should be documented in the topic contract. When a single dominant key cannot be split, the senior approach is a composite key or a custom partitioner, with explicit acknowledgment of the ordering trade-off. Monitoring per-partition lag, not just aggregate lag, is how you detect a hot partition before it becomes an outage.
Visual Overview
Partition Assignment (default partitioner)
Record key: "SAV2001"
│
▼
murmur2("SAV2001") = 738291456
│
▼
738291456 % 12 partitions = 0
│
▼
┌──────────────────────────────────────────────┐
│ Topic: transfers (12 partitions) │
│ │
│ P0 [SAV2001, SAV2001, ...] ◀── all SAV2001 │
│ P1 [SAV3045, ...] │
│ P2 [SAV1122, ...] │
│ ... │
│ P11 [SAV9988, ...] │
└──────────────────────────────────────────────┘
Hot partition (bad key: region)
┌──────────────────────────────────────────────┐
│ P0 [MUMBAI, MUMBAI, MUMBAI, ...] 90% load │
│ P1 [DELHI, ...] 5% load │
│ P2 [KOLKATA, ...] 3% load │
│ P3 [CHENNAI, ...] 2% load │
└──────────────────────────────────────────────┘
Key Takeaway
Choosing a partition key is about balancing ordering and distribution. Kafka hashes the key modulo the partition count, and records with the same key always land in the same partition, which gives per-key ordering. At National Bank of India, Rajesh keys by account id because there are millions of accounts with roughly even traffic, so load spreads across all partitions while every account's debits and credits stay in order.
A bad key has low cardinality or heavy skew. If ninety percent of traffic keys to one value, that value hashes to a single partition and creates a hot partition. The consumer on that partition falls far behind while others sit idle, the broker leading it runs hot, and adding more consumers does not help because a single partition cannot be subdivided.
The senior techniques for handling skew are composite keys that fan a dominant account across a few partitions, custom partitioners that route premium accounts to dedicated partitions, and monitoring per-partition lag to detect hotspots before they become outages. The caution is that loosening the key for distribution may lose per-account ordering, so Pankaj only applies it where the ordering change is tolerable.
Remember one sentence: the partition key controls both correctness through ordering and performance through distribution, and getting it wrong wastes a cluster that has plenty of partitions but can only go as fast as its hottest one.
---