Skip to content

AxonOps — AI-Native Control Plane for Open Source Data Platforms

Kafka Producer Partitioning

Partitioning determines which partition receives each record. The partitioning strategy affects message ordering, load distribution, and consumer parallelism.


ProducerTopic: orders (6 partitions)Record(key, value)PartitionerP0P1P2P3P4P5Partitioner determinestarget partition based on:- Key (if present)- Round-robin (null key, pre-2.4)- Sticky (null key, 2.4+)- Custom logic
AspectImpact
OrderingRecords with same key go to same partition (ordered)
ParallelismMore partitions enable more consumers
Load balancingEven distribution prevents hot partitions
LocalityRelated records can be co-located

Key is null?yesnoKafka version >= 2.4?yesnoStay on current partitionuntil batch is full orlinger.ms expiresSticky PartitioningCycle throughall partitionsRound-robinhash = murmur2(keyBytes)Same key always mapsto same partition(assuming partition count unchanged)partition = (hash & 0x7fffffff) % numPartitions

When a key is provided, the default partitioner uses murmur2 hashing.

// Same key always goes to same partition
producer.send(new ProducerRecord<>("orders", "user-123", order1)); // Partition X
producer.send(new ProducerRecord<>("orders", "user-123", order2)); // Partition X
producer.send(new ProducerRecord<>("orders", "user-456", order3)); // Partition Y

Hash calculation:

partition = (murmur2(keyBytes) & 0x7fffffff) % numPartitions
PropertyGuarantee
DeterministicSame key → same partition (given same partition count)
DistributionGenerally uniform across partitions
OrderingRecords with same key maintain order

For null keys, sticky partitioning improves batching efficiency.

Sticky PartitioningTime 0-5msBatch Full (5ms)Time 5-10msRecord 1 → P0Record 2 → P0Record 3 → P0Send batch to P0Record 4 → P2Record 5 → P2Record 6 → P2After batch completes,switch to new partition
BehaviorRound-Robin (Pre-2.4)Sticky (2.4+)
Record distributionEvenBursty per partition
Batch efficiencyPoorExcellent
Network utilizationHigherLower
LatencyHigher (small batches)Lower (full batches)

public interface Partitioner extends Configurable, Closeable {
/**
* Compute the partition for the given record.
*
* @param topic The topic name
* @param key The key (or null if no key)
* @param keyBytes Serialized key (or null)
* @param value The value (or null if no value)
* @param valueBytes Serialized value (or null)
* @param cluster Current cluster metadata
* @return The partition number
*/
int partition(String topic, Object key, byte[] keyBytes,
Object value, byte[] valueBytes, Cluster cluster);
}
public class RegionPartitioner implements Partitioner {
private Map<String, List<Integer>> regionPartitions;
@Override
public void configure(Map<String, ?> configs) {
// US: partitions 0-2, EU: 3-5, APAC: 6-8
regionPartitions = Map.of(
"US", List.of(0, 1, 2),
"EU", List.of(3, 4, 5),
"APAC", List.of(6, 7, 8)
);
}
@Override
public int partition(String topic, Object key, byte[] keyBytes,
Object value, byte[] valueBytes, Cluster cluster) {
Order order = (Order) value;
String region = order.getRegion();
List<Integer> partitions = regionPartitions.getOrDefault(
region, List.of(0, 1, 2, 3, 4, 5, 6, 7, 8));
// Hash within region's partitions
int hash = Utils.murmur2(keyBytes);
int index = (hash & 0x7fffffff) % partitions.size();
return partitions.get(index);
}
@Override
public void close() {}
}
public class PriorityPartitioner implements Partitioner {
// High priority: partitions 0-1, Normal: 2-7, Low: 8-9
@Override
public int partition(String topic, Object key, byte[] keyBytes,
Object value, byte[] valueBytes, Cluster cluster) {
Event event = (Event) value;
int hash = keyBytes != null ? Utils.murmur2(keyBytes) : 0;
int hashAbs = hash & 0x7fffffff;
switch (event.getPriority()) {
case HIGH:
return hashAbs % 2; // Partitions 0-1
case NORMAL:
return 2 + (hashAbs % 6); // Partitions 2-7
case LOW:
return 8 + (hashAbs % 2); // Partitions 8-9
default:
return hashAbs % cluster.partitionCountForTopic(topic);
}
}
}
partitioner.class=com.example.RegionPartitioner
# Additional partitioner-specific config
region.partition.mapping=US:0-2,EU:3-5,APAC:6-8

