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.
Design a simple asynchronous pipeline for transactional email delivery. Requirements: up to 100k emails/day peak, under 5s from user action to 'queued', resilience to SMTP outages, duplicate prevention, and visibility for a sales dashboard showing queued/sent/failed counts. Choose components (for example SNS+SQS, worker fleet, SES) and describe message flow, retry/DLQ behavior, and monitoring.
Sample Answer
Direct answer
For 100k emails/day with a hard 5-second "queued" acknowledgment and resilience to SMTP (Simple Mail Transfer Protocol) outages, the design decouples the fast, synchronous part (accept the request and durably queue it) from the slow, unreliable part (actually talking to an email provider), fronted by a pub/sub topic that fans out to a durable queue, drained by an autoscaled worker fleet that calls the provider and reports status back to a store the dashboard reads.
Structured elaboration
Components and message flow
- The application publishes an
EmailRequestedevent to a pub/sub topic, for example SNS (Amazon's Simple Notification Service, a managed pub/sub topic), immediately after basic validation and an idempotency-key check. This is the step that must land inside the 5-second budget. - The topic fans out to a durable queue that the worker fleet actually polls, for example SQS (Amazon's Simple Queue Service, a managed message queue). Using a topic-plus-queue split rather than publishing straight to a queue means a second subscriber (an audit log, analytics) can be added later without touching the producer.
- A worker fleet pulls from the queue, calls the provider, for example SES (Amazon's Simple Email Service, a transactional email-sending service) or a direct SMTP relay, and on success marks the item sent; on a provider or SMTP-level failure it lets the message become visible again for retry instead of acknowledging it away.
- Every state transition (queued, sent, failed) writes a row to a small status table that the sales dashboard reads, keyed by message id, so the dashboard reads data the pipeline already produces rather than polling the provider itself.
This same event shape generalizes across transactional email types, order confirmations, password resets, account-activation emails, by carrying a type field on the event rather than needing a separate pipeline per email type.
Duplicate prevention. Generate an idempotency key at the point of triggering the send (an order id plus email type, or a client-supplied request id), and check it against a store (a table with a unique constraint, or a cache with a time-to-live matching the outage window you want to tolerate) before publishing. If the key already exists, return the existing status instead of publishing again. This check has to happen before the publish, not after, otherwise two racing requests both pass the check and the email goes out twice.
Retry and dead-letter-queue behavior. The queue's visibility timeout controls how long a message stays invisible to other workers while one worker is processing it. If the SMTP call is retried with capped exponential backoff (say 1s, 2s, 4s) inside the worker for a small number of attempts, a worker crash mid-send just makes the message visible again for another worker. After a bounded number of redeliveries (the queue's own receive count, not in-worker retries), route the message to a dead-letter queue so a genuinely bad message (a malformed address, a recipient that permanently rejects) does not spin forever and starve the queue for everyone else.
Resilience to SMTP outages. The queue is the buffer. If the provider is down, workers back off (their retries fail, they let visibility expire) and the queue simply grows, durably, up to whatever retention window is configured. No messages are lost as long as the queue's retention exceeds the expected outage length. When the provider recovers, the worker fleet drains the backlog, autoscaling on queue depth so the drain is fast rather than trickling out at the steady-state worker count.
Monitoring. Queue depth and the age of the oldest message (a proxy for how close the pipeline is to the SLA and to retention limits), send success/failure rate by provider response code, dead-letter-queue depth (should stay near zero, growth signals a systemic problem), and the queued/sent/failed counts the dashboard itself needs, sourced from the same status table.
Worked example
100k emails/day averages to:
avg rate=86400100000≈1.16 msg/sThat average is trivial to handle; the real design driver is peak, not average. As an illustrative planning assumption, if 20% of the day's volume goes out inside a single one-hour campaign send:
campaign peak=36000.20×100000≈5.56 msg/sEven a modest worker fleet (5 to 10 workers, each handling one send at a time) clears that comfortably. That is also why the 5-second budget is spent almost entirely on the accept-and-queue path (validate, check the idempotency key, publish), not on waiting for the SMTP call: the API handler returns a fast "queued" response the moment the publish acknowledges, and the actual send happens after the response, off the request's critical path.
Trade-offs and pitfalls
- Doing the idempotency check and the publish as two separate, non-atomic steps under concurrent requests reintroduces the duplicate you were trying to prevent. Use a store with a unique constraint or a conditional write so the check-and-set is effectively one operation.
- Sizing queue retention shorter than the outage duration you actually need to tolerate silently discards messages once retention expires during a long provider outage. Size it to the SLA (service-level agreement) you promise, not to a default.
- Treating every SMTP failure the same wastes retries: a 5xx from the provider is worth retrying, a permanent bounce (an invalid address) is not, and should go straight to failed or the dead-letter queue instead of consuming retry budget.
- A dashboard that queries the provider's API directly for status, instead of reading the pipeline's own status table, couples dashboard latency and availability to a third party you do not control.
Explain backpressure and flow-control techniques across networked services and message brokers: TCP flow control, HTTP/2 flow control, reactive streams (request-n), credit-based broker flow control, and producer-side throttling. For each technique explain when it's most appropriate and how you'd design end-to-end controls to prevent OOMs and cascading failures.
Sample Answer
Direct answer
These five techniques operate at different layers, from the raw byte stream up to an individual message, and they compose: none of them alone protects an entire pipeline from an out-of-memory (OOM) crash or a cascading failure, the end-to-end design comes from layering them so each protects the specific thing the layer below it cannot.
Structured elaboration
| Technique | Mechanism | Layer | Most appropriate when |
|---|---|---|---|
| Transmission Control Protocol (TCP) flow control | Receiver advertises a byte window in each acknowledgment; sender never has more unacknowledged bytes outstanding than that window | Transport, automatic for any TCP connection | Baseline protection for any point-to-point byte stream, especially when you do not control the application protocol running on top of it |
| HTTP/2 flow control | A credit-like byte window, but per multiplexed stream as well as per connection | Application-transport boundary | Many logical exchanges (for example gRPC calls) multiplexed over one TCP connection, where one slow stream should not starve the others sharing that connection |
Reactive streams (request(n)) | The subscriber explicitly tells the publisher how many items it is ready to receive next; the publisher cannot push more than requested | Application, in-process or library-level | An async pipeline or stream-processing library where backpressure needs to be an explicit, item-oriented part of the consumer's own control flow, not inferred from bytes |
| Credit-based broker flow control | A consumer grants the broker a bounded number of unacknowledged in-flight messages (a prefetch or credit limit); each acknowledgment replenishes one unit | Message-broker to consumer relationship | Bounding one consumer's own in-flight work without needing the producer to coordinate with it directly, the broker mediates |
| Producer-side throttling | The producer itself is rate-limited before sending, based on a fixed budget or on live downstream health signals (queue depth, consumer lag, error rate) | Outermost, protects the whole system including the broker | Protecting the system as a whole, including the broker's own storage, not just one consumer |
Why layering matters, not picking one. TCP and HTTP/2 flow control protect the wire, an application can still accumulate an unbounded in-memory backlog even while the underlying connection is perfectly healthy, because those layers only bound bytes in flight on the network, not messages queued in application memory waiting to be processed. Credit-based broker flow control is what actually bounds an individual consumer's own memory footprint, by capping how many unacknowledged messages the broker will hand it. Producer-side throttling is the only one of the five that protects the broker's own memory or disk, without it, a healthy consumer relationship does nothing to stop the broker itself from accumulating an unbounded queue if producers keep publishing faster than the system can drain.
Preventing cascading failure specifically. Without an upstream signal, a slow consumer causes the broker's queue depth to grow unbounded, risking the broker's own out-of-memory (OOM) crash or disk exhaustion. If the broker then starts shedding load (rejecting connections or writes) to protect itself, producers that do not back off in response to that rejection can retry-storm the broker, making the incident worse instead of better. This is why producer-side throttling needs to be dynamic, responsive to live broker or consumer health signals, rather than a fixed rate calibrated only for normal conditions; a fixed rate does nothing extra during the exact moment it is needed most.
Worked example
A consumer configured with a prefetch (credit) limit of 50 unacknowledged messages, average payload size 2 kilobytes (KB).
With credit-based flow control: the broker never has more than 50 unacknowledged messages outstanding to this consumer, so its in-flight memory footprint for this consumer's queue is bounded at roughly 50 x 2 KB = 100 KB, regardless of how large the total backlog waiting behind those 50 becomes.
Without it (an unbounded prefetch): the broker keeps pushing every available message, so the consumer's in-flight count, and its memory footprint, grows with the size of the backlog itself, with no structural bound, until the backlog stops growing or the consumer runs out of memory.
The structural difference (bounded versus unbounded growth) is what the mechanism buys you; the 100 KB figure is a direct, pinned-input calculation (50 messages times 2 KB each), not a measurement.
Trade-offs and pitfalls
- Setting broker credit or prefetch too low sacrifices throughput, many small round trips to replenish a small credit budget.
- Setting it too high defeats the purpose, it looks bounded on paper but the bound is too large to meaningfully protect memory.
- Assuming TCP or HTTP/2 flow control alone is "enough" backpressure is the single most common pitfall named implicitly by this question: those protect the wire, not an application's own queues or memory.
- Static, fixed-rate producer throttling calibrated for normal conditions does not actually prevent cascading failure during a real incident, since the fixed rate was never designed to respond to the incident happening; dynamic throttling that reacts to consumer lag or broker health is what closes that gap.
Event-sourcing stores all state changes as events. Discuss the trade-offs between storing only events versus introducing periodic snapshots. As a data engineer, explain snapshotting frequency, snapshot storage, snapshot validation, rehydration cost, and strategies for compaction or archival to control event-store growth.
Sample Answer
Direct answer
Storing only events gives perfect auditability and deterministic rebuilds, but rehydration cost (the work to replay events into current state) grows with the event count, so as a stream ages, reads and rebuilds get slower and more expensive. Periodic snapshots cap that cost by giving rehydration a recent starting point instead of the beginning of time, at the price of extra storage and a validation problem: a snapshot must be provably consistent with the events it claims to summarize. As a data engineer, pick a snapshot cadence and compaction policy driven by measured rehydration cost, not by a fixed rule of thumb.
Structured elaboration
Snapshotting frequency
- Event-count-based: snapshot every N events (commonly in the low thousands, tuned to the aggregate). Predictable worst-case replay cost regardless of how much wall-clock time has passed.
- Time-based: snapshot daily or hourly, better for aggregates with low or bursty event velocity where event count alone is not a reliable trigger.
- Hybrid: aggressive event-count triggers for hot, frequently-updated aggregates; time-based triggers for long-lived but rarely-updated ones.
- Whichever trigger is chosen, tune it against measured rehydration cost (replay time and CPU per rehydration), not a value picked without data.
Snapshot storage
- Store snapshots as immutable objects in cost-efficient storage, tagged with the aggregate identifier, the schema version, and the sequence number of the last event folded into the snapshot.
- Keep hot, frequently-accessed snapshots in fast storage or a cache; move cold snapshots to cheaper storage tiers with lifecycle rules.
Snapshot validation
- A snapshot must carry: the last-applied event sequence number, a schema version, and a checksum of its contents.
- On load, verify the checksum and confirm sequence continuity (no gap between the snapshot's last-applied sequence and the next event to replay). On mismatch, fall back to a full replay from the last known-good snapshot or from the beginning.
- Run periodic background verification jobs that independently rebuild a sample of aggregates from events alone and diff against the stored snapshot, to catch silent snapshot corruption before it is relied upon.
Rehydration cost
- Rehydration cost is: load the snapshot, then replay only the events recorded after that snapshot's sequence number. Cost scales with events-since-snapshot, not with the aggregate's total lifetime event count.
- The snapshot cadence directly bounds the worst case: with a snapshot taken every N events, no rehydration ever replays more than N-1 events.
Compaction and archival to control event-store growth (folding the replay/backfill and derived-dataset-reprocessing nuance)
- Compact by taking a full snapshot and, only where retention rules permit it, archiving or truncating events older than that snapshot's sequence to cold, cheaper storage rather than deleting them outright; legal or audit requirements often forbid true deletion.
- The archival tier still has to remain replayable: any derived dataset (an analytics table, a machine learning feature store, a rebuilt read model) that was originally built by consuming the full event history needs that same history available if it must ever be reprocessed, for example after a bug fix in the transformation logic. A retention policy that only optimizes for "rehydrate a live aggregate quickly" and quietly discards old events breaks backfill for any derived dataset that depended on that full history, even though the live aggregates themselves are unaffected. Decide retention and compaction against both use cases explicitly, not just the aggregate-rehydration one.
- Use a tiered retention: hot recent events in the primary event store, older events moved to compressed, partitioned cold storage with an index that supports selective replay by aggregate and time range.
- Mark truncation points explicitly (a compaction marker event, or metadata) so any consumer replaying the stream can detect where the live tier's history starts and knows to fetch older ranges from the archive if it needs them.
Worked example
An account aggregate receives 50 events per day on average.
- Without snapshots, rehydrating that account after 5 years of history means replaying 5 * 365 * 50 = 91,250 events.
- With a snapshot taken every 1,000 events, the worst-case replay after any snapshot is 999 events (999 / 91,250 = 1.09%, so rehydration cost is bounded to roughly 1% of the no-snapshot case at year 5, not strictly under 1%: it is a hair over).
- At 50 events/day, a snapshot every 1,000 events fires roughly every 20 days (1,000 / 50 = 20), so the account accumulates at most 20 days' worth of unsnapshotted events at any time.
- If the business later needs to reprocess the full 5-year history to backfill a new derived dataset (say, a new fraud-scoring feature that needs every historical event, not just the latest snapshot), the archived event range covering all 91,250 events must still be retrievable even though live rehydration never touches most of them.
Trade-offs and pitfalls
- Common wrong turn: choosing a snapshot cadence without measuring actual rehydration cost, then discovering it under- or over-snapshots (too frequent wastes storage and write bandwidth on snapshotting; too infrequent leaves rehydration slow).
- Common wrong turn: treating snapshot validation as optional. An unvalidated, silently corrupt snapshot is worse than no snapshot: it produces confidently wrong current state instead of forcing a (correct, if slow) full replay.
- Common wrong turn: designing retention purely around live-aggregate rehydration speed and discovering, only when a derived dataset needs to be rebuilt, that the events required for that rebuild were already archived out of reach or deleted.
- Senior signal: naming the specific numeric relationship between snapshot cadence and worst-case replay cost, and treating archival policy as a decision that serves more than one consumer (live rehydration and derived-dataset reprocessing), not just the first one that comes to mind.
In a shopping-cart checkout flow, decide which sub-steps should be synchronous (e.g., payment authorization) and which can be asynchronous (e.g., sending confirmation email, analytics). Explain how your choices affect user experience, system correctness, error-handling, and eventual consistency guarantees.
Sample Answer
Direct answer
In a checkout flow, keep synchronous only the sub-steps whose outcome the user or the next step must know before the transaction can be considered complete: inventory availability check and payment authorization. Everything that does not gate the "did this order succeed" answer, such as sending the confirmation email and recording analytics events, should be asynchronous, published as events once the order is durably created.
Structured elaboration
Apply a single test to each sub-step: "if this step fails or is slow, must the user's checkout fail or wait?" Payment authorization fails the test: if the card is declined, the order must not be placed, so it has to complete (or definitively fail) before the checkout response returns. Confirmation email and analytics both pass the test in the other direction: if the email provider is down, the order is still valid and the customer should not see an error; if the analytics pipeline is backed up, that has zero bearing on whether the customer got their item.
Effects of this split:
- User experience. The customer sees a fast, honest response: "order confirmed" as soon as payment clears, not "order confirmed, email pending" or a spinner while a marketing analytics call finishes. Moving email/analytics off the critical path directly lowers perceived latency, since the response no longer waits on the slowest of several unrelated systems.
- System correctness. Correctness now hinges only on the synchronous steps actually being atomic or safely retryable: payment authorization must be idempotent (a network retry must not double-charge) and the order record must not be marked "placed" unless payment is confirmed. The asynchronous steps cannot violate correctness of the order itself, because they consume from an event that is only published after the order is already valid; at worst, a failed email send means a customer does not get a receipt email, not that they have a wrong order.
- Error handling. Synchronous steps need explicit user-facing error handling (declined card, out-of-stock) because the user is waiting on the answer. Asynchronous steps need consumer-side error handling instead: retries with backoff, and a dead-letter queue (DLQ, a queue that holds messages a consumer could not process after exhausting retries) for a persistently failing email send, which an on-call engineer or automated remediation job can drain later without ever bothering the customer.
- Eventual consistency guarantees. The order itself is strongly consistent the moment checkout returns (it either happened or it did not). The order's "surrounding" state, such as "has the customer been emailed" or "has this purchase been counted in today's revenue dashboard," becomes eventually consistent: it will be true within some bounded window (seconds to low minutes, driven by consumer lag) but is not guaranteed true at the instant checkout returns. That gap needs to be a conscious guarantee you can state, not an accident: e.g., "confirmation email delivered within 5 minutes of order placement, monitored via consumer lag on the notifications topic."
Worked example
Sequence for a 75order:(1)synchronouslyreserveinventoryfortheSKUandauthorizepaymentfor75; if either fails, return an error to the user immediately and nothing else happens. (2) On success, write the order row and, in the same database transaction (or via the transactional outbox pattern, where an "OrderPlaced" row is written to an outbox table in the same commit and a separate relay publishes it), emit an "OrderPlaced" event. (3) Return "order confirmed" to the user at this point, without waiting on anything downstream. (4) A notifications consumer subscribed to "OrderPlaced" sends the confirmation email, retrying up to 3 times with backoff on transient send failures before landing the message in a DLQ. (5) An independent analytics consumer subscribed to the same event increments the day's revenue counter. Steps 4 and 5 run in parallel, are unaware of each other, and neither can block or fail step 1 through 3.
Trade-offs and pitfalls
The main pitfall is drawing the line by "which steps feel slow" rather than "which steps the user's success/failure outcome depends on"; a fast email send is still a UX and correctness bug if it is on the synchronous path, because it adds a dependency the checkout does not need. The opposite pitfall is making payment authorization asynchronous "to be consistent" with the rest of the flow: that forces the UI into an awkward "we'll email you when your payment clears" pattern for something users expect an immediate answer to, and it reopens the question of what state the order is in while payment is pending. A senior answer also flags that moving a step asynchronous introduces a durability requirement: if the order write and the event publish are not atomic, a crash between them can silently drop confirmation emails and analytics events for orders that did place successfully, which is exactly the failure mode the outbox pattern exists to close.
Describe delivery semantics for messaging systems: at-most-once, at-least-once, and exactly-once. For each semantic, explain the practical impact on data loss and duplication, what guarantees the broker provides versus what the application must ensure, and give a realistic example where that semantic is appropriate (e.g., analytics ingestion vs financial transactions).
Sample Answer
Direct answer
At-most-once, at-least-once, and exactly-once describe how many times a message's effect is applied to a consumer's state when failures happen, and they trade off data loss against duplication: at-most-once can lose messages but never duplicates them, at-least-once never loses messages but can duplicate them, and exactly-once achieves neither loss nor duplication, at the cost of real coordination complexity between the broker and the application. The broker only ever guarantees delivery attempts and retries; whether duplicates or gaps actually matter is always decided by what the application does with what it receives.
Structured elaboration
| Semantic | Data loss | Duplication | What the broker guarantees | What the application must ensure |
|---|---|---|---|---|
| At-most-once | Possible | Not possible | A single delivery attempt, no automatic retry | Tolerate occasional missing messages; nothing to deduplicate |
| At-least-once | Not possible (assuming retries eventually succeed) | Possible | Persistence and retry until acknowledgment | Idempotency or deduplication (unique message identifiers, a dedup store, or naturally idempotent writes) |
| Exactly-once | Not possible | Not possible | Coordination primitives (transactional offsets/produce, or equivalent) that make the combination possible | Either rely on those broker-provided transactional guarantees end to end, or implement application-level idempotent writes tight enough to simulate the same outcome |
At-most-once in practice
- The broker or transport makes a single delivery attempt and does not retry on failure; if the consumer or the network drops the message, it is gone.
- Appropriate when losing an occasional message is cheaper than the cost of retry infrastructure and the risk of duplicates, for example high-volume sensor telemetry feeding a dashboard where one missing data point among thousands is invisible.
At-least-once in practice
- The broker persists the message and retries delivery until it receives an acknowledgment, which means a consumer crash between processing a message and acknowledging it results in redelivery of a message that was already (partially or fully) processed.
- The broker's job stops at "you will eventually get this message, possibly more than once." The application's job is to make processing that message twice harmless: a unique message identifier plus a dedup store, or a write that is naturally idempotent (an upsert keyed by a stable identifier, rather than an increment).
- Appropriate for analytics ingestion or audit logging, where losing a record is unacceptable but a duplicate can be cleaned up downstream (deduplicated at query time, or filtered by the same identifier used for idempotency).
Exactly-once in practice
- True end-to-end exactly-once requires coordination beyond a single retry-and-acknowledge loop: either the broker offers transactional semantics that atomically tie together "I read this" and "I wrote that" (so a crash mid-processing rolls back cleanly rather than leaving a partial, retryable state), or the application achieves the same outcome by combining at-least-once delivery with strict idempotent processing, which is functionally exactly-once from the business's point of view even though the transport itself only promised at-least-once.
- Appropriate for financial transfers or order processing, where a duplicate effect (charging a customer twice, decrementing inventory twice for one order) or a lost effect (an order accepted but never charged) both cause real monetary or legal problems, not just a dirty analytics row.
Worked example
A payments system processes a $250 transfer as a single message.
- Under at-most-once: if the consumer crashes after receiving the message but before applying the transfer, the $250 transfer is simply lost, no automatic retry recovers it, and the sender's balance is silently wrong by $250 unless a separate reconciliation process catches it.
- Under at-least-once with no application-side idempotency: the same crash-after-receive scenario causes the broker to redeliver the message; if the consumer blindly reapplies "subtract $250" a second time, the sender's balance is now wrong by $250 in the opposite direction (over-debited), a duplicate-effect bug, not a loss bug.
- Under at-least-once with application-side idempotency (a unique transfer identifier and an idempotent upsert: "set this transfer's status to applied if it is not already applied," not "subtract $250 unconditionally"): the redelivery is a no-op the second time, and the sender's balance is debited exactly once, $250, regardless of how many times the message is redelivered. This is the practical way most systems get exactly-once business outcomes without needing the broker to provide literal exactly-once transport.
Trade-offs and pitfalls
- Common wrong turn: choosing at-most-once for anything with real business consequence (payments, inventory decrements, order state) purely because it is the simplest and fastest option; the missing-message failure mode is invisible until an audit or a customer complaint surfaces it.
- Common wrong turn: assuming "the broker supports exactly-once" is a complete answer without checking what scope that guarantee actually covers (often just broker-internal processing, not the full path out to an external side effect like a payment gateway call, which the broker cannot make transactional on your behalf).
- Common wrong turn: implementing at-least-once with no deduplication strategy at all, treating "the broker retries" as sufficient without doing the application-side half of the job.
- Senior signal: naming which side, broker or application, is responsible for which half of the guarantee, and picking the example that matches the actual business cost of loss versus duplication rather than defaulting to whichever semantic sounds strongest.
Unlock Full Question Bank
Get access to all 25 Event-Driven Architecture and Asynchronous Messaging interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.