SysDesignPrep.com
Interactive concepts

Message ordering: an interactive simulator of parallel consumers, partitions and retries

Events are produced in order, but nothing guarantees they are processed in order. Parallel consumers, retries and multiple partitions all let a later event overtake an earlier one, so an order can go from shipped back to paid. Systems that need order get it per key: route each entity’s events through one partition, or make the consumer reject events older than what it has already applied.

The playground sends five orders’ status events through four consumers three ways. Turn on a failed event that is retried, or shuffle the processing times, and compare the final statuses.

Five orders each emit created, paid, shipped and delivered, and each event sets the order's status. Four consumers take between 1 and 4 time units per event. With one shared queue, a consumer can finish an order's later event before another finishes its earlier one: 2 events land after a newer one, the status goes backwards, and 1 order ends in the wrong state. Partitioning by order id keeps each order on one consumer, so its events apply in order, at the cost of finishing at 18 instead of 13: a busy consumer cannot hand work to an idle one. Versioned writes accept events in any order and drop any older than the stored status: 2 stale events ignored, and every order ends delivered, finishing as fast as the shared queue.

Shared queueany consumer takes the next event
#1 shipped#2 delivered#3 delivered#4 delivered#5 delivered
Status went backwards
2
Wrong final status
1
Finished at
13
Partitioned by order idone consumer owns each order
#1 delivered#2 delivered#3 delivered#4 delivered#5 delivered
Status went backwards
0
Wrong final status
0
Finished at
18
Shared queue, versioned writesthe database drops events older than it holds
#1 delivered#2 delivered#3 delivered#4 delivered#5 delivered
Status went backwards
0
Wrong final status
0
Finished at
13
Bars: 2P is order 2 paid; C created, P paid, S shipped, D delivered.applied out of orderignored as stalefailed, retried later

Three ways order breaks

  • Parallel consumers on one queue. Two consumers take an order’s paid and shipped events a moment apart; if paid takes longer, shipped lands first and paid then overwrites it. In the playground, a shared queue sends 2 events in after newer ones, and one order ends at shipped instead of delivered.
  • Retries. A failed event sent to the back of the queue, or to a retry topic, is processed after its successors. Even partitioning by order id does not help: the retried paid event lands after shipped and delivered, and the order ends at paid.
  • Producers. A producer retrying sends with several requests in flight can reorder them; Kafka’s idempotent producer keeps them in order.

Getting order back

  • Partition by key. Kafka keeps order within a partition, SQS FIFO within a message group, and Pub/Sub within an ordering key. Use the entity id as the key and one consumer handles each entity at a time. The price is parallelism: in the playground the partitioned run finishes at 18 instead of 13, because a busy consumer cannot hand its orders to an idle one, and one hot key can hold up its whole partition.
  • Retry in place. Keep retrying the failed event before moving on, or park every later event for that key until it succeeds, rather than skipping ahead.
  • Version the writes. Give every event a sequence number per entity and apply it only if it is newer than the stored version. Order no longer matters for the final state: in the playground every order ends delivered, at the shared queue’s speed, with stale events dropped.
  • Prefer commutative updates where you can: increments and set unions give the same result in any order.

Global order across all events is rarely needed and does not scale, since it allows only one consumer. Ask which events must be ordered relative to each other; usually it is only events about the same thing.

Keyed producing and versioned writes

# producer: key by order id, so all of an order's events go to one partition, in order
producer = Producer({"bootstrap.servers": BROKERS, "enable.idempotence": True})
producer.produce("order-events", key=str(order_id), value=json.dumps(event))

-- consumer: apply an event only if it is newer than what the row reflects
UPDATE orders
SET status = :status, version = :seq
WHERE id = :order_id AND version < :seq;
-- 0 rows updated: a newer event already landed, so this one is dropped

In a system design interview

State the ordering you need and how you get it: “Order events are keyed by order id, so each order’s events stay in one partition and one consumer applies them in sequence. Each event carries a per-order version, and the status update is conditional on it, so a replay or a late retry cannot move an order backwards. Failures for an order block only that order, in a retry queue keyed the same way.” Expect the follow-ups: hot keys, what happens when partitions are added and keys move, and ordering across two different entities.

Read the guide to message ordering or Kafka internals, see Kafka partitions and consumer groups and delivery semantics, or watch a strong candidate design a message queue.

Questions people ask

Do message queues guarantee ordering?

Usually only within a narrow scope. Kafka preserves order within a partition, SQS FIFO within a message group, and Pub/Sub within an ordering key. Standard queues with competing consumers do not preserve order at all, because consumers process messages in parallel and at different speeds.

How do you keep events for the same entity in order?

Route them by key: use the entity id as the partition key or message group, so all its events go to one partition and one consumer processes them sequentially. Different entities still run in parallel across partitions.

Why do retries break message ordering?

A failed message that is retried later, from the back of the queue or a separate retry topic, is processed after messages that came after it. To keep order, retry in place before moving on, or park all later messages for the same key until the failed one succeeds.

What are versioned writes?

Each event carries a sequence number or version for its entity, and the consumer applies it only if it is newer than the version stored, for example with UPDATE ... WHERE version < :seq. Late or duplicate events are then ignored, so the final state is correct whatever order they arrive in.

What does ordering cost?

Parallelism. With per-key ordering, one consumer handles each key at a time, so a slow or hot key delays every event behind it in its partition, and idle consumers cannot help. Global ordering would allow only one consumer for everything.

Can Kafka producers reorder messages?

Without idempotence, a producer that retries a failed batch while other batches are in flight can write them out of order. With enable.idempotence, the broker uses sequence numbers to keep each producer’s writes to a partition in order without duplicates.

Do you need global ordering?

Rarely. Most systems need events about the same entity, such as one order or one account, in order, but not events about different entities. Identify the key that defines what must be ordered and preserve order only within it.