// Send to specific partition (bypasses partitioner)
ProducerRecord<String, String> record = new ProducerRecord<>(
"orders", // topic
2, // partition (explicit)
"key", // key
"value" // value
);
producer.send(record);
Use CaseRationale
Ordered replayReplay events to same partition
TestingControl partition for test isolation
MigrationMaintain partition assignment during migration
Special routingOverride partitioner for specific records

Key Selection CriteriaGood KeysPoor KeysUser IDOrder IDSession IDEntity IDTimestampRandom UUIDBoolean flagVery common value- High cardinality- Even distribution- Meaningful grouping- Hot partitions- No logical grouping- Skewed distribution
PatternKey FormatUse Case
Entity keyuser-123Group by entity
Composite keytenant-1:user-123Multi-tenant systems
Time-bucketed2024-01:user-123Time-series with ordering
Hash keymd5(payload)Deduplication
// WRONG: All orders from large customer go to one partition
String key = order.getCustomerId(); // Customer "BigCorp" = 80% of orders
// BETTER: Distribute within customer
String key = order.getCustomerId() + ":" + order.getOrderId();
// OR: Use composite key with salt
String key = order.getCustomerId() + ":" + (order.getOrderId().hashCode() % 10);

Partition Count Changes Affect Key Distribution

Adding partitions changes the key-to-partition mapping. Records with the same key may go to different partitions before and after the change.

Before: 4 PartitionsAfter: 6 Partitionskey-A → P0key-B → P1key-C → P2key-D → P3key-A → P4key-B → P1key-C → P0key-D → P3Same keys may mapto different partitionsBreaking ordering guaranteesAdd 2 partitions
StrategyDescription
Over-provision partitionsCreate more partitions than immediately needed
Use partition-aware keysInclude partition hint in key
Custom partitionerMaintain backward compatibility
Reprocess dataRebuild from source after partition change

// Monitor partition distribution
Map<Integer, Long> partitionCounts = new HashMap<>();
producer.send(record, (metadata, exception) -> {
if (metadata != null) {
partitionCounts.merge(metadata.partition(), 1L, Long::sum);
}
});
// Check for skew
long total = partitionCounts.values().stream().mapToLong(Long::longValue).sum();
double expected = total / (double) partitionCounts.size();
partitionCounts.forEach((partition, count) -> {
double ratio = count / expected;
if (ratio > 1.5 || ratio < 0.5) {
log.warn("Partition {} is skewed: {} records (expected ~{})",
partition, count, (long) expected);
}
});
CauseSolution
Few high-volume keysAdd salt to key, use composite key
Poor hash distributionUse different key field
Null keys with round-robinUpgrade to sticky partitioner
Custom partitioner bugReview and fix partitioner logic

public class AvailabilityAwarePartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes,
Object value, byte[] valueBytes, Cluster cluster) {
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
List<PartitionInfo> available = cluster.availablePartitionsForTopic(topic);
if (available.isEmpty()) {
// All partitions unavailable; use any partition
int hash = keyBytes != null ? Utils.murmur2(keyBytes) : 0;
return (hash & 0x7fffffff) % partitions.size();
}
if (keyBytes == null) {
// Sticky to available partition
return available.get(ThreadLocalRandom.current().nextInt(available.size()))
.partition();
}
// Hash to available partition
int hash = Utils.murmur2(keyBytes);
return available.get((hash & 0x7fffffff) % available.size()).partition();
}
}
# Wait for partition availability
partitioner.availability.timeout.ms=0 # 0 = don't wait

MetricDescription
record-send-total per partitionRecords sent to each partition
byte-total per partitionBytes sent to each partition
batch-size-avg per partitionBatch sizes per partition
IssueSymptomSolution
Hot partitionOne partition with much higher trafficImprove key distribution
Empty partitionsSome partitions receive no trafficCheck key cardinality
Ordering violationsOut-of-order processingEnsure consistent key usage