When Does Kafka Ordering Actually Matter?
Published: June 25, 2026
Background — Two Async Pipelines After a Message Is Sent
Section titled “Background — Two Async Pipelines After a Message Is Sent”When a user sends a message in Minichat, two things happen asynchronously through Kafka.
The first is real-time delivery. The message is consumed and broadcast via WebSocket to all users currently connected to the chatroom. The chat-message topic handles this.
The second is DB state update. The chatroom’s lastWrittenMessageId is updated to the latest message ID, which is used to display the most recent message in the chatroom list and to calculate unread counts. The user-chat-update topic handles this.
Both pipelines run through the same Kafka cluster.
The Problem — Kafka Can Reorder Messages
Section titled “The Problem — Kafka Can Reorder Messages”When a Kafka topic has multiple partitions, message ordering across partitions is not guaranteed. The order in which the Producer sends messages and the order in which the Consumer receives them can differ.
If chat-message events arrive out of order:
User A sends “Hello” → “How are you?” in sequence. But another user’s screen shows “How are you?” → “Hello”. The conversation no longer makes sense.
If user-chat-update events arrive out of order:
| Time | Consumer processes | lastWrittenMessageId |
|---|---|---|
| T1 | Message A (id=170) → UPDATE | 170 |
| T2 | Message B (id=150) → UPDATE | 150 ← overwritten |
Message A (id=170) was generated before message B (id=150). If the Consumer processes them in the opposite order — A then B — the stored value regresses from 170 to 150.
Both topics face the possibility of ordering reversal.
Initial Assumption — Use Kafka Keys for Both Topics
Section titled “Initial Assumption — Use Kafka Keys for Both Topics”At first, the answer seemed straightforward. If ordering matters, use a Kafka key.
By using chatId as the key for both topics, messages from the same chatroom would always land in the same partition, and Kafka would preserve their order. It was the safest solution.
But before applying it to every topic, I stopped and asked a different question.
What exactly does each Consumer do with the data?
Analyzing the Data — Same Kafka, Different Requirements
Section titled “Analyzing the Data — Same Kafka, Different Requirements”Both topics run on the same Kafka cluster.
At first glance, they seem to have the same ordering problem.
But once I looked at what each Consumer actually does, they turned out to be solving completely different problems.
chat-message — ordering is the product.
The sequence users see is the conversation.
If “Hello” appears after “How are you?”, the product behaves incorrectly. Ordering is not an implementation detail—it is part of the user experience itself.
user-chat-update — ordering is not the product.
This Consumer does not reconstruct a conversation. It only maintains lastWrittenMessageId in the database.
As shown in Part 1, message IDs are generated by the Snowflake algorithm and increase monotonically. The Consumer does not care whether event 170 arrives before event 150. It only needs the final maximum value.
As long as the update logic preserves max(current, newValue), out-of-order delivery has no effect on correctness.
Trade-off — Ordering vs Parallelism
Section titled “Trade-off — Ordering vs Parallelism”The data characteristics were clear. The next question was whether ordering justified giving up partition-level parallelism.
With a key: All events sharing the same chatId land in a single partition. The Consumer processes them serially, so ordering is preserved. But load distribution depends entirely on key distribution — a hot chatroom can bottleneck a single partition.
Without a key: Kafka distributes events across partitions via round-robin. Load is evenly spread, but the Consumer may receive events in any order. This only works if the downstream system can absorb out-of-order delivery.
| Criterion | Key (ordering) | No key (parallelism) |
|---|---|---|
| Ordering guarantee | Preserved within partition | None |
| Load distribution | Depends on key distribution | Even |
| Precondition | None | Data must tolerate reordering |
The Decision
Section titled “The Decision”The final decision was intentionally different for each topic.
chat-message: Use chatId as Kafka key. The order in which messages appear on a user’s screen is the user experience itself. Ordering is worth sacrificing parallelism for.
user-chat-update: No key. lastWrittenMessageId is a max-value problem. The Consumer’s conditional UPDATE absorbs ordering reversals. Ordering is worth sacrificing to gain parallelism.
The reason was not Kafka. The reason was the data model.
Implementation
Section titled “Implementation”ChatMessageProducer.java — with key
public void send(MessageSendEvent event) { String key = String.valueOf(event.getTalkMessage().getChatId()); kafkaTemplate.send(TOPIC, key, event);}chatId is passed as the Kafka key. Messages from the same chatroom enter the same partition, and the Consumer broadcasts them to WebSocket in order.
UserChatUpdateProducer.java — without key
public void sendUserChatUpdateEvent(UserChatUpdateEvent event) { kafkaTemplate.send(TOPIC, event);}No key. Kafka distributes events across partitions via round-robin.
UserChatUpdateConsumer.java — conditional UPDATE
public void consume(UserChatUpdateEvent event) { List<Long> userChatIds = userChatRepository.findIdsByChatId(event.getChatId()); if (userChatIds.isEmpty()) return;
userChatJdbcRepository.batchUpdateLastWrittenMessage( userChatIds, event.getLastMessageId(), event.getTimestamp() );}The SQL executed by batchUpdateLastWrittenMessage:
UPDATE user_chatSET last_written_message_id = ?, last_message_timestamp = ?WHERE id = ? AND is_deleted = false AND (last_written_message_id IS NULL OR last_written_message_id < ?)The WHERE last_written_message_id < ? clause absorbs ordering reversals. If a larger value is already recorded, the UPDATE simply does not execute.
Validation — Final State Under Ordering Reversal
Section titled “Validation — Final State Under Ordering Reversal”To confirm that events sent without a key produce a correct final state despite reordering, I ran a test against a real Kafka cluster.
Test Setup
- Kafka: localhost:9092, 3 partitions
- DB: H2 in-memory
- Spring Boot 3.3.1 + Spring Kafka
Scenario 1: 5 Shuffled Events
Section titled “Scenario 1: 5 Shuffled Events”Send order: [150, 130, 170, 120, 160] — no key.
| Receive order | messageId | Result |
|---|---|---|
| #1 | 120 | UPDATE |
| #2 | 160 | UPDATE |
| #3 | 150 | SKIP |
| #4 | 130 | SKIP |
| #5 | 170 | UPDATE |
Final DB value: 170 ✅
The Consumer received events in a completely different order (120 → 160 → 150 → 130 → 170) than they were sent (150 → 130 → 170 → 120 → 160). But the WHERE < ? clause skipped every value smaller than the current maximum. The final state converged to the correct max.
Scenario 2: 100 Shuffled Events
Section titled “Scenario 2: 100 Shuffled Events”IDs 1 through 100, shuffled and sent without a key.
| Metric | Value |
|---|---|
| UPDATE count | 7 |
| SKIP count | 93 |
| Final DB value | 100 ✅ |
93 out of 100 events were absorbed. Regardless of how severely ordering was disrupted, the conditional UPDATE always preserved the maximum value.
Same Pattern, Different Layer
Section titled “Same Pattern, Different Layer”The pattern used here is the same one from Part 1, applied at a different layer.
| Part | Layer | Data | Update Strategy |
|---|---|---|---|
| Part 1 | Redis | lastReadMessageId | Lua Script: if ARGV[1] > current |
| Part 2 | DB | lastWrittenMessageId | SQL: WHERE last_written_message_id < ? |
Both rely on the same precondition — Snowflake IDs guarantee monotonic increase — and apply the same rule: “update only if the new value is greater.” In Part 1, this pattern replaced a distributed lock. In Part 2, it replaced Kafka’s ordering guarantee.
Conclusion
Section titled “Conclusion”The same infrastructure does not require the same strategy. The question was never “Should Kafka preserve ordering?”
The real question was “Does this data actually need ordering?”
For chat-message, the answer was yes.
For user-chat-update, the answer was no.