Message Ordering And Partitioning
Purpose
Ordering is always scoped and staged. A partitioned log commonly provides a total append
order within one partition; a consensus log or singleton sequencer can provide a wider
total order at the price of a serialized sequencing/commit point. Neither guarantees that
parallel consumers start, finish or make external effects visible in that order. State the
entity/key/partition scope and the stage—source commit, broker append, delivery, handler
completion or sink commit—rather than writing only "processed in order".
The failure this prevents is the guarantee nobody actually has. A design says "processed in
order", the implementation gets per-partition ordering, the key is absent or the partition
count changes, and an older update overwrites a newer one — days after the deploy, in one
entity, with no error anywhere. The second failure is its mirror: a single-partition topic
paying for ordering the handlers never needed, discovered when throughput has to double and
cannot.
Workflow
Inspect client/broker versions, key serialization/partitioner configuration, Java toolchain,
consumer execution and sink transactions. Kafka references use 4.1 semantics; examples are
partial and do not authorize upgrading a project or changing its delivery contract.
- Write the required scope down as a sentence. "Records for the same account id must be
applied in authoritative account-version order at sink commit" identifies a scope and stage.
Define the version authority as well. "The queue is ordered" does not, and it is
the thing that ships.
- Ask whether ordering is required at all before designing for it. Commutative handlers,
or a version guard on full snapshots, can relax arrival order for specified outcomes.
Check intermediate transitions and external effects before relaxing keys or sequencing.
See
references/designing-without-ordering.md.
- Choose the partition key from the ordering scope, then check it for skew. The entity
whose order must hold forces the key; whether that key is evenly loaded is a separate
question, owned by
sharding-and-partitioning and hot-partitions-and-rebalancing.
- Choose partition count and mapping evolution deliberately. It bounds parallel group
ownership for that topic now; default modulo partitioners commonly remap keys when count
changes. A stable custom mapping, quiescence or epoch/barrier migration can preserve a
contract, but an uncoordinated count increase cannot.
- Audit the consumer for the four things that break order inside a partition: parallel
dispatch, republished retries, DLQ skips, and rebalance overlap
(
references/where-ordering-breaks.md).
- Audit the producer: a missing key, concurrent producers for one key, and in-flight
retries that can be overtaken.
- Test by shuffling and concurrency schedules. Assert final state and every required
intermediate/external invariant. Failure can expose a handler bug, missing metadata or a
true domain ordering requirement; passing finite cases is evidence, not a general proof.
Decision block
Require per-key ordering when:
- the handler is not commutative and the record carries no version you can trust
- the entity is a state machine whose out-of-order transitions would be applied rather than
rejected — a cancel arriving before its create, a delete before its update
- a create/delete pair for one key can be reordered into a resurrection
Avoid requiring ordering when:
- the handler uses a commutative, duplicate-safe merge for the required outcome
- complete snapshots have authoritative versions and an atomic guard; intermediate effects
may be intentionally skipped (a versioned delta alone does not satisfy this)
- a rebuildable projection also tolerates out-of-order intermediate behavior
Prefer a version guard instead when:
- per-key throughput exceeds what one handler can sustain, so ordering costs capacity
- the chosen ordering key is skewed and the hot key would become a serial bottleneck
- records arrive from more than one producer, where "the order they happened" is not
observable in the log anyway
Require a global total order only when:
- the availability/throughput of one logical sequencer and ordered commit point is acceptable.
Parallel compute may surround it, but visible ordered effects must serialize or buffer/reorder
Rules
- Never write "ordered" without scope, stage and failure behavior. Broker products differ:
common scopes include channel/partition, key/message group and a single total-order log.
Redelivery, retry, failover and parallel handlers can change delivery/completion/effect order
even when append order remains intact.
- A record with no key normally relies on the client's partitioner; across multiple
partitions this does not preserve domain-key order. An explicit stable partition or a
single-partition topic can still provide append order. Inspect actual routing, including
configurations that ignore keys; the presence of a key alone proves nothing.
- With the default modulo-style mapping, per-key log ordering holds only while mapping is
stable. Adding partitions remaps some keys: new records for key K can land on a different partition while K's earlier records
sit in the old one, and there is no ordering relation between two partitions. There is no
guarantee "across the change" to reason about — the two histories are simply unordered.
- Increasing count without a mapping/cutover protocol is safe only where cross-change per-key
ordering is unnecessary. Otherwise use quiescence or a versioned migration with a per-key/
global barrier (
references/where-ordering-breaks.md).
- Ordering is the order the broker accepted records, not the order events happened: two
producers writing one key have their relative order decided by arrival. Record timestamps are
not an ordering either — clock skew between producers is unbounded.
- There is no ordering across topics, and none across partitions of one topic. A flow that
spans both has no order at all unless the records carry one.
- Consumer, parallel dispatch: unordered concurrent execution of polled records can break
per-partition completion order. Keyed dispatch —
hash(key) % workers, one queue per worker —
preserves per-key order only with stable mapping, FIFO admission and completion before
the next task (including effects), and makes one slow key block every key that shares its
worker. Choose it knowingly.
- Consumer, retry: republishing a failed record to the back of the topic or to a retry
topic lets later records for the same key overtake it. Blocking in-place retry preserves
order at the cost of head-of-line blocking on the whole partition. Both are defensible; the
bug is choosing one without noticing (
retries-and-backoff).
- Consumer, DLQ: routing one record aside and continuing means the next record for that key
is applied without the skipped effect. This violates the contract when later transitions
require it; an explicitly skippable independent event may be safe. Where
per-key order matters, pause the key or the partition instead
(
poison-messages-and-dlq).
- Consumer, rebalance: a partition can be revoked while records remain in flight, and the
new owner resumes from a checkpoint. Group assignment does not fence late side effects.
Stop admission, commit only the completed prefix of delivered records (offset numbers can
have gaps), and make the sink reject stale ownership
epochs or tolerate duplicates.
- Producer, in-flight retries: non-idempotent producers with multiple batches in flight can
reorder a failed/retried batch behind a later success. Kafka's idempotent producer preserves
order within its producer session subject to documented configuration; it does not order
independent producer instances or business events. Current defaults and allowed in-flight
limits are version-specific.
- Ordering and skew pull the key in opposite directions. When one entity is hotter than a
partition, a version guard can reject stale final-state updates but does not preserve every
intermediate transition or external effect. Decide whether coalescing, aggregation, a
sequencer plus parallel execution, or domain redesign can relax the actual invariant.
Ordering contract template
Scope: accountId
Source order: monotonically increasing account version committed by the authority
Broker order: same key maps to one partition within mapping epoch E
Delivery: at-least-once; retries may be out of delivery order
Apply rule: atomically commit v only when v == current + 1
Duplicate rule: verify event identity/payload; quarantine conflicting equal versions
Gap rule: park boundedly, then fetch snapshot/replay missing range
Visibility: account state commits in version order; notifications may arrive later
Mapping change: close E, record barrier, drain through barrier, open E+1
Security and operational edge cases
- Do not trust a caller-provided version as authority; authenticate producer identity and bind
sequence/version to the aggregate or signed event stream.
- Poison records and missing sequence values can block a key forever. Bound parking, expose
gap age and provide resync/reconciliation—not silent skip.
- Sequence counters need overflow/reset/restore semantics; database restore or producer epoch
reset can make a numerically lower valid history appear stale.
- Retention/compaction may remove the record needed to fill a gap. Recovery then requires an
authoritative snapshot with a version watermark.
References
- Where ordering holds and where it breaks — the
guarantee stated per scope with what each does and does not cover, the breakage catalogue with
the code or configuration shape that produces each, and the partition-count change as a
one-way door with the migration that avoids it. Read when auditing a consumer or a producer,
and before changing a partition count.
- Designing for no ordering requirement — version
guards, commutative operations, last-write-wins with its data-loss caveat, state-machine
guards that reject invalid transitions, and shuffle tests that challenge whether handlers are
order-insensitive. Read before accepting an ordering requirement, and when per-key throughput
is the bottleneck.
1---2name: message-ordering-and-partitioning3description: Ordering guarantees and their exact scope/stage: common logs order per partition while a global total order requires a serialized sequencer; per-key ordering depends on key-to-partition mapping remaining stable; why the partition count is nearly a one-way door; what silently breaks order in a consumer or producer; and whether ordering is required at all — version guards, commutative handlers, state-machine guards. Use when a design says messages are processed in order with no scope, when partitions are added to a live topic, when records are produced with no key, when the handler dispatches to an executor in the poll loop, when a retry republishes to the topic's tail, or when an older update overwrites a newer one. Not duplicates (delivery-semantics), repeat-safe handlers (idempotency), consumer offsets (kafka-consumers-in-java), key choice (sharding-and-partitioning), skew (hot-partitions-and-rebalancing), the failing record (poison-messages-and-dlq), or what a reader observes (consistency-models).4---56# Message Ordering And Partitioning78## Purpose910Ordering is always scoped and staged. A partitioned log commonly provides a total append11order **within one partition**; a consensus log or singleton sequencer can provide a wider12total order at the price of a serialized sequencing/commit point. Neither guarantees that13parallel consumers start, finish or make external effects visible in that order. State the14entity/key/partition scope and the stage—source commit, broker append, delivery, handler15completion or sink commit—rather than writing only "processed in order".1617The failure this prevents is the guarantee nobody actually has. A design says "processed in18order", the implementation gets per-partition ordering, the key is absent or the partition19count changes, and an older update overwrites a newer one — days after the deploy, in one20entity, with no error anywhere. The second failure is its mirror: a single-partition topic21paying for ordering the handlers never needed, discovered when throughput has to double and22cannot.2324## Workflow2526Inspect client/broker versions, key serialization/partitioner configuration, Java toolchain,27consumer execution and sink transactions. Kafka references use 4.1 semantics; examples are28partial and do not authorize upgrading a project or changing its delivery contract.29301. **Write the required scope down as a sentence.** "Records for the same account id must be31 applied in authoritative account-version order at sink commit" identifies a scope and stage.32 Define the version authority as well. "The queue is ordered" does not, and it is33 the thing that ships.342. **Ask whether ordering is required at all before designing for it.** Commutative handlers,35 or a version guard on full snapshots, can relax arrival order for specified outcomes.36 Check intermediate transitions and external effects before relaxing keys or sequencing.37 See `references/designing-without-ordering.md`.383. **Choose the partition key from the ordering scope**, then check it for skew. The entity39 whose order must hold forces the key; whether that key is evenly loaded is a separate40 question, owned by `sharding-and-partitioning` and `hot-partitions-and-rebalancing`.414. **Choose partition count and mapping evolution deliberately.** It bounds parallel group42 ownership for that topic now; default modulo partitioners commonly remap keys when count43 changes. A stable custom mapping, quiescence or epoch/barrier migration can preserve a44 contract, but an uncoordinated count increase cannot.455. **Audit the consumer for the four things that break order inside a partition**: parallel46 dispatch, republished retries, DLQ skips, and rebalance overlap47 (`references/where-ordering-breaks.md`).486. **Audit the producer**: a missing key, concurrent producers for one key, and in-flight49 retries that can be overtaken.507. **Test by shuffling and concurrency schedules.** Assert final state and every required51 intermediate/external invariant. Failure can expose a handler bug, missing metadata or a52 true domain ordering requirement; passing finite cases is evidence, not a general proof.5354## Decision block5556```text57Require per-key ordering when:58- the handler is not commutative and the record carries no version you can trust59- the entity is a state machine whose out-of-order transitions would be applied rather than60 rejected — a cancel arriving before its create, a delete before its update61- a create/delete pair for one key can be reordered into a resurrection62Avoid requiring ordering when:63- the handler uses a commutative, duplicate-safe merge for the required outcome64- complete snapshots have authoritative versions and an atomic guard; intermediate effects65 may be intentionally skipped (a versioned delta alone does not satisfy this)66- a rebuildable projection also tolerates out-of-order intermediate behavior67Prefer a version guard instead when:68- per-key throughput exceeds what one handler can sustain, so ordering costs capacity69- the chosen ordering key is skewed and the hot key would become a serial bottleneck70- records arrive from more than one producer, where "the order they happened" is not71 observable in the log anyway72Require a global total order only when:73- the availability/throughput of one logical sequencer and ordered commit point is acceptable.74 Parallel compute may surround it, but visible ordered effects must serialize or buffer/reorder75```7677## Rules7879- Never write "ordered" without scope, stage and failure behavior. Broker products differ:80 common scopes include channel/partition, key/message group and a single total-order log.81 Redelivery, retry, failover and parallel handlers can change delivery/completion/effect order82 even when append order remains intact.83- A record with **no key** normally relies on the client's partitioner; across multiple84 partitions this does not preserve domain-key order. An explicit stable partition or a85 single-partition topic can still provide append order. Inspect actual routing, including86 configurations that ignore keys; the presence of a key alone proves nothing.87- With the default modulo-style mapping, per-key log ordering holds only while mapping is88 stable. **Adding partitions remaps some keys**: new records for key K can land on a different partition while K's earlier records89 sit in the old one, and there is no ordering relation between two partitions. There is no90 guarantee "across the change" to reason about — the two histories are simply unordered.91- Increasing count without a mapping/cutover protocol is safe only where cross-change per-key92 ordering is unnecessary. Otherwise use quiescence or a versioned migration with a per-key/93 global barrier (`references/where-ordering-breaks.md`).94- Ordering is the order the broker **accepted** records, not the order events happened: two95 producers writing one key have their relative order decided by arrival. Record timestamps are96 not an ordering either — clock skew between producers is unbounded.97- There is no ordering across topics, and none across partitions of one topic. A flow that98 spans both has no order at all unless the records carry one.99- **Consumer, parallel dispatch**: unordered concurrent execution of polled records can break100 per-partition completion order. Keyed dispatch — `hash(key) % workers`, one queue per worker —101 preserves _per-key_ order only with stable mapping, FIFO admission and completion before102 the next task (including effects), and makes one slow key block every key that shares its103 worker. Choose it knowingly.104- **Consumer, retry**: republishing a failed record to the back of the topic or to a retry105 topic lets later records for the same key overtake it. Blocking in-place retry preserves106 order at the cost of head-of-line blocking on the whole partition. Both are defensible; the107 bug is choosing one without noticing (`retries-and-backoff`).108- **Consumer, DLQ**: routing one record aside and continuing means the next record for that key109 is applied without the skipped effect. This violates the contract when later transitions110 require it; an explicitly skippable independent event may be safe. Where111 per-key order matters, pause the key or the partition instead112 (`poison-messages-and-dlq`).113- **Consumer, rebalance**: a partition can be revoked while records remain in flight, and the114 new owner resumes from a checkpoint. Group assignment does not fence late side effects.115 Stop admission, commit only the completed prefix of delivered records (offset numbers can116 have gaps), and make the sink reject stale ownership117 epochs or tolerate duplicates.118- **Producer, in-flight retries**: non-idempotent producers with multiple batches in flight can119 reorder a failed/retried batch behind a later success. Kafka's idempotent producer preserves120 order within its producer session subject to documented configuration; it does not order121 independent producer instances or business events. Current defaults and allowed in-flight122 limits are version-specific.123- Ordering and skew pull the key in opposite directions. When one entity is hotter than a124 partition, a version guard can reject stale final-state updates but does not preserve every125 intermediate transition or external effect. Decide whether coalescing, aggregation, a126 sequencer plus parallel execution, or domain redesign can relax the actual invariant.127128## Ordering contract template129130```text131Scope: accountId132Source order: monotonically increasing account version committed by the authority133Broker order: same key maps to one partition within mapping epoch E134Delivery: at-least-once; retries may be out of delivery order135Apply rule: atomically commit v only when v == current + 1136Duplicate rule: verify event identity/payload; quarantine conflicting equal versions137Gap rule: park boundedly, then fetch snapshot/replay missing range138Visibility: account state commits in version order; notifications may arrive later139Mapping change: close E, record barrier, drain through barrier, open E+1140```141142## Security and operational edge cases143144- Do not trust a caller-provided version as authority; authenticate producer identity and bind145 sequence/version to the aggregate or signed event stream.146- Poison records and missing sequence values can block a key forever. Bound parking, expose147 gap age and provide resync/reconciliation—not silent skip.148- Sequence counters need overflow/reset/restore semantics; database restore or producer epoch149 reset can make a numerically lower valid history appear stale.150- Retention/compaction may remove the record needed to fill a gap. Recovery then requires an151 authoritative snapshot with a version watermark.152153## References154155- [Where ordering holds and where it breaks](references/where-ordering-breaks.md) — the156 guarantee stated per scope with what each does and does not cover, the breakage catalogue with157 the code or configuration shape that produces each, and the partition-count change as a158 one-way door with the migration that avoids it. Read when auditing a consumer or a producer,159 and before changing a partition count.160- [Designing for no ordering requirement](references/designing-without-ordering.md) — version161 guards, commutative operations, last-write-wins with its data-loss caveat, state-machine162 guards that reject invalid transitions, and shuffle tests that challenge whether handlers are163 order-insensitive. Read before accepting an ordering requirement, and when per-key throughput164 is the bottleneck.