Event-Driven Architecture and Asynchronous Messaging Questions
Designing systems around events and message passing: publish/subscribe, message queues, event streaming, choreography versus orchestration, and decoupling producers from consumers. Covers delivery semantics (at-least-once, at-most-once), ordering, backpressure, dead-letter handling, and the operational tradeoffs of asynchronous flows. Includes async processing patterns for offloading slow work.
How would you choose a Kafka partitioning key for a user-events topic such that ordering is preserved per user but partitions are balanced across the cluster? Discuss hashing strategies, handling hot users, and approaches for multi-tenant fairness when some tenants generate far more events than others.
Sample Answer
Direct answer
Use user_id itself as the partition key so Kafka's partitioner (a mod-based hash of the key) always routes every event for a given user to the same partition, which is what gives per-user ordering, it falls directly out of using the same key for the same entity, not something built separately. Balance across the cluster then comes from having enough distinct keys and a reasonably uniform hash, with a deliberate, separate remediation for the minority of disproportionately hot keys.
Structured elaboration
Hashing strategy. Kafka's default partitioner computes hash(key) % num_partitions (using a murmur2-based hash internally); the exact hash function matters less than the property that it is deterministic, the same key always maps to the same partition number for a fixed partition count. Because the partition assignment is a pure function of the key, ordering per key is automatic once you commit to using that key consistently, not an extra mechanism you build on top.
Why "balanced" is a statistical property, not a guarantee. With many distinct user ids and a well-distributed hash, the law of large numbers makes total load spread roughly evenly across partitions, if every key generates traffic at roughly the same rate. The question specifically names the case where that assumption breaks: hot users.
Handling hot users. Two remediations, with different costs:
- Key salting (sub-partitioning): append a bounded suffix to a hot user's key only once their rate crosses a threshold (
user_id#0throughuser_id#K-1), spreading that one user's traffic across up to K partitions. The explicit cost: per-user ordering across the salted shards is no longer guaranteed, only ordering within each shard is. This is a real trade-off to name out loud, not a free win, if the consuming system genuinely needs strict per-user order even for hot users, salting defeats that and a downstream re-sequencing step would be needed, which mostly defeats the purpose of salting in the first place. - Isolating known hot keys: if hot users are identifiable ahead of time (service accounts, bots, high-volume enterprise integrations), route them to a separate topic or partition set with its own capacity plan, keeping the main topic's "roughly uniform" assumption valid for the long tail of ordinary users.
Multi-tenant fairness. Partitioning purely by user_id does nothing to stop one tenant (an account with many users) from occupying a disproportionate share of partitions and consumer capacity relative to a smaller tenant, since partition assignment has no concept of tenant at all. If fairness across tenants is a hard requirement: give large or noisy tenants a dedicated topic or partition range so they cannot starve others' consumer lag budget, and layer per-tenant rate limiting or quota enforcement on the consumer or producer side (the same token-bucket idea used for rate limiting generally), since a static partition assignment cannot adapt to traffic that changes over time the way an explicit quota can.
Worked example
A small, executed illustration of the mechanism (using Python's built-in zlib.crc32 purely as a stand-in deterministic hash function to demonstrate the mod-based mechanic; Kafka's actual default partitioner uses a different, murmur2-based hash internally, the mechanism, not the specific hash, is what this illustrates).
import zlib
num_partitions = 6
for uid in ["user-1042", "user-8831", "user-2207", "user-77519"]:
h = zlib.crc32(uid.encode())
print(uid, "partition=", h % num_partitions)
user-1042 partition= 3
user-8831 partition= 4
user-2207 partition= 4
user-77519 partition= 0
Every event for user-1042 always lands on partition 3, giving that user strict per-user order for free. Now simulate salting the hot user user-8831 into 12 sub-keys (user-8831#0 through user-8831#11):
counts = {}
for shard in range(12):
key = f"user-8831#{shard}"
p = zlib.crc32(key.encode()) % num_partitions
counts[p] = counts.get(p, 0) + 1
print(counts)
{4: 5, 0: 1, 5: 5, 1: 1}
The 12 salted sub-keys land on 4 of the 6 partitions instead of all landing on partition 4 as the unsalted key would. The spread is not perfectly even at this small sample size (partitions 4 and 5 got 5 each, partitions 0 and 1 got 1 each), which is expected statistical noise at n = 12, not a flaw in the technique; at real production volumes for a genuinely hot key, the same law of large numbers that balances ordinary keys also evens out a hot key's salted shards.
Trade-offs and pitfalls
- Salting trades away strict per-user order for the salted user specifically, in exchange for spreading their load; state this cost explicitly when proposing it, do not present it as a free fix.
- Changing the partition count later reshuffles the mod-based assignment for every existing key, not just new ones, since
hash(key) % num_partitionschanges for essentially every key whennum_partitionschanges. Under-provisioning partition count up front makes a later increase a disruptive, coordinated migration, not an incremental capacity add. - "Balanced across the cluster" and "fair across tenants" are different guarantees: the first is about hash uniformity over many keys, the second is about the business meaning of who owns which keys, no hashing scheme fixes tenant fairness on its own.
- Picking the wrong entity granularity for the key, for example partitioning by
session_idwhen the actual ordering requirement is per-user across sessions, silently breaks the guarantee the system was supposed to provide.
Explain the difference between message queues and event streams using: (1) simple high-level definitions, (2) step-by-step architecture differences (point-to-point vs publish-subscribe, retention and consumer offsets), (3) concrete use cases (task queue for background jobs vs event log for analytics), (4) discuss delivery semantics (at-least-once, at-most-once, exactly-once) and operational trade-offs.
Sample Answer
Direct answer
A message queue is a transient work-distribution mechanism: a message is delivered to one consumer and then typically removed. An event stream is a durable, ordered, append-only log that many consumers can read and re-read independently, each tracking their own position. Use a queue for background jobs that need to happen once; use an event stream when multiple consumers need to replay or independently process the same history of events, such as for analytics.
Structured elaboration
(1) Simple high-level definitions. A message queue holds discrete units of work; once a consumer successfully processes (and acknowledges) a message, it is generally gone from the queue. An event stream holds an ordered, append-only sequence of events that persists for a configured retention period regardless of whether any consumer has read them yet, and multiple independent consumers can read the same stream at their own pace.
(2) Step-by-step architecture differences.
- Delivery model: a queue is point-to-point: competing consumers pull from the same backlog, and each message goes to exactly one of them. An event stream is publish-subscribe at the storage layer: every consumer group gets its own full view of the log, so 5 independent consumer groups can each read every event, not compete for it.
- Retention: a queue typically discards a message once acknowledged (or after a short retention window for safety); an event stream retains events for a configured duration (hours to indefinitely) independent of consumption, which is what makes replay possible.
- Consumer offsets: a queue broker tracks per-message state (delivered, in-flight, acknowledged) on the broker's behalf; a consumer of an event stream tracks its own offset (a pointer into the log marking "I have processed up through here"), which the consumer can reset backward to reprocess history or forward to skip it, something a queue's consume-once model does not support.
(3) Concrete use cases. A task queue for background jobs (resize an uploaded image, send a single email, generate a PDF) is the queue use case: each job runs once, and once done there is no reason to revisit it. An event log for analytics (every purchase event, retained for 30+ days, read independently by a real-time dashboard consumer, a daily batch-aggregation consumer, and a fraud-detection consumer, each at their own offset) is the event-stream use case: the same history needs to serve multiple, independently-paced readers, and a new consumer added next month should be able to read events from before it existed.
(4) Delivery semantics and operational trade-offs. Both queues and streams commonly offer at-least-once delivery (a message may be delivered more than once, so consumers must be idempotent) as the practical default, since it is the easiest guarantee to build reliably. At-most-once (a message may be silently lost but never duplicated) is rarely chosen deliberately; it falls out of not retrying failed deliveries and is generally the wrong default for anything that matters. Exactly-once, from the consumer's experience, is achievable at the application layer by combining at-least-once delivery with a consumer-side idempotency check (a deduplication store keyed by message ID with a time-to-live, TTL), rather than relying on a broker guarantee alone; broker-level exactly-once semantics (EOS) protocols exist on some platforms but their internals are a streaming-platform implementation detail, not something the consuming application needs to reason about. Operationally, queues are simpler to run (bounded backlog, straightforward monitoring of queue depth) but lose replay ability; event streams give you replay and multiple independent consumer groups at the cost of needing retention-size planning, offset-lag monitoring per consumer group, and generally a heavier broker to operate.
Worked example
An e-commerce platform's "PurchaseCompleted" event needs to reach a real-time inventory-decrement consumer (must process within seconds), a nightly analytics-aggregation job (reads the whole day's events once, in batch), and a newly-added fraud-review consumer (needs to backfill and reprocess the last 7 days of purchases to retrain a rule set). This is only possible on an event stream: the inventory consumer reads from the current offset forward, the analytics job resets its offset to the start of each day, and the fraud-review consumer resets its offset back 7 days on rollout, all three reading the same retained log independently. If "PurchaseCompleted" had instead been placed on a point-to-point queue, only one of the three would ever have received a given event, and none of them could replay history that had already been consumed.
Trade-offs and pitfalls
The most common pitfall is reaching for an event stream by default because it seems more capable, then paying its operational cost (retention planning, per-consumer-group offset-lag monitoring, a heavier broker) for a workload that was really a simple once-only task queue. The opposite pitfall is placing something that needed independent replay, like the fraud-review backfill above, onto a queue and discovering months later that the history needed to replay it was already gone. A senior answer treats "does more than one independent party need to read this, possibly at different times or replaying history" as the deciding question, not "which technology is newer or more scalable."
An incident: duplicated messages caused double charges for customers. As the Solutions Architect, outline the incident response steps you would take immediately to mitigate harm, the structure of a root-cause analysis you would lead, short-term customer remediation steps, and permanent architectural fixes you would recommend to prevent recurrence. Include stakeholder communication and post-mortem deliverables.
Sample Answer
Direct answer
Treat this as two parallel tracks that start immediately: stop the bleeding (mitigate customer harm right now) and understand what happened (root-cause analysis), followed by short-term remediation for affected customers and permanent architectural fixes so it can't recur, all wrapped in disciplined stakeholder communication and a written post-mortem. The specific technical cause of duplicated messages causing double charges is almost always a missing or broken idempotency layer on the payment-consuming side combined with at-least-once delivery redelivering a message that had already been successfully processed once.
Structured elaboration
Immediate mitigation steps. First, stop new harm: if the duplicate-charge pattern is ongoing, pause or circuit-break the specific consumer/flow causing it rather than the whole platform, to limit blast radius while investigation continues. Identify the affected population precisely (which orders/customers, over what time window) using the event/transaction logs, since the fix and the remediation both depend on knowing the exact scope, not an estimate. If the root cause is quickly identifiable as a missing idempotency check, a fast, reviewed hotfix (even a narrow one, like adding a dedup check keyed on the payment request's event id before charging) can stop new duplicates while the fuller fix is designed.
Root-cause analysis (RCA) structure. Build a timeline: when did duplicate messages start being produced or redelivered, what changed at that time (a deploy, a broker/network incident, a downstream latency spike that triggered retries), and where specifically in the pipeline did the duplicate turn into a duplicate charge (a duplicate message alone is not a customer-facing bug if the consumer was idempotent; the RCA needs to identify why it wasn't, or why the idempotency layer that should have existed didn't catch it). Trace both the "why did a duplicate happen at all" (transport/delivery layer, usually retry behavior or a network blip) and the "why did it become a double charge" (the deeper bug: no idempotency key, or an idempotency key with too short a deduplication window (TTL, time-to-live) that expired before the redelivery arrived, or a completely missing check).
Short-term customer remediation. Identify every affected customer from the precise scope established above, issue refunds for the duplicate charge specifically (not just "credits," an actual reversal of the erroneous charge), and proactively notify affected customers rather than waiting for them to notice and complain, since a company-initiated fix lands very differently than a customer discovering it on their statement. Track remediation completion as its own metric (percentage of affected customers refunded and notified) so this doesn't quietly stall after the initial incident excitement fades.
Permanent architectural fixes. Add (or fix) a consumer-side idempotency layer at the payment-processing boundary specifically: before charging, check whether this exact request (keyed by a stable identifier from the originating event, not a value the consumer invents itself) has already been successfully charged, and if so, skip the charge and return the prior result. Set the dedup store's TTL to comfortably exceed the maximum realistic redelivery delay the messaging system can produce, not an arbitrarily short window. Add monitoring specifically for duplicate-delivery rate and for any charge attempt that the idempotency layer catches and blocks, so a redelivery event is visible on a dashboard before it ever becomes a customer complaint again.
Absorbed angle: remediation tiering (fix now versus fix long-term). Explicitly separate and communicate two different fix tracks rather than presenting one undifferentiated "we fixed it": the immediate mitigation (the narrow hotfix or circuit-break that stopped new harm within hours) and the permanent fix (the properly designed, reviewed, tested idempotency layer and monitoring that closes the actual gap, which may reasonably take longer). Naming this distinction explicitly to stakeholders prevents the common failure of either declaring victory too early on a stopgap, or blocking the urgent mitigation while waiting for the fully engineered permanent fix.
Stakeholder communication. Give leadership and any customer-facing teams (support, account management) a clear, factual, non-technical summary early: what happened, who's affected, what's being done right now, and when the next update will come, then keep that update cadence even if the update is "still investigating, next update at X time." For affected customers specifically, communication should be proactive, plain-language, and lead with the remediation (the refund), not a technical explanation of message queues.
Post-mortem deliverables. A written, blameless timeline of the incident; the confirmed root cause at both the delivery layer and the application layer; the immediate mitigation and permanent fix, with the "fix now versus fix long-term" distinction preserved; the customer remediation scope and completion status; and a small number of concrete, owned, dated action items (not a long unowned wishlist) to close the gap the RCA identified, including the monitoring/alerting that will catch this class of bug earlier next time.
Worked example
The RCA timeline shows a network blip between the payment-consumer service and the broker at 14:12, causing the broker's client library to retry an unacknowledged charge request; the original request had, in fact, already been processed successfully by the payment consumer just before the connection dropped, so the retry was a true duplicate delivery of an already-completed action. Because the payment consumer had no idempotency check, it charged the customer a second time. The immediate mitigation at 14:40 was a targeted hotfix rejecting a charge request whose transaction reference had already appeared in the prior 24 hours' charge log, deployed within the hour to stop new duplicates. The permanent fix, delivered over the following two weeks, replaced that stopgap with a proper idempotency-key check backed by a dedicated dedup store with a TTL set well beyond the broker's maximum realistic redelivery delay, plus a new dashboard alert on any charge blocked by that check. Customer remediation identified 340 affected orders from the transaction log in the 14:12-14:40 window, all refunded and proactively emailed within 48 hours, with completion tracked to 100% before the post-mortem was closed.
Trade-offs and pitfalls
The most common mistake is treating "we deployed a fix" as the end of the incident before customer remediation is actually complete and verified, the technical fix and the customer-harm fix are two different deliverables with two different completion criteria. A second common mistake is an RCA that stops at "a network blip caused a retry" without going one layer deeper to "and the consumer wasn't idempotent, which is the actual gap," the network blip is nearly unavoidable in a distributed system; the missing idempotency layer is the fixable root cause. Rushing a broad permanent fix under incident pressure, without the fix-now/fix-later distinction, risks shipping a rushed, under-tested change to a payment path, exactly where a rushed change is most dangerous; the narrow, reviewable stopgap exists precisely to buy time for the permanent fix to be done properly.
Compare synchronous REST and asynchronous messaging for inter-service communication. For each approach explain: failure semantics, coupling, observability, latency, consistency guarantees, and operational complexity. Provide examples of when you'd prefer one over the other.
Sample Answer
Direct answer
Synchronous REST and asynchronous messaging differ on every axis that matters for inter-service communication: failure semantics, coupling, observability, latency, consistency guarantees, and operational complexity. REST gives an immediate, explicit success/failure answer at the cost of temporal coupling; asynchronous messaging decouples caller and callee at the cost of an implicit, eventually-resolved answer. Prefer REST when the caller needs a definitive result to proceed; prefer messaging when the interaction can tolerate delay and the two sides should be able to fail, scale, and deploy independently.
Structured elaboration
Take each axis for both approaches directly:
| Axis | Synchronous REST | Asynchronous messaging |
|---|---|---|
| Failure semantics | Caller gets an explicit HTTP status (2xx/4xx/5xx) or a timeout; failure is immediate and attributable to one call. | Failure is implicit and delayed: a message can fail after N retries and land in a dead-letter queue (DLQ), discovered later by whoever monitors the DLQ, not by the original producer. |
| Coupling | Temporal coupling (both services must be up simultaneously) plus contract coupling to the exact response shape and latency. | Only schema coupling to the message contract; producer and consumer need not be online at the same time, and consumers can be added without producer changes. |
| Observability | A single request/response pair is easy to trace with standard distributed tracing (a request ID follows one call stack). | Requires correlating a message across an asynchronous boundary (a shared correlation ID propagated through message headers) and tracking consumer lag, queue depth, and DLQ growth as first-class signals, since "is this working" is no longer visible in a single trace. |
| Latency | Bounded by the caller's timeout; the caller experiences the callee's latency directly, and a slow callee makes the caller slow. | The producer's latency is just "message accepted by the broker," typically single-digit milliseconds; end-to-end processing latency is decoupled from the producer and instead depends on consumer throughput and backlog. |
| Consistency guarantees | Strong consistency is achievable at the call boundary: the caller knows the callee's result before proceeding. | Eventual consistency: the caller only knows the message was durably accepted, not that the consumer has processed it; there is a window, bounded by consumer lag, during which downstream state has not caught up. |
| Operational complexity | Lower: no broker to run, fewer moving parts, well-understood tooling (load balancers, HTTP status codes, standard retries). | Higher: a broker (or managed equivalent) to operate or pay for, plus delivery-semantics decisions (at-least-once handling), consumer-side idempotency, ordering/partition-key design, and DLQ/retry policy to build and monitor. |
Worked example
A ride-hailing platform's "request ride" endpoint needs to synchronously call a pricing service, because the rider must see a confirmed price before confirming the ride; if pricing is slow or down, the request should fail fast rather than silently succeed with an unknown price, so REST with a tight timeout (e.g., 500ms) and a clear 503 on failure is the right fit here: failure semantics are explicit, and the caller cannot proceed without the answer. In contrast, once the ride completes, updating the driver's lifetime earnings dashboard and running fraud-pattern analysis on the trip do not gate anything the rider or driver is waiting on; publishing a "ride.completed" event lets those two consumers process independently, at their own pace, and a backlog in the fraud-analysis consumer (say, several minutes of lag during a traffic spike) has zero effect on ride completion, which is exactly the isolation asynchronous messaging is bought for.
Trade-offs and pitfalls
The common wrong turn is treating this as an architecture-wide choice ("we are a REST shop" or "we are event-driven") rather than a per-interaction decision on these six axes; most real systems need both, often for different steps of the same business flow. A subtler pitfall is picking asynchronous messaging for its scalability story while underestimating the observability tax: without correlation IDs threaded through message headers and consumer-lag/DLQ-depth dashboards from day one, an asynchronous flow that silently stalls is far harder to detect than a synchronous call that returns an explicit error, because nothing "fails" in a way that pages anyone; the failure just accumulates quietly as growing lag until a downstream SLA (service-level agreement) is missed.
Design graceful degradation strategies for an event-driven notification subsystem when downstream push services (APNs/FCM/email API) are rate-limited or unavailable. Include queueing, prioritization, circuit breakers, fallbacks, retry policies, and how to communicate delays to users and product teams.
Sample Answer
Direct answer
When downstream push providers, the Apple Push Notification service (APNs), Firebase Cloud Messaging (FCM), or an email API, are rate-limited or down, the notification subsystem should queue rather than drop, prioritize what gets sent first, trip a circuit breaker so a struggling provider is not hammered by continued retries, fall back to an alternate channel where one exists, and communicate the delay honestly to both the end user and the team that will get paged about it.
Structured elaboration
| Provider signal | Mechanism | Behavior |
|---|---|---|
| Rate-limited (429 or a backoff header) | Retry with capped exponential backoff and jitter | Requeue with a delay; do not hammer a provider that is telling you to slow down. |
| Sustained failures (a run of consecutive failures) | Circuit breaker opens | Stop sending to that provider for a cooldown window; redirect new sends to queue-and-wait or a fallback channel. |
| Provider fully unavailable | Queue with priority lanes | Critical notifications (security alerts, payment failures) are preserved and retried first once the provider recovers; low-priority notifications (marketing) are shed or deferred longest. |
| Breaker cooldown elapsed (half-open) | Canary a small fraction of traffic | Confirm real recovery before resuming full volume; avoids re-tripping immediately. |
Queueing and prioritization. Tier notifications at publish time (critical/transactional versus informational/marketing), and give the critical tier its own queue or a priority field so degradation sheds the low-priority tier first when the backlog needs to be bounded, the same principle as general backpressure design, tuned here to notification-specific business tiers rather than message size or type.
Circuit breakers. Track failure rate per downstream provider independently, APNs failing does not mean FCM is failing, open the breaker on a sustained failure rate over a short window, and use a half-open state that only lets a small canary volume through before fully reopening the gate. This avoids an immediate re-trip the instant a provider blips back to life.
Fallbacks. Define a real fallback path, not just "retry harder." If push fails and the notification matters, fall back to email (or SMS for the most critical class); if the fallback channel is also degraded, the message stays queued for the original channel with a bounded retry window rather than looping forever.
Retry policy. Capped exponential backoff with jitter, so retries spread out over time instead of synchronizing into a new burst against a provider that is already struggling, and a maximum retry count or time window after which the message moves to a dead-letter queue for manual or delayed reprocessing.
Communicating delays. To end users, this typically means an honest, generic in-app state ("notifications may be delayed") rather than silence, especially for anything the user is actively waiting on. To the product team, it means a dashboard and an alert channel showing current breaker state, queue depth by priority tier, and the estimated time to drain the backlog, so a rate-limit incident is visible as a known, bounded event rather than something someone has to reconstruct from logs afterward.
Worked example
backoff schedule (s):1,2,4,8,16,30capWith a circuit breaker configured to open after 5 consecutive failures within a 60-second window, and a 60-second cooldown before moving to half-open: a sustained APNs outage produces at most 5 failed attempts against the provider, roughly 1+2+4+8+16 = 31 seconds of retry spacing, before the breaker opens and sending stops. From that point, the queue simply holds critical-tier messages for retry once the breaker's half-open canary confirms recovery, rather than continuing to retry against a provider that has already signaled it is down.
Trade-offs and pitfalls
- Retrying without backoff or a circuit breaker turns a provider rate-limit into a self-inflicted denial-of-service against your own outbound traffic, and can get an account or IP range throttled or blocked further.
- Treating all notifications as equally important during degradation means low-value marketing pushes consume the same retry budget as a security alert. Tiering has to exist before the incident, not be improvised during it.
- A fallback channel with no cap of its own can silently overload email, or SMS, which usually carries a real per-message cost, the moment push degrades. Size and rate-limit the fallback too.
- Silence to the user reads as though the notification never happened. Even a generic delay indicator is better than nothing, but overpromising a specific recovery timeline the system cannot guarantee just moves the trust problem downstream.
Unlock Full Question Bank
Get access to all 38 Event-Driven Architecture and Asynchronous Messaging interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.