A customer places an order and then cancels it. A consumer that maintains the order’s current state should apply those events in that sequence. If it applies the cancellation first and the placement afterward, it may leave a cancelled order marked as active.
Kafka provides an ordered log within each partition, but preserving the intended sequence of business actions requires more than storing records in order. In this chapter, we’ll examine the scope of Kafka’s guarantee and follow how producers, retries, consumer processing, and recovery affect the result.
Kafka assigns an offset to each record it appends to a partition. Those offsets establish the partition’s log order. When a consumer reads forward, it receives the available application records from that partition in offset order.
Suppose orders.events carries order-lifecycle events. Kafka has successfully appended two records with key ord-1042 to partition 0: OrderPlaced at offset 40, followed by OrderCancelled at 41. Partition 1 contains events for another order.
The diagram shows the two independent sequences. The arrows indicate log order, not event timestamps or consumer completion times.
A forward reader of partition 0 encounters 40 before 41. Kafka does not require it to read partition 0 offset 40 before partition 1 offset 90. Those offsets belong to different logs and cannot establish a sequence between them.
This remains true if one consumer reads both partitions. The order in which it combines or prints records is not a topic-wide ordering guarantee. Separate topics also have separate logs, even when their partition numbers and record keys match.
The guarantee concerns the order of records that are available to read. Compaction can remove older records, transaction isolation can filter records, and replay can deliberately revisit earlier offsets. Ordered reading does not mean every published event appears exactly once or that the full history remains available forever.
It helps to distinguish the stages involved:
Correct processing requires the stages that matter to preserve the same sequence. Kafka’s log is one part of that path.
For per-order processing, the placement and cancellation should consistently reach the same partition. Using the order ID as the record key is a common way to achieve this.
That placement depends on the producers using compatible key bytes and routing behavior, with an unchanged partition count. An explicit partition choice, a custom routing rule, or a serializer change can send the same apparent key elsewhere. Increasing the number of partitions can also change the hash-based destination of future records while earlier records remain where they were.
The topic boundary matters too. If placement goes to orders.placed and cancellation goes to orders.cancelled, using key ord-1042 in both does not create a shared log order. An application that needs their sequence must coordinate the streams or use a suitable shared event stream, such as orders.events in our example.
Sharing a partition gives related records a common log, but it does not tell the producer which business event belongs first. Imagine one service publishes placement while another publishes cancellation. A delay in the first service can let the cancellation reach Kafka first, even if the business system accepted the order before the cancellation.
Both records can have the correct key and still arrive in the wrong business sequence. The source needs a defined way to order changes, such as an authoritative owner for the order and a publication path that preserves its sequence. That owner may have several instances; the requirement is coordinated ordering per order, not necessarily one process for the entire system.
Timestamps alone do not provide that coordination. Clocks can differ, events can arrive late, and two events can have equal timestamps. Kafka appends records as they arrive; it does not sort a partition by occurredAt or by the Kafka record timestamp.
Even when one producer submits related events in the right sequence, retry settings can affect their append order. For the configuration details here, assume the Apache Kafka Java producer from Kafka 4.3.
An in-flight request is a request the producer has sent to a broker but has not yet received a response to. Allowing several requests in flight improves concurrency, but without producer idempotence, retries can allow a later batch to overtake an earlier one.
Consider an intentionally unsafe configuration: enable.idempotence=false, retries enabled, and max.in.flight.requests.per.connection greater than 1. Batch A contains placement, and Batch B contains cancellation for the same order and partition.
In this separate failure scenario, the first attempt for A fails before Kafka appends it. B succeeds, and then A’s retry succeeds. The diagram shows how the retries reverse the intended sequence before it becomes the stored log order.
Consumers then read cancellation before placement because that is the order Kafka stored. Kafka has preserved the partition’s log order; the producer configuration failed to preserve submission order through the retry.
An idempotent producer uses producer sequence information so supported internal retries do not append duplicate copies or reorder the producer’s records within a partition. For Java producer ordering with idempotence, the required settings are acks=all, a positive retry count, and at most five in-flight requests per connection.
This properties excerpt makes the relevant choices explicit; it is not a complete producer configuration:
Leave retries unset to use the Java client’s positive default. Explicitly enabling idempotence also makes incompatible settings fail configuration validation rather than silently disabling the protection. These settings preserve per-partition producer order within the supported retry behavior; they do not coordinate independent producers.
Reducing in-flight requests to one can prevent the illustrated overtaking when you disable idempotence, but it reduces concurrency and does not prevent retry duplicates. Setting it to one is not required merely to preserve order with correctly configured idempotence.
Internal retries and application resubmissions are different. If a send ultimately fails, publishing later dependent events and resubmitting the earlier event afterward can still violate the business sequence. Treat an unresolved send outcome as a recovery decision for that sequence. Producer idempotence does not recognize an arbitrary new send as a duplicate just because it has the same key or JSON event ID.
Return to the correctly stored history: placement at offset 40, cancellation at 41. In a regular consumer group, one consumer receives that partition assignment once assignments settle. It reads 40 before 41.
That does not require its business operations to finish in the same order. Suppose it immediately hands both records to a general worker pool. The placement worker is slow, while the cancellation worker finishes quickly.
The following is an intentionally flawed processing design. Assume each worker blindly writes the event’s status to the reporting database, without checking event versions or state transitions.
Kafka delivered the records in order, but the application left the order in the wrong state. Moving the work to asynchronous HTTP calls can cause the same problem even without an explicit worker pool.
A straightforward design processes each partition sequentially and waits for the required effect to complete before advancing to the next record. Consumers can still process different partitions in parallel.
When the application requires ordering only per key, an application can use separate serial work queues for keys while processing unrelated keys concurrently. That design requires more coordination: it must limit queued work, preserve each key’s sequence, handle failures, and track completion across the partition.
The committed offset must not pass unfinished work. If 41 finishes while 40 is still running, committing 42 tells a replacement consumer to skip both records. Completion of a later offset does not make an earlier operation complete.
A failure can cause an application to repeat an earlier event without changing the stored log. Suppose it finishes the database update for offset 40, then crashes before saving its next position. A replacement may read 40 again before continuing to 41.
The processing history is then 40, 40, 41. This is repeated work, not a reordering of Kafka’s records. The application needs idempotent processing, where repeating an event does not repeat its business effect incorrectly. A database consumer must check the event ID and update state in one atomic operation. Separate operations let concurrent workers pass the same check.
A rebalance creates another concern when processing happens in background workers. Kafka can transfer partition ownership, but that transfer does not automatically cancel an old worker’s database request. An old owner could finish a delayed write after the new owner has applied newer state.
The diagram shows how a delayed write from the old owner can land after the new owner has moved on.
Applications using asynchronous processing must coordinate outstanding work during handoff. External systems may also need an ownership token or version condition that rejects stale writes. Preventing an old consumer from committing offsets is not the same as preventing its already-running external operation from completing.
Retry topics also affect sequence. A retry topic holds records the consumer sets aside to retry. If placement at 40 fails, sending it to a retry topic and continuing with cancellation at 41 allows the cancellation to finish first. Keeping the same key on the retry record does not restore ordering across the two topics.
If placement must succeed before the consumer can handle cancellation, later dependent work must wait. Pausing the source partition is one approach, provided the consumer continues the polling and group participation its client requires. An application can instead hold back only that key’s later work, but it then needs explicit tracking of that dependency.
This is a trade-off: strict sequencing can delay later work behind one failing event. Sending the event to a dead-letter topic, a destination for records the consumer cannot process normally, lets the application continue, but leaves a gap in the business sequence. That is acceptable only if the contract defines how the application handles the gap.
An application can make the intended business sequence explicit with an entity version. For example, an authoritative order service can publish placement as version 1 and cancellation as version 2:
orderVersion is application data, not a Kafka-assigned field. Its usefulness depends on the source assigning versions consistently as it records order changes. Adding a counter independently in several producers would not establish a reliable order.
A consumer storing the last applied version can detect repeated, older, or missing updates. The correct response depends on the event contract.
If every event contains a complete replacement snapshot, an atomic version-checked update may allow the consumer to accept version 2 and reject a later write of version 1. In that case, the newer snapshot must contain everything needed to represent the current state, and skipped versions must not carry required separate actions.
If events instead describe changes the consumer must apply in full, such as debits to an account, skipping an intermediate version can lose an operation. A version gap then needs buffering, retry, or reconciliation with an authoritative source. “Keep the largest version” is not a general repair for out-of-order events.
The version check and the state change must be atomic, meaning they succeed or fail together. Otherwise, two workers can both pass a check and still write in the wrong sequence. External actions, such as sending a refund request, need compatible sequencing and duplicate handling of their own.
Versioning helps detect or reject incorrect processing; it does not rearrange the Kafka log. It is useful when data crosses multiple streams, when delayed operations are possible, or when a consumer must defend against stale updates during recovery.
Start with the smallest set of operations that must stay in sequence. An order projection may need order-level sequencing. An account balance may need account-level sequencing. A count of independent page views may tolerate a different completion order, provided the application handles repeated events correctly.
Requiring one order across every event in a topic is more restrictive. A single-partition topic provides one log sequence and, in a regular group, one assigned reader for that topic. It also concentrates that partition’s write path and limits how the group can divide reading work. The application still needs to preserve source and processing order around that log.
Kafka transactions do not turn a multi-partition topic into one ordered log. They coordinate whether the producer commits or aborts its transactional writes. Consumers using read_committed avoid aborted transactional data, but partitions remain independent and external effects do not automatically become ordered or atomic with Kafka.
Replication and retention solve different problems too. A surviving eligible replica can preserve the log through a broker failure, but durability depends on configuration and the failure. Cleanup or data loss can leave history unavailable. An ordering guarantee is not a promise that every required event will always remain recoverable.
For the order example, a practical design aligns four responsibilities: the source establishes the order’s sequence, producers preserve it in one partition, consumers apply dependent effects in sequence, and recovery rejects duplicates or stale work without silently skipping required events. Each responsibility matters even when the other three are correct.
Kafka preserves log order within each partition. It provides no total order across partitions or topics, and ordered delivery does not guarantee ordered completion of business actions.
Preserve the required sequence throughout the application: route related events consistently, use suitable producer retry settings, coordinate concurrent processing, and handle recovery deliberately. Keys, idempotence, transactions, and event versions each address part of the problem; none alone guarantees the intended business order from source to final effect.