Create your own
Lesson illustration

Defining Event Ordering in Business Workflows

Hello. In the previous lesson, you selected delivery guarantees based on the consequences of loss and duplicate processing. Ordering is the next constraint to make explicit: a duplicate-safe consumer can still make an incorrect decision if it applies dependent events in the wrong sequence.

The important correction is that “the system needs ordered events” is almost never a complete requirement. Ordering has a scope. This lesson will help you define that scope in business terms, translate it into an ordering key, and avoid paying for unnecessary global sequencing. This is foundational for later Kafka partitioning, RabbitMQ consumer design, and CDC pipelines.


Ordering is a business rule, not a broker feature

Consider these events:

  • OrderPlaced(orderId=O-104)
  • OrderPaid(orderId=O-104)
  • OrderCancelled(orderId=O-104)
  • OrderPlaced(orderId=O-105)

A Shipping service may need to know that, for order O-104, it must not issue a shipping label after a valid cancellation. But it has no business reason to wait for all activity on O-104 before handling O-105.

So the actual requirement is not “all order events must be ordered.” It is:

Events that determine the lifecycle of the same order must be observed and processed in their required sequence. Events for different orders may proceed independently.

That distinction is what allows a system to scale.

Four questions that define an ordering requirement

When someone says “we need ordering,” turn it into a precise statement by asking:

  1. Which events are related?
    Is the sequence between OrderPlaced and OrderCancelled important? What about OrderPlaced and CustomerAddressChanged?

  2. What business identity defines the scope?
    Is it one order, customer, shipment, account, device, invoice, or tenant?

  3. What specifically breaks if the order is reversed?
    Could it create invalid state, trigger an irreversible side effect, or merely produce a temporarily stale read model?

  4. Must events be processed sequentially, or can the consumer tolerate temporary disorder?
    A consumer may be able to store facts and act only after all required facts are present. In that case, strict transport ordering may be unnecessary.

A concise requirement should look like this:

For each orderId, the Fulfilment service must apply order-lifecycle events in source sequence so that cancellation prevents later fulfilment actions. Events for different orders may be processed concurrently.

This is an ordering scope: the smallest group of events that must retain a meaningful sequence.


Global ordering is usually the wrong requirement

A global order means one sequence across every event in a topic or system:

Event 1, Event 2, Event 3, Event 4, ...

It sounds safe, but it creates a single processing lane. One slow event, poison message, or downstream timeout can delay unrelated work. A surge from one customer can hold up every other customer.

For a high-volume microservice platform, global ordering is normally both unnecessary and operationally expensive. It should be reserved for rare cases where the business truly has one shared mutable sequence, such as a single auction ledger or a single regulatory sequence number.

More commonly, the needed scope is per entity:

Business requirementAppropriate ordering scopeTypical key
Apply changes to an order lifecycleOne orderorderId
Maintain a customer profile projectionOne customercustomerId
Calculate a bank account’s running balanceOne accountaccountId
Process device telemetry in sequenceOne devicedeviceId
Preserve events within a tenant, with no ordering across tenantsOne tenanttenantId
Rebuild an event-sourced aggregateOne aggregateAggregate ID

The goal is:

Require order only within the smallest business boundary whose state or decisions depend on it.

This gives you ordered handling where it matters and parallelism everywhere else.


Why ordering is not the same as timestamps

It is tempting to put an occurredAt timestamp on every event and sort by it. Timestamps are valuable metadata, but they are not a reliable ordering mechanism for live distributed processing:

  • Clocks on different hosts can disagree.
  • Network delay means an earlier event may arrive later.
  • A consumer cannot know whether a missing earlier timestamp is delayed or will never arrive.
  • Two valid events can occur within the same clock resolution.
  • A timestamp tells you when something claims to have happened; it does not necessarily establish a safe processing dependency.

If a source owns a sequential entity lifecycle, use a source-controlled sequence or version when the consumer needs to detect stale or missing changes:

