A ride-hailing platform must process billions of location and trip events per day, with per-trip ordering. Design the partitioning, consumer scaling and failure handling.
Show the full answer Hide the answer
What is being tested
Whether you scope ordering guarantees to what the business actually needs, and whether you have thought past the happy path to lag, skew and poison messages.
Partitioning
Key by trip_id. Ordering in a log is guaranteed only within a partition, so the key defines
the ordering scope. Per-trip ordering is what the requirement actually asks for — global ordering
would require a single partition, which caps throughput at one consumer and is never what anyone
means.
Choose the partition count for the parallelism you will need in two years, not today, because increasing it later changes the key-to-partition mapping and breaks ordering across the change. Over-provisioning partitions is cheap; re-partitioning a live stream is not.
Separate streams by event class. High-frequency location pings and low-frequency trip lifecycle events have completely different volumes, retention needs and consumers. One stream forces the lifecycle consumers to read through the pings.
Consumer scaling
Consumer groups: each partition is assigned to exactly one consumer in a group, so parallelism is capped by partition count. Adding consumers beyond that does nothing, which surprises people during an incident.
Design consumers to be stateless and idempotent. Delivery is at least once; a rebalance or a crash replays from the last committed offset. Commit offsets after processing, not before, and accept the resulting duplicates rather than risking loss.
Skew, which is the hard part
Location events do not distribute evenly. A city centre at surge time generates orders of magnitude more events than a suburb at 4am. If any part of the key correlates with geography, one partition saturates while others idle.
Keying on trip_id largely avoids this, because trip IDs are effectively random — which is exactly
why it is the right key. If a business requirement forced keying on city, the mitigation is a
composite key (city:bucket) that splits hot cities across partitions while preserving whatever
ordering is genuinely required within a bucket.
Backpressure and failure handling
- Lag as the primary SLI. Consumer lag per partition, alerted on age rather than message count. A stalled consumer produces no errors — everything looks healthy while the business goes stale.
- Bounded internal buffers in consumers. A consumer that reads faster than it can write downstream and buffers in memory turns lag into an out-of-memory crash, which causes a rebalance, which slows everyone.
- Dead-letter topic for messages that fail repeatedly, with the original message, the error, and enough context to reprocess later. Without it, one malformed event stalls a partition forever.
- Retry topics with increasing delays rather than in-line retries, so a slow-failing message does not block the partition behind it.
- Rebalance storms. A consumer slow to process gets evicted, triggering a rebalance that pauses the whole group, making more consumers slow. Tune the poll interval and max records per poll to the actual processing time, and prefer incremental rebalancing.
Batching and cost
At billions of events a day, per-event overhead dominates the bill. Batch on produce, compress, and size batches against the latency requirement — location pings can tolerate a second of batching, trip lifecycle events cannot.
What a strong answer adds
Naming what should not be on the stream. Not every interaction is an event: if a caller needs an answer to proceed, a synchronous call is simpler and safer, and forcing it through a log produces correlation IDs, reply topics and timeouts — a request/response call with worse tooling.