How message ordering works in distributed systems
How messaging systems maintain ordering guarantees using partition keys, sequence numbers, vector clocks, and causal ordering when total order is too expensive.
The Problem Statement
Interviewer: "You are building a chat application where messages sometimes appear out of order. A user sends 'Hello' then 'How are you?' but the recipient sees them reversed. How do you guarantee message ordering in a distributed system?"
This question tests three things: your understanding of why ordering is hard in distributed systems (network delays, multiple producers, parallel consumers), your knowledge of the ordering spectrum (total order, partial order, causal order), and whether you can pick the right guarantee for the right use case instead of over-engineering.
I have seen candidates either say "just use Kafka" (which shows no understanding of why Kafka's ordering works) or try to implement a global total order (which is prohibitively expensive at scale). The strong answer picks the minimum ordering guarantee that satisfies the use case and explains why stronger guarantees are not needed.
Clarifying the Scenario
You: "Before I jump into the solution, I want to understand the ordering requirements."
You: "When you say 'out of order,' are we talking about messages within a single conversation appearing in the wrong sequence, or messages across different conversations being globally misordered?"
Interviewer: "Within a single conversation. Two users in a chat room should see messages in the same order."
You: "Got it. And how many messages per second are we talking about? A few hundred in a group chat, or millions across the entire platform?"
Interviewer: "Think WhatsApp scale. Billions of messages per day, but each conversation is relatively low-throughput."
You: "OK. And do we need exactly-once delivery, or is at-least-once with idempotent consumers acceptable?"
Interviewer: "At-least-once is fine as long as messages appear in order and duplicates are handled."
You: "I will structure my answer in three parts. First, why ordering is hard and the three levels of ordering guarantees. Second, how partition-key ordering solves the chat use case. Third, the edge cases like out-of-order delivery, consumer rebalancing, and causal ordering across conversations."
My Approach
I break message ordering into four layers of understanding:
- Why ordering is inherently hard: Network partitions, variable latency, multiple producers, parallel consumers
- The ordering spectrum: Total order versus partial order versus causal order, and the cost of each
- Partition-key ordering: How Kafka and similar systems give you per-partition ordering cheaply
- Consumer-side ordering: Handling rebalancing, retries, and out-of-order delivery at the consumer
The key insight is that total ordering across all messages is almost never necessary and incredibly expensive. Per-conversation ordering (partial order) is cheap and sufficient for nearly every chat, event sourcing, or workflow use case.
The Architecture
The architecture works like this. When User A sends a message to a conversation, the API gateway assigns a sequence number and publishes the message to Kafka using the conversation_id as the partition key. Because all messages for the same conversation go to the same partition, and Kafka guarantees order within a partition, the messages are stored in exactly the order they were produced.
Each partition is consumed by exactly one consumer in the consumer group. This consumer processes messages sequentially, writes them to the message store with their sequence numbers, and pushes them to connected clients via WebSockets.
The partition key is the critical design choice. By hashing conversation_id to a partition, we get per-conversation ordering without paying for global ordering. Two different conversations can be processed in parallel on different partitions with no ordering guarantees between them, and that is exactly what we want.
The single most important sentence in this article: total ordering is expensive, partial ordering is cheap. If you take away one thing, it is this: identify the minimum ordering boundary that satisfies your use case (usually a single entity like a conversation or user) and partition by that key. Do not reach for global ordering unless you absolutely need it.
The Ordering Spectrum
Before diving into implementation, I want to be precise about the three levels of ordering guarantees because candidates often confuse them.
Total order means every consumer sees every event in exactly the same sequence. If events A, B, C happen, every consumer sees A then B then C. This requires consensus (Raft, Paxos, ZAB) and is what systems like ZooKeeper and etcd provide. The cost: every write requires a majority quorum acknowledgment, limiting throughput to thousands of writes per second, not millions.
Partial order (also called per-key order) means events with the same key are ordered, but events with different keys have no ordering relationship. Kafka partitions give you this. All messages with conversation_id=42 are ordered, but there is no guarantee about the relative ordering of messages in conversation_id=42 versus conversation_id=99.
Causal order sits between total and partial. If event B was caused by (or observed) event A, then every consumer sees A before B. But events that are truly independent (no causal relationship) can appear in any order. This is what vector clocks and hybrid logical clocks enable.
For chat: partial order (per-conversation) is almost always sufficient. Total order is overkill and would be a bottleneck. Causal order is only needed for cross-conversation dependencies, which are rare.
Partition-Key Ordering in Kafka
This is the workhorse of message ordering in modern distributed systems. Understanding how Kafka's partition-level ordering works is essential for any system design interview involving messaging.
A Kafka partition is an append-only log. Every message gets a monotonically increasing offset. The producer sends messages to the partition, and the broker appends them in arrival order. The consumer reads messages in offset order, one at a time, and commits the offset after successful processing.
This gives us a strong guarantee: within a single partition, messages are totally ordered by offset. If Producer A sends "Hello" before "How are you?" and both go to the same partition, the consumer will always see "Hello" first.
But here is the subtlety that catches people. The ordering guarantee is at the partition level, not the topic level. If two messages go to different partitions (because they have different partition keys), there is no ordering guarantee between them. This is by design, not a bug. It allows Kafka to scale horizontally by distributing partitions across brokers.
The most common mistake in interviews is saying "Kafka guarantees message ordering." It does not guarantee global ordering. It guarantees per-partition ordering. If your producer sends messages to different partitions (different keys), those messages can arrive at consumers in any relative order. Always specify the scope of the ordering guarantee.
For your interview: the sentence to say is "I would partition by conversation_id so all messages for a conversation land on the same Kafka partition, giving me ordered delivery within a conversation without paying for global ordering."
Producer Ordering Guarantees
One nuance that separates senior from junior answers: the producer's ordering guarantee depends on configuration.
By default, Kafka allows up to 5 in-flight requests per connection (max.in.flight.requests.per.connection=5). If the first request fails and is retried while the second request succeeds, the messages arrive at the broker out of order. This means even within a single producer, ordering can break under failures.
The fix: enable idempotent producers (enable.idempotence=true). This automatically sets max.in.flight.requests.per.connection=5 with idempotent sequencing, meaning the broker deduplicates and reorders retried messages. The producer assigns a monotonically increasing sequence number to each message, and the broker only accepts messages whose sequence number is exactly one greater than the last seen.
Without idempotent producers, you would need to set max.in.flight.requests.per.connection=1 to guarantee ordering, which cuts throughput significantly. Idempotent producers give you both ordering and throughput.
Kafka's idempotent producer guarantee is per-partition, per-producer-session. If the producer restarts and gets a new producer ID, the sequence numbers reset. For cross-restart guarantees, you need Kafka transactions, which assign a stable transactional.id to the producer.
Causal Ordering with Vector Clocks
Partition-key ordering solves the single-conversation case, but some systems need a stronger guarantee: causal ordering. If Alice sends a message, Bob reads it and replies, then Carol should see Alice's message before Bob's reply, even if they are in different conversations or channels.
This is causal ordering. Event B is causally dependent on event A if B could only have happened after observing A. Causal ordering guarantees that if B depends on A, every observer sees A before B.
Let me walk through the three main clocking mechanisms candidates should know.
Lamport clocks are the simplest. Each node maintains a single counter that increments on every event. When sending a message, attach the counter. When receiving, set your counter to max(local, received) + 1. This gives you a total ordering of events, but it cannot distinguish between causally related events and concurrent events. Two events with Lamport timestamps 5 and 6 might be causally related (5 happened before 6) or completely independent.
Vector clocks solve this. Instead of one counter, each node maintains a vector with one counter per node. Node A's clock might be [A:3, B:2, C:0], meaning A has seen 3 of its own events, 2 from B, and none from C. When A sends a message to B, it includes its full vector. B merges by taking the max of each component. The key property: you can compare two vector clocks and determine if one happened before the other, or if they are concurrent (neither happened before the other).
Hybrid Logical Clocks (HLC) combine physical timestamps with a logical counter. They give you the best of both worlds: causality tracking like vector clocks, but with timestamps that are close to physical time (useful for human-readable ordering). CockroachDB and Spanner use variants of this approach.
Concrete Vector Clock Example
Let me walk through a concrete example, because the theory of vector clocks is clearer with actual numbers.
Three nodes (A, B, C) start with clocks [0, 0, 0]:
- A sends a message. A's clock becomes
[1, 0, 0]. The message carries[1, 0, 0]. - B receives A's message. B merges:
max([0,0,0], [1,0,0]) = [1,0,0], then increments its own position:[1, 1, 0]. - B sends a message. B's clock becomes
[1, 2, 0]. The message carries[1, 2, 0]. - C sends a message (independently, without having received anything). C's clock becomes
[0, 0, 1]. The message carries[0, 0, 1].
Now compare events 3 and 4:
- B's message:
[1, 2, 0] - C's message:
[0, 0, 1]
B has a higher A-component (1 > 0), but C has a higher C-component (1 > 0). Neither dominates the other, so these events are concurrent. No causal relationship exists between them.
Compare events 1 and 3:
- A's message:
[1, 0, 0] - B's second message:
[1, 2, 0]
Every component of A's clock is less than or equal to B's, with at least one strict less-than. So A's message happened before B's second message. This makes sense: B received A's message before sending its own.
This ability to detect concurrency is the key advantage of vector clocks over Lamport clocks. In a database with multi-leader replication, concurrent writes to the same key need special conflict resolution (last-writer-wins, merge, or user intervention). A Lamport clock cannot tell you if two writes are concurrent or causally related.
In a chat application, you rarely need vector clocks. Per-conversation sequence numbers (partition-key ordering) handle 99% of cases. Vector clocks matter when you need cross-conversation causal ordering, like ensuring that "Alice left the group" is visible to all members before any message sent after the leave. Most teams use Lamport or hybrid clocks for this, not full vector clocks.
The honest answer for most chat systems: you do not need vector clocks. Per-conversation sequence numbers handle ordering within a conversation. For the rare cross-conversation case (like "Alice left the group"), use a simple happens-before relationship tracked by the server that manages group membership. Do not over-engineer this.
Real-World Ordering in Chat Systems
Let me walk through how a production chat system (like WhatsApp or Slack) actually handles ordering, because the theory and the practice look quite different.
WhatsApp approach: Each message gets a server-assigned timestamp and a per-conversation sequence number. The server is the single source of truth for ordering within a conversation. Even if two users send messages "simultaneously," the server serializes them and assigns consecutive sequence numbers. The client displays messages sorted by sequence number, not by local timestamp.
Slack approach: Slack uses a ts (timestamp) field as both the message ID and the sort key. The timestamp is assigned by the server with enough precision (6 decimal places) to avoid collisions. Messages are sorted by ts within a channel. Because the server assigns timestamps, clock skew between user devices is irrelevant.
Discord approach: Discord uses Snowflake IDs (timestamp + worker ID + sequence number) as message IDs. The timestamp component means messages are roughly time-ordered, but the sequence number component handles multiple messages within the same millisecond. Snowflake IDs are generated at the gateway, not by the client.
The pattern all three share: the server assigns the ordering key, not the client. Client clocks are unreliable. Server clocks are synchronized via NTP and produce monotonic sequence numbers. This is the simplest and most reliable approach.
Handling Out-of-Order Delivery in Consumers
Even with perfect ordering at the broker level, messages can appear out of order at the consumer. This happens during consumer rebalancing, retry storms, and when consumers process messages at different speeds. This deep dive covers how to build a consumer that maintains ordering guarantees even when the delivery layer does not.
There are three scenarios where ordering breaks at the consumer level.
Scenario 1: Rebalancing. When a consumer fails or a new consumer joins the group, Kafka reassigns partitions. The new consumer starts reading from the last committed offset. If the old consumer processed a message but did not commit the offset, the new consumer will reprocess that message. This does not break ordering (the messages still arrive in offset order), but it causes duplicates. The fix is idempotent writes: check if the message already exists before writing.
Scenario 2: Multi-threaded consumers. If a consumer hands off messages to a thread pool for parallel processing, messages may complete out of order. Message 101 might finish processing before message 100 if 100 involves a slow database query. The fix: use a single thread per partition, or use a resequencing buffer that holds messages until all prior messages have been processed.
Scenario 3: Retry with backoff. If processing message 100 fails and is retried with exponential backoff, message 101 arrives and is processed successfully first. Now 101 is in the database but 100 is not. The fix: stop processing the partition when a message fails. Retry the failed message in-place before moving to the next offset. This is Kafka's default behavior (auto-commit disabled, sequential processing).
The Dead Letter Queue Problem
When a message consistently fails processing (a poison pill), it blocks the entire partition. All subsequent messages for every conversation on that partition are stuck. This is the ordering vs. availability tradeoff.
Three approaches to handle this:
Approach 1: Retry with a limit, then skip. Retry the failed message N times, then log it, skip it, and continue processing. The downside: you now have a gap in the conversation's message sequence. The consumer must handle this gap gracefully (show an "undelivered message" placeholder or backfill asynchronously).
Approach 2: Dead letter queue (DLQ). After N retries, move the failed message to a separate DLQ topic for manual investigation. Continue processing the partition. The DLQ preserves the message for later replay, but the consumer must handle the temporary gap.
Approach 3: Per-key queuing within the consumer. Route each message to a per-conversation in-memory queue. If processing fails for conversation 42, only that conversation's queue is blocked. Messages for other conversations on the same partition continue processing in parallel. This gives you per-key isolation without losing ordering, but adds complexity and memory overhead.
My recommendation: start with approach 1 (retry then skip), use the DLQ for forensics, and only move to approach 3 if a single hot conversation's failures are blocking the entire partition.
Exactly-Once Processing Across Systems
The hardest ordering problem in practice is not ordering itself but ensuring each message is processed exactly once while maintaining order. Kafka gives you exactly-once within its own ecosystem, but the moment you write to an external database, you are back to at-least-once unless you coordinate carefully.
The standard pattern is the outbox pattern with offset tracking:
- The consumer reads message at offset N from Kafka.
- In a single database transaction, the consumer: (a) writes the message to the message table, and (b) updates a
kafka_offsetstable with partition and offset N. - The consumer does NOT commit the offset to Kafka.
- On restart, the consumer reads the last processed offset from the database (not from Kafka) and seeks to that position.
This makes the database the single source of truth for "what has been processed." Even if the consumer crashes between the database commit and the Kafka offset commit, the database has the correct state and the consumer will skip already-processed messages on restart.
The tradeoff: this only works when the consumer writes to a single database. If the consumer writes to multiple systems (database + search index + cache), you need distributed transactions or eventual consistency with idempotent replays.
Ordering During Consumer Group Rebalancing
Consumer rebalancing is the most common source of ordering issues in production Kafka deployments. Let me walk through what happens step by step, because this is a question interviewers love to ask as a follow-up.
When a consumer leaves a group (crash, deployment, or scaling event), Kafka redistributes its partitions to the remaining consumers. During this window:
-
Stop-the-world pause: All consumers in the group pause processing for the duration of the rebalance (with the default "eager" rebalance protocol). This can take 5-30 seconds depending on group size.
-
Offset gap risk: The departing consumer may have processed messages beyond its last committed offset. The new consumer starts from the committed offset, reprocessing those messages. This is why idempotent writes are essential.
-
Temporary ordering violation: If you use cooperative rebalancing (the newer incremental protocol), consumers continue processing their non-revoked partitions during the rebalance. But revoked partitions go through a brief gap where no consumer is processing them, potentially causing a burst of messages when the new consumer picks them up.
The mitigation strategy I recommend:
- Use cooperative sticky assignor (
partition.assignment.strategy=org.apache.kafka.clients.consumer.CooperativeStickyAssignor) to minimize partition movement during rebalancing. - Set session timeout (
session.timeout.ms=30000) and heartbeat interval (heartbeat.interval.ms=10000) appropriately so the group coordinator detects failures quickly without false positives. - Implement a drain-on-revoke callback: when a partition is revoked, finish processing any in-flight messages before releasing it.
- Store processed offsets in the application database (not just in Kafka) so you can deduplicate on restart.
This diagram shows the eager rebalancing protocol. With cooperative rebalancing, only the affected partitions (P0, P1, P2) are paused. Consumer 2 continues processing P3, P4, P5 uninterrupted, which significantly reduces the impact.
The golden rule for ordered consumers: one thread per partition, sequential processing, commit after success, and idempotent writes. If you follow these four rules, ordering is guaranteed even through rebalancing and retries.
The Tricky Parts
-
Producer ordering is not guaranteed without idempotence. If a producer sends message A, the broker acknowledges it, then the producer sends message B, but the ack for A is lost, the producer retries A. Now the broker has A, B, A. Without idempotent producers (
enable.idempotence=true), you get duplicates and potentially reordering. Kafka's idempotent producer assigns a sequence number to each message and the broker deduplicates retries. I always mention this in interviews because it shows I understand the failure modes, not just the happy path. -
Partition count changes break ordering. If you increase the number of Kafka partitions, the hash function
conversation_id % Nproduces different results for the same conversation. Messages that were on partition 3 might now go to partition 7. You cannot simply add partitions to an ordered topic. Either freeze the partition count at creation time, or use a consistent hashing scheme with a mapping table that you update during migration. In practice, most teams over-provision partitions (create 128 or 256 partitions upfront) and accept the empty-partition overhead rather than deal with migration pain later. -
Exactly-once across systems is the real problem. Kafka can give you exactly-once within its ecosystem (idempotent producers + transactions + read_committed consumers). But when you write from Kafka to an external database, you need to coordinate the Kafka offset commit with the database write. The standard pattern is to store the Kafka offset in the same database transaction as the message write, creating an atomic "process + commit" operation. Without this coordination, you get at-least-once delivery and must handle duplicates in the database layer.
-
Causal ordering across services is harder than within one service. If service A publishes an event "user created" and service B publishes "order placed" (which depends on the user existing), there is no built-in mechanism to ensure consumers see "user created" before "order placed" unless both events go through the same ordered channel. In practice, consumers must handle out-of-order cross-service events by buffering and retrying. The consumer of "order placed" checks the user store, and if the user does not exist yet, it delays processing for a few hundred milliseconds and retries.
-
Clock skew makes timestamp-based ordering unreliable. Even with NTP synchronization, clocks on different machines can differ by 10-100ms. In a burst of messages, a message sent 50ms later by a machine with a slightly behind clock will have an earlier timestamp. This is why Kafka uses broker-assigned offsets (monotonic within a partition) rather than producer timestamps for ordering. I have seen production issues where engineers used
event_timefor ordering instead of the Kafka offset, and messages appeared scrambled during high-load periods. -
Consumer lag creates ordering illusions. When a consumer falls behind (high lag), it processes old messages while new messages pile up. If the consumer writes to a real-time feed (like a chat UI), users see stale messages appearing after newer ones. The fix: track consumer lag per partition and alert when it exceeds a threshold (e.g., 10,000 messages or 30 seconds). For real-time applications, consider using the committed offset's timestamp to detect staleness and warn users that messages may be delayed.
-
Multi-region ordering is a fundamentally different problem. If you have Kafka clusters in US-East and EU-West (for latency), and a conversation spans regions, you get two independent ordered logs. MirrorMaker 2 can replicate between clusters, but replication lag (50-200ms for cross-region) means the ordering guarantee now has a window of uncertainty. For truly global ordering, you need a single Kafka cluster (which means cross-region latency for all writes) or an application-level merge strategy that reconciles the two logs using timestamps and sequence numbers In chat systems, the ordering unit is the conversation. Messages within a conversation must be ordered. Messages across conversations do not need ordering. The volume per conversation is low (tens of messages per second at most), but the total volume across all conversations is enormous.
In event sourcing, the ordering unit is the aggregate (a user, an order, an account). Events for the same aggregate must be strictly ordered because each event builds on the previous state. If event 3 is "balance = 100" and event 4 is "withdraw 50," processing them out of order produces the wrong balance. The volume per aggregate is typically very low, but the correctness requirement is absolute.
In stream processing (analytics, ETL), ordering often matters within a time window but not globally. You might need all events for the same user within a 5-minute window to be ordered for sessionization, but events from different windows can be processed independently.
| Conflating delivery order with processing order | "Kafka delivers in order so it is processed in order" | Multi-threaded consumers can process messages out of order even if they arrive in order | "Per-partition ordering plus single-threaded consumption ensures both delivery and processing order." | | Ignoring the producer side | "Just sort by timestamp at the consumer" | Producer retries can reorder messages at the broker if idempotence is disabled | "Enable idempotent producers so retries do not cause reordering at the broker level." | The takeaway: always ask "what is the ordering boundary?" before choosing an implementation. The answer determines whether you need total order, per-key order, or no order at all.
-
Causal ordering across services is harder than within one service. If service A publishes an event "user created" and service B publishes "order placed" (which depends on the user existing), there is no built-in mechanism to ensure consumers see "user created" before "order placed" unless both events go through the same ordered channel. In practice, consumers must handle out-of-order cross-service events by buffering and retrying.
-
Clock skew makes timestamp-based ordering unreliable. Even with NTP synchronization, clocks on different machines can differ by 10-100ms. In a burst of messages, a message sent 50ms later by a machine with a slightly behind clock will have an earlier timestamp. This is why Kafka uses broker-assigned offsets (monotonic within a partition) rather than producer timestamps for ordering.
What Most People Get Wrong
| Mistake | What they say | Why it is wrong | What to say instead |
|---|---|---|---|
| Over-promising ordering | "Kafka guarantees message ordering" | Only per-partition ordering, not global | "Kafka guarantees ordering within a partition. I use partition keys to get per-conversation ordering." |
| Using wall clocks | "Sort by timestamp" | Clocks drift across machines, not monotonic | "Wall clocks are unreliable for ordering. Use broker-assigned sequence numbers or logical clocks." |
| Single partition for ordering | "Use one partition so everything is ordered" | Creates a bottleneck, cannot scale | "Partition by the ordering key (conversation_id) to get per-key ordering with horizontal scalability." |
| Ignoring consumer rebalancing | "The consumer reads in order" | During rebalancing, messages can be redelivered and duplicated | "Use idempotent writes and commit offsets only after successful processing to handle rebalancing." |
| Total order when partial suffices | "We need all messages globally ordered" | Total order requires consensus (Raft/Paxos), extremely expensive | "Per-conversation ordering is sufficient for chat. Total order is only needed for things like distributed transactions." |
One mistake I see in interviews: candidates start talking about consensus algorithms (Raft, Paxos) when the interviewer asks about message ordering. Consensus gives you total order, which is overkill for almost every messaging use case. Start with partition-key ordering, explain why it is sufficient, and only mention consensus if the interviewer explicitly asks about total ordering or distributed transactions. Jumping to Raft signals that you do not understand the tradeoff spectrum.
How I Would Communicate This in an Interview
Here is how I would actually say this in 90 seconds:
"Message ordering in distributed systems is hard because of network delays, multiple producers, and parallel consumers. The key insight is to pick the weakest ordering guarantee that satisfies your use case.
For a chat application, I need per-conversation ordering, not global ordering. I would use Kafka with conversation_id as the partition key. This ensures all messages for the same conversation land on the same partition, and Kafka guarantees order within a partition via monotonically increasing offsets.
On the producer side, I enable idempotent producers to prevent duplicate messages from retries. The producer assigns a sequence number, and the broker deduplicates.
On the consumer side, I use a single thread per partition with manual offset commits. I commit the offset only after successfully writing the message to the database. This prevents out-of-order processing and ensures at-least-once delivery. Idempotent writes to the database handle the duplicates.
For the rare case where I need causal ordering across conversations, like ensuring 'Alice left the group' is visible before post-leave messages, I attach a hybrid logical clock timestamp and use a small resequencing buffer on the consumer side.
The tricky parts are partition rebalancing (where consumers can reprocess messages), exactly-once delivery across Kafka and the database (solved by storing the offset in the same transaction), and the fact that you cannot change partition counts without breaking key-based ordering.
The design principle: total order requires consensus and is expensive. Partial order (per-key) is cheap and scalable. Choose the minimum guarantee that satisfies the business requirement."
Interview Cheat Sheet
- Trigger: "How do you order messages?" β Immediately clarify: "Do we need total order, per-key order, or causal order?"
- Partition key is the answer: "Partition by the ordering key (conversation_id, user_id, order_id). Kafka gives per-partition ordering for free."
- Never use wall clocks: "Wall clocks drift across machines. Use broker-assigned offsets or logical clocks for ordering."
- Consumer golden rule: "One thread per partition, sequential processing, commit after success, idempotent writes."
- Idempotent producer: "Enable
enable.idempotence=trueto prevent duplicate messages from producer retries." - Exactly-once across systems: "Store the Kafka offset in the same database transaction as the message write. This makes process-and-commit atomic."
- Partition count is immutable: "Once you choose a partition count for an ordered topic, you cannot change it without breaking key-based routing."
- Causal ordering: "Only needed when cross-key events have happens-before relationships. Use hybrid logical clocks, not vector clocks, for most systems."
- Rebalancing safety: "During consumer rebalancing, the new consumer starts from the last committed offset. Design for at-least-once delivery with idempotent writes."
- Scale signal: "Per-partition throughput is bounded. If one key gets too hot (a viral group chat), you need sub-partitioning or key-level sharding within the consumer."
Test Your Understanding
Quick Recap
- Total ordering across all messages requires consensus (Raft, Paxos) and is prohibitively expensive at scale. Only use it when absolutely necessary, like distributed transactions.
- Partition-key ordering is the sweet spot for most systems: partition by the ordering key (conversation_id, order_id) and get per-key ordering with horizontal scalability.
- Kafka guarantees order within a partition via monotonically increasing offsets, not via timestamps or arrival time at the producer.
- Idempotent producers (
enable.idempotence=true) prevent message duplication and reordering caused by producer retries. - Consumer ordering requires single-threaded processing per partition, manual offset commits after successful processing, and idempotent writes to handle redelivery.
- Wall clocks cannot be trusted for ordering because of clock drift across machines. Use broker-assigned sequence numbers or logical clocks (Lamport, vector, or hybrid).
- Causal ordering across different keys/topics requires explicit dependency tracking via logical clocks or shared state, and is rarely worth the complexity for chat applications.
- Partition count changes break key-based routing, so over-provision at topic creation or use a mapping table instead of modular hashing.
Related Concepts
- How Kafka works internally: Deep dive into the partition log structure, consumer groups, and offset management that power the ordering guarantees discussed here.
- How event sourcing works: Event sourcing depends on ordered, immutable event logs. The ordering challenges in this article directly apply to building event stores.
- How exactly-once delivery works: The "exactly-once across systems" problem (Kafka offset + database write in one transaction) is covered in depth there.
- How distributed consensus works: Total ordering requires consensus algorithms like Raft or Paxos. Understanding why consensus is expensive explains why per-key ordering is preferred.
- How vector clocks work: Extended treatment of vector clocks, version vectors, and conflict resolution in multi-leader replication systems.