{
  "id": "evt-4f1c",
  "type": "com.acme.orders.order-status-changed.v1",
  "source": "urn:acme:orders",
  "subject": "order/O-104",
  "data": {
    "orderId": "O-104",
    "version": 17,
    "status": "CANCELLED"
  }
}

Here, version: 17 has a precise meaning only if the Order service owns and assigns versions for the order. A consumer can then recognize that version 15 is older than its current version 16, or that version 18 arrived while version 17 is absent.

Do not invent a cross-service “global version” unless there is a genuinely centralized authority capable of defining it.


The broker boundary: ordered streams, not one ordered universe

Kafka topics are divided into partitions. Each partition is an append-only ordered log, but a multi-partition topic has no global order across all partitions.

Two producer clients append events to four partitions of one Kafka topic. Each partition is its own ordered log; the topic as a whole is not one globally ordered sequence.

The following short video gives the essential model before we apply it to business keys.

Partitions | Apache Kafka 101 (2025 Edition)

Watch “Partitions | Apache Kafka 101 (2025 Edition)” from Confluent Developer. It explains why Kafka partitions a topic for scale and exactly where its ordering guarantee begins and ends.

Watch partition scope to see why a partitioned topic cannot offer one guaranteed sequence across the entire topic. Then watch keyed routing. Focus on the contrast: records without a key are distributed across partitions, while records with the same key consistently go to the same partition and retain their sequence there.

Kafka’s official design documentation describes the same mechanism: the producer chooses a partition, and a key can be hashed so that all records with that key go to the same partition.

Design

Read the relevant parts of Apache Kafka’s official Design documentation to connect a business ordering key to Kafka’s partition and consumer-group model.

In “The Producer”, under “Load balancing,” read from partition selection. Focus on the example of a user ID key: it is the direct technical basis for placing one entity’s event stream in one partition. Then, in “Consumer Position,” read from the partition model. Note the limit: within one consumer group, one partition is assigned to exactly one active consumer at a time. This enables ordered consumption for that partition while other partitions can be handled in parallel.

A useful way to visualize the relationship is:

A Kafka topic is divided into partition streams, and events for a particular entity can form an ordered entity stream when they share a stable partition key.

For an orders.lifecycle topic:

  • O-104 is used as the record key for every lifecycle event about order O-104.
  • Kafka routes all those records to the same partition.
  • One active consumer in the Fulfilment consumer group handles that partition at a time.
  • Events for O-105, O-106, and other orders may map to other partitions and be processed concurrently.

The key is therefore not merely an indexing field. It is a business concurrency decision.


Choosing the right ordering key

Choosing a key that is too broad reduces parallelism. Choosing one that is too narrow breaks a business invariant.

Example: order lifecycle

Suppose the Order service emits:

  • OrderPlaced
  • OrderAddressChanged
  • OrderPaid
  • OrderCancelled
  • OrderShipped

The normal ordering scope is one order. Use:

Kafka record key = orderId

The Shipping service can then apply events for O-104 sequentially, while labels for unrelated orders are created concurrently.

Using customerId instead would be broader than needed. A customer with ten open orders would force all their order activity into one ordered lane, even though the orders are independent.

Example: account transfers

Now consider a transfer from account A-1 to account A-2. There is no single partition key that makes an event simultaneously belong to both account streams. Keying the transfer by the source account preserves source-account ordering, but not an independent ordering guarantee with the destination account.

This is a signal to stop and clarify the invariant:

  • Must the transfer be recorded as one indivisible business fact? Publish one FundsTransferred event with a stable transfer ID.
  • Must each account’s balance be correct? The account-owning service should serialize its own account changes and publish authoritative balance or ledger events.
  • Must a downstream service wait until both OrderPlaced and PaymentAuthorized are known? It may not need a strict cross-topic sequence at all; it can maintain workflow state keyed by orderId and act only once both facts are present.

Do not try to solve a cross-entity transactional problem merely by picking a clever partition key. A partition provides order for one key space, not global distributed coordination.

Example: customer lifecycle across event types

A customer must be created before its profile can be changed, and no changes should be applied after closure. These are different event types, but they describe one entity lifecycle.

They therefore need the same ordering scope:

topic: customers.lifecycle
key: customerId

Splitting them into separate customer-created, customer-address-changed, and customer-closed topics makes cross-topic order unavailable. A consumer recovering from downtime might drain the address-change backlog before it reads the create backlog.

The Confluent article below is useful because it challenges an overly simplistic “one event type, one topic” convention when lifecycle ordering matters.

Should You Put Several Event Types in the Same Kafka Topic? | Confluent

Read this Confluent article for a practical topic-design consequence of ordering scope: dependent event types about one entity may need to share both a topic and a partitioning key.

Read the opening customer example and “Ordering problems,” from the ordering problem. Notice why timestamps do not repair a live consumer’s cross-topic ordering problem. Then read the numbered guidance in “When to split topics, when to combine?”, especially the topic rules. Treat these as design heuristics, not an instruction to combine unrelated high-volume streams.

The key principle is narrower and more reliable than “put all events in one topic”:

If events must retain a fixed sequence for the same entity, they must share an ordered transport scope: in Kafka, the same topic and the same partition key.


“Received in order” versus “completed in order”

A partition’s records are read in order. That does not automatically mean your business effects complete in order.

Imagine a Spring consumer that receives events in sequence but hands each one to an asynchronous executor:

Partition 2 receives:
1. OrderPlaced(O-104)
2. OrderCancelled(O-104)

Consumer starts both handlers concurrently:
- OrderPlaced handler waits on a slow database operation.
- OrderCancelled handler completes first.

The broker delivery order was correct. The outcome may still be wrong if the placed handler later overwrites the cancelled state or sends an invalid fulfilment command.

Ordering therefore has two layers:

LayerQuestion
Transport orderDid the broker make related records available in a defined sequence?
Processing orderDid the consumer preserve that dependency through database writes, asynchronous work, retries, and external calls?

If sequence is essential, process dependent records serially for that key, or use a version-aware state update that rejects stale transitions. For example:

Update order_projection
set status = :incomingStatus,
    version = :incomingVersion
where order_id = :orderId
  and version < :incomingVersion;

This does not replace all ordering concerns, but it prevents an older state snapshot from overwriting a newer one.

The same warning applies to RabbitMQ. A single FIFO queue with one consumer can preserve straightforward sequential handling. Multiple competing consumers improve throughput, but two related messages may be processed concurrently and finish in reverse order. RabbitMQ-specific patterns for routing and consumer concurrency come in the next module; the business analysis you are doing now is the same.


When you can avoid strict ordering

Strict order is valuable when the meaning of event depends directly on the successful application of event . But many workflows can be designed around facts and conditions, rather than a presumed arrival sequence.

Suppose Warehouse receives two events for an order:

  • OrderPlaced
  • PaymentAuthorized

Warehouse only needs to create a shipping label when both facts are true. It can maintain a small workflow state record:

orderIdOrder placed?Payment authorized?Label created?
O-104yesyesyes

If PaymentAuthorized arrives first, Warehouse records that fact and waits. When OrderPlaced later arrives, the condition becomes true and it creates the label—once, using the idempotency approach from the previous lesson.

This is not an excuse to ignore ordering where a true state transition depends on it. It is a way to avoid imposing an artificial global sequence across independently produced events.

The following section of CodeOpinion’s video makes this trade-off concrete.

Message Ordering in Pub/Sub or Queue

Watch selected parts of CodeOpinion’s “Message Ordering in Pub/Sub or Queue.” It distinguishes genuine ordering needs from workflow designs that can safely record facts in either arrival order.

Start with why order matters for the two common motivations: process flow and replicated state. Then watch parallel consumers to see why competing consumers require a partition, message-group, or ordering-key mechanism for per-entity sequencing. Finally, watch workflow alternative. Focus on the order-and-payment example: a workflow can persist both facts by orderId and trigger an action only after its required conditions are satisfied.

Three valid responses to out-of-order events

When a consumer sees a later event before a required earlier event, choose deliberately:

  1. Reject or pause processing
    Appropriate when the earlier event is indispensable. For example, an event-sourced aggregate cannot safely replay a missing sequence number.

  2. Buffer and wait
    Appropriate when a missing predecessor is expected to arrive soon and there is a timeout, monitoring policy, and bounded storage plan.

  3. Record the fact and evaluate conditions later
    Appropriate for workflows where the business decision depends on a set of facts, not their arrival order.

“Apply it anyway and hope” is not a strategy.


Failure policy is part of the ordering requirement

If OrderPlaced(O-104) fails permanently but OrderPaid(O-104) is behind it in the same ordered stream, what should happen?

There is no universal answer:

  • If Fulfilment cannot safely process payment without the order, it should stop or quarantine that order’s sequence until the issue is resolved.
  • If a read model can tolerate an incomplete order temporarily, it may record the later event separately and reconcile once the missing event is restored.
  • If the failed event is malformed and unrecoverable, a dead-letter decision must explicitly state whether later events for that entity may proceed.

This produces an important operational trade-off:

The narrower the ordering scope, the narrower the blast radius of a blocked event.

A poison event for O-104 should ideally delay O-104, not every order in the system. That is another reason to avoid global ordering.


An interview-ready method

For an ordering question in a system-design interview, avoid starting with “Kafka preserves order.” Instead, reason from the domain:

  1. State the invariant.
    “A cancelled order must not later be fulfilled or shipped.”

  2. State the scope.
    “The required sequence is per order, not across all orders.”

  3. Choose the key.
    “All order lifecycle records use orderId as their partition key.”

  4. State the broker boundary accurately.
    “Kafka preserves append and consumption order within one topic partition. It does not provide global topic order or cross-topic order.”

  5. Describe consumer behavior.
    “The Fulfilment consumer must not parallelize dependent events for the same key in a way that reverses their effects. It uses event versions or guarded state transitions as a second line of defence.”

  6. Describe failure behavior.
    “If an event is missing or poison, we define whether that order is blocked, buffered, or moved to an exception workflow; unrelated orders continue.”

A compact answer might be:

“For this workflow, ordering is required per orderId because cancellation and fulfilment are mutually dependent state transitions. I will publish all lifecycle event types to one topic using orderId as the Kafka key, so they map to one partition and one active consumer within the consumer group. I do not require global ordering because unrelated orders can proceed independently. The consumer preserves processing order for an order and uses a version check to reject stale updates. A poison event blocks or quarantines only that order’s progression, with an operational recovery path.”


Key takeaways

  • An ordering scope is the smallest business group of events that must be sequenced to preserve a state invariant or decision.
  • Global ordering is usually an unnecessary throughput and availability bottleneck.
  • In Kafka, ordering is guaranteed within a partition, not across a multi-partition topic or across topics.
  • A stable business key, such as orderId or customerId, maps one entity’s records to one partition and defines its ordered lane.
  • Events that require relative order must share both a topic and partitioning key; timestamps do not repair cross-topic live-processing order.
  • Broker delivery order is not enough: consumer concurrency, asynchronous processing, retries, and side effects can still reverse completion order.
  • Some workflows can tolerate out-of-order arrival by recording facts and acting only when all required conditions are present.
  • Every ordering design needs an explicit policy for missing, delayed, and poison events.

Next, you will turn this analysis into a practical at-least-once consumer design: defining a stable idempotency key and a durable deduplication strategy.

Can't find a good explanation? Sign up and we'll make it for you

Sign up