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.
You must guarantee strict global ordering of financial transactions across a multi-tenant system at very high throughput. Evaluate feasibility and, if strict global ordering is infeasible, propose practical alternative designs (per-tenant ordering, causal guarantees, reconciliation approaches) that balance ordering, throughput, and tenant isolation.
Sample Answer
Direct answer
Strict global ordering across a high-throughput, multi-tenant system is not practically feasible: any true total order requires funneling every write through a single serialization point, which caps throughput at that one point's own processing rate no matter how much hardware sits behind it elsewhere. The workable alternative scopes real ordering to the smallest unit that actually needs it, almost always per-account or per-tenant, and uses weaker, explicitly chosen guarantees (causal ordering for the rare cross-tenant case, asynchronous reconciliation for everything else) rather than paying for a global order the business rarely needs.
Structured elaboration
Why strict global order does not scale. Even a distributed, consensus-backed log (for example one built on a Raft-style protocol) still funnels appends through one leader at a time for that log. Every transaction from every tenant has to pass through that single sequencer in sequence, so total throughput is bounded by how fast that one component can durably append, not by how many machines the rest of the system has.
Per-tenant ordering. Financial correctness usually requires that one account's transactions are ordered relative to each other, not that every account's transactions are ordered relative to every other account's. Partitioning the log by tenant or account (the same partition-key idea used for per-entity ordering generally) gives each partition its own independent, strictly-ordered sequence, running fully in parallel with every other partition. Aggregate throughput now scales with the number of partitions instead of being capped by one global sequencer.
Causal ordering for the cases that genuinely span tenants. A transfer between two accounts is the classic case where two different partitions' events do need to be ordered relative to each other. Causal ordering (vector clocks, or a simpler explicit dependency marker on the transfer event) only orders events that are causally related, one happened because of or before the other, and leaves everything else unordered relative to each other, which is far cheaper than a global order while still giving the specific guarantee a cross-account transfer needs.
Reconciliation for cross-topic and eventual cases. Where strict real-time ordering across tenants is not required at all, for example end-of-day settlement, let each partition run at full independent speed and detect and resolve any cross-tenant ordering conflicts afterward with an asynchronous reconciliation pass (netting, double-booking detection, balance verification) rather than paying an ordering cost on the hot path. This is also the natural fallback when an ordering question spans more than two partitions or even more than one topic entirely, cross-topic ordering has no single shared log to order against by construction, so reconciliation, not a bigger sequencer, is the realistic answer there.
Balancing ordering, throughput, and tenant isolation. Ordering scope should be the smallest unit that genuinely needs it (usually per-account, occasionally a causally-linked pair of accounts). Throughput follows directly from how many independent ordering units exist, more units means more parallelism. Tenant isolation falls out of the same per-tenant partitioning as a side benefit: a noisy or buggy tenant cannot stall another tenant's ordering, because they are on different partitions with different sequencers.
Worked example
An illustrative feasibility ceiling, using a stated assumption, not a measured benchmark: assume a single global sequencer requires a synchronous, durable append taking 5 milliseconds per transaction.
throughputmax=latencyappend1=0.005s1=200tx/sNow compare to partitioning by tenant across, say, 200 tenants, each with its own sequencer running at the same assumed 5 millisecond append latency:
aggregate throughput=200 tenants×200tx/s per tenant=40,000tx/sThis is a proportional argument from a stated assumption, not a claim about any real system's measured numbers, it illustrates why partitioning multiplies throughput (more independent sequencers), not what any particular deployment would actually achieve.
Trade-offs and pitfalls
- Per-tenant partitioning does not solve cross-tenant operations by itself, transfers and settlement still need the causal-ordering or reconciliation layer named above, explicitly, not as an afterthought.
- Reconciliation moves complexity into conflict detection and resolution logic; "eventual" is not a synonym for "free," it is real engineering effort relocated off the hot path.
- Conflating "each partition is strictly ordered" with "the whole system is strictly ordered" is a common miscommunication, both in interviews and in real incident postmortems, be precise about which one you are claiming.
- Explaining this design to a non-technical stakeholder or auditor who expects a single global transaction order needs the same clarity: per-account order is preserved and provable, cross-account order is handled deliberately via causal links or reconciliation, not silently absent.
In an event-sourced system, describe how you would implement backup, restore, and reprocessing strategies when event schemas evolve (adding fields, removing fields, changing semantics). Address immutability of events, event versioning, enrichment vs transformation, tools and practices for replaying events into new projections, and safe rollbacks if reprocessing causes issues.
Sample Answer
Direct answer
Treat events as immutable and never edit history in place: back up the raw event log and periodic snapshots separately, version every schema change so old events remain interpretable forever, and do all reprocessing (replaying events into a new or corrected projection) through a governed pipeline with a dry run, a diff against known-good output, and a documented rollback before anything touches production. The core discipline is that a schema change never rewrites what already happened; it only changes how new readers interpret it, via explicit transformation logic kept alongside the schema registry.
Structured elaboration
Immutability of events
- A stored event is never edited or deleted (subject to legal retention limits) once written; every event carries its schema version so a reader always knows which interpretation rules to apply.
- Corrections to a wrong past fact are themselves new events (a compensating or correcting event), not silent edits to history. This preserves the audit trail's core value: what actually happened is recoverable exactly as it happened, including any mistake and its correction.
Event versioning
- Every event carries explicit metadata: event type, schema version, and a timestamp. A schema registry tracks compatibility rules (backward, forward, or full) per event type.
- Prefer additive changes (new optional fields with defaults). For a genuinely breaking change (a field removed, or its meaning changed), introduce a new event type or version rather than mutating the old one's meaning in place.
- Maintain a version-aware deserializer, or an explicit "upcaster" registry mapping older event versions to the current shape, so replay from the very beginning of the log always works even after several schema generations.
Enrichment vs transformation
- Enrichment happens at read time: a consumer or projector adds derived fields (a lookup join, a denormalized label) without changing the stored event bytes at all. This is the default, lower-risk choice whenever the correction or addition can live entirely downstream of the log.
- Transformation is a deliberate, governed rewrite: when correctness or performance genuinely requires a canonical change to what is stored, it happens by emitting new-version events through an idempotent migration pipeline, never by editing the original bytes. Escalate to transformation only when enrichment cannot solve the problem, since transformation carries materially more operational risk.
Backup and restore
- Back up the raw append-only event log continuously (or on a tight schedule) and take periodic snapshots of aggregate state and read-model projections separately; store both in durable, immutable, checksummed storage with clear retention policy.
- Restore in the correct order: bring back the raw events first (the source of truth), then replay them into whatever projection code and snapshot generation is current, rather than restoring a stale snapshot as if it were authoritative on its own.
Tools and practices for replaying events into new projections (folding the media/blob-payload nuance)
- A replay orchestration service accepts a manifest: which stream(s), which offset range (or the full history), which projection code version, and a dry-run flag; it should support partitioned, parallelizable replay with rate limiting so a full historical replay does not overwhelm downstream systems.
- Binary or media payloads (an image, an audio clip, a large document attached to an event) need different handling than a small JSON field: store the actual bytes in object storage and put only a reference (a pointer plus a content checksum and a payload-format version) inside the event itself. This keeps the event log itself small and fast to replay, and it makes media-format evolution (a new image encoding, a new document schema) its own explicitly versioned concern, checked via the checksum and the payload-format version rather than assumed compatible.
- Run new projection logic in shadow first: replay into a separate, non-serving projection, and diff its output against the currently-serving one (or against known-correct reconciliation totals) before cutting any production read traffic over.
Safe rollbacks if reprocessing causes issues
- Dry-run and checksum-validate before any production replay actually writes anything that serves traffic.
- Canary the replay: run it against a small time window or a small partition subset first, compare digests or reconciliation totals against the original, and only widen the replay once that canary is clean.
- Keep the previous projection's snapshot intact and untouched during a reprocessing run, so if the new projection is wrong, the rollback is: stop, discard the new projection's output, and resume serving from the untouched previous snapshot, no data was lost because the source events were never modified.
Worked example
An events log for a document-management system stores 12 million events over 3 years, including DocumentUploaded events that reference a scanned PDF stored in object storage (never inlined in the event itself: the event carries {doc_id, storage_key, checksum, payload_format_version: 1}). The team introduces payload_format_version: 2, switching to a new PDF/A archival encoding for new uploads.
- Enrichment path: a new "searchable text" field is added by a downstream OCR job that reads the referenced PDF and writes the extracted text into a separate read model. This never touches the original event or its stored bytes, so it ships with zero replay risk.
- Transformation path: 3 years later, the team decides all archived documents should be re-encoded to the new format for long-term storage compliance. This is a deliberate, governed transformation: a migration job reads each
DocumentUploadedevent (all 12 million, in a rate-limited, partitioned replay), re-encodes the referenced PDF, writes it to a new storage key, and emits a newDocumentReencodedevent carrying the new key, checksum, andpayload_format_version: 2, rather than overwriting the original event or the original stored bytes. The original event, and the original file, remain exactly as they were.
Trade-offs and pitfalls
- Common wrong turn: reaching for a transformation (a rewrite) when enrichment (an additive, read-time derivation) would have solved the same business need with far less risk and no touching of history.
- Common wrong turn: inlining large or binary payloads directly in events "for simplicity." It makes the event log itself expensive to store and slow to replay, and couples every future replay to whatever encoding was in use when the event was written.
- Common wrong turn: restoring a snapshot without also restoring and verifying the underlying event log's integrity first, which can silently paper over a corrupted or incomplete backup of the actual source of truth.
- Senior signal: keeping enrichment as the default and treating transformation as an escalation that requires its own governed pipeline (dry run, canary, diff, rollback plan), rather than treating all schema evolution as equally low-risk.
For an event consumer that fails intermittently calling an external API, design a retry and DLQ strategy that includes exponential backoff with jitter, a maximum number of retries, and escalation to a DLQ. Provide pseudocode (language-agnostic) or a flow description that prevents thundering-herd effects and supports observability of intermediate retry attempts.
Sample Answer
Direct answer
Classify the failure first: only retry errors that look transient (timeouts, connection resets, a 5xx-class downstream error), and send anything that looks permanent (a validation error, a malformed payload) straight to the dead-letter queue (DLQ) without spending retry budget on it. For the retryable path, compute an exponentially growing delay capped at a maximum, then actually sleep a random value between zero and that delay (full jitter), not the delay itself, since randomizing what many failing consumers do at once is what prevents them from all retrying in lockstep.
Structured elaboration
Backoff with a cap. Each retry's base delay grows exponentially with the attempt number, but is capped so it never grows unreasonably large:
delayi=min(cap,base⋅multi−1),sleep∼Uniform(0,delayi)With base = 100 ms, mult = 2, cap = 5000 ms, the uncapped sequence of computed delays for attempts 1 through 5 is 100, 200, 400, 800, 1600 milliseconds (each simply doubling the previous one, 100 x 2^(i-1)), well under the cap in this range.
Why jitter specifically prevents a thundering herd. If every failing consumer instance computed and slept the exact same delay sequence, an outage affecting many instances at once would cause them all to retry in a synchronized wave, at roughly the same moment as each other, again and again, which can itself overload a downstream service that was only briefly struggling. Sleeping a uniformly random value between zero and the computed cap (full jitter) spreads that same population of retries out across the whole window instead of concentrating them, so the retries decorrelate from each other even though every instance is running identical logic.
Max retries and DLQ escalation. Once the retryable path exhausts its retry budget, escalate to the DLQ rather than retrying forever; a message that failed every attempt within the budget is very unlikely to succeed on attempt N+1 without something changing.
Transient versus permanent classification (the absorbed nuance). Not every failure deserves the same treatment. A connection timeout or a 5xx-class error from the external API is plausibly transient, worth the full retry budget. A 4xx-class validation error or a schema mismatch is not going to resolve itself on retry, sending it straight to the DLQ preserves retry budget for messages that might actually succeed and gets the unretryable ones in front of a human faster.
Observability of intermediate attempts. Log or emit a metric on every attempt, not just the final success or DLQ outcome: attempt number, computed delay, and the error, tagged with the message or event identifier. Without this, a message that failed on attempt 3 of 5 and then recovered is invisible in aggregate dashboards that only show terminal outcomes, which makes diagnosing a slow-failing dependency much harder during an incident.
flowchart TD
A[Consume message] --> B[Call external API]
B -->|success| C[Ack message]
B -->|failure| D{attempt <= max_retries?}
D -- No --> E[Send to DLQ, ack original]
D -- Yes --> F["Compute backoff = min(cap, base*mult^attempt)"]
F --> G["Sleep random(0, backoff): full jitter"]
G --> H[Redeliver / retry]
H --> B
Worked example
A rolling outage takes the external API down for several minutes. A hundred consumer instances all hit their first failure within the same second.
- Without jitter: every instance computes the identical sequence
100, 200, 400, 800, ...milliseconds and sleeps exactly that long, so all hundred retry at (roughly) the same instant repeatedly, a synchronized retry wave hitting the API the moment it starts to recover, which can knock it back down. - With full jitter: each instance independently sleeps a value drawn uniformly between 0 and the computed cap for that attempt, so the same hundred retries spread across the whole window instead of landing together, giving the recovering API a smoothly ramping load instead of a spike.
This is a structural argument about what jitter changes (the retry timing distribution across instances), not a claim about a specific measured outcome for any particular API or incident.
Trade-offs and pitfalls
- Setting max retries too high delays DLQ visibility of a dependency that is actually broken, not just slow.
- Setting max retries too low routes ordinary transient blips to the DLQ, creating needless manual-review work for on-call.
- Skipping the transient-versus-permanent classification wastes retry budget on errors that were never going to succeed and delays the human attention a permanent failure actually needs.
- Not tagging attempt number and computed delay in logs or metrics makes a slowly-degrading dependency far harder to diagnose, since only the terminal outcome (success or DLQ) is visible without it.
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.
Explain the difference between publish-subscribe and point-to-point (producer-consumer) messaging patterns. Provide concrete scenarios where pub/sub is a better fit (e.g., notifications, analytics) and where queues are preferable (e.g., work queues, task processing), particularly in multi-tenant SaaS and event-driven microservice architectures.
Sample Answer
Direct answer
Publish-subscribe (pub/sub) delivers each message to every interested subscriber, so it fits situations where multiple independent parties need to know the same fact happened, such as notifications or analytics. Point-to-point (producer-consumer, work-queue style) delivers each message to exactly one consumer among a pool, so it fits situations where a unit of work must be done exactly once by whichever worker picks it up, such as background task processing. The distinction is about fan-out (one-to-many awareness) versus load distribution (one-of-many execution), and both patterns are commonly implemented on the same underlying broker.
Structured elaboration
Publish-subscribe. A publisher emits an event to a topic; every subscriber with an active subscription receives its own copy. Subscribers are typically unaware of each other, can be added or removed without changing the publisher, and each one processes the event for its own purpose. This is the right model whenever "N different systems need to react to the same fact" is the actual requirement: a "UserSignedUp" event might be consumed by an email-welcome service, an analytics pipeline, and a fraud-scoring service simultaneously, with none of them competing for the message.
Point-to-point (work queues). A producer places a task on a queue; a pool of competing consumers pulls from the same queue, and each task is handled by exactly one consumer. This is the right model for "this unit of work needs to happen once, by whichever worker is free," such as resizing an uploaded image or sending a single transactional email: you do not want three workers all resizing the same image.
Where pub/sub is the better fit. Notifications is the clearest case: a single "OrderShipped" event needs to reach a push-notification service, an SMS service, and an in-app activity feed, each independently, and adding a fourth channel later should not require touching the producer. Analytics is the same shape: every business event (page view, purchase, signup) typically needs to reach an analytics pipeline in addition to whatever else consumes it, without competing with those other consumers for the message.
Where queues are preferable. Work queues and task processing are the clear case: a video-transcoding job, a report-generation job, or an outbound-email send should be picked up and completed by exactly one worker, with the queue's competing-consumers model providing natural load balancing and horizontal scaling (add more workers, they compete for the same backlog) without any risk of duplicate execution beyond what at-least-once delivery already requires the consumer to handle idempotently.
Multi-tenant SaaS (software as a service) and event-driven microservices. In a multi-tenant SaaS system, pub/sub is what lets independently-owned services (billing, usage-metering, audit logging) all react to the same tenant-level event, such as "SubscriptionUpgraded," without the team that owns the upgrade flow needing to know or coordinate with every downstream consumer; new consumers subscribe without any change to the publisher. Point-to-point queues, by contrast, are what those same microservices use internally for their own background work, such as a billing service's queue of pending invoice-generation tasks, where exactly-once-effective execution by one worker in the pool is the requirement, not fan-out to observers.
Worked example
A multi-tenant SaaS platform publishes a "TenantUpgraded" event when a customer moves from a free to a paid plan. Three independent subscribers exist on this topic: a billing service that starts metered invoicing, a feature-flag service that unlocks paid features, and a customer-success service that triggers an onboarding email sequence. All three receive their own copy of the same event; the team that owns the upgrade flow never had to know these three consumers existed. Separately, the feature-flag service's own onboarding-email trigger enqueues an actual "send welcome email" task onto a point-to-point work queue consumed by a pool of 5 worker processes; only one of those 5 workers ends up sending that specific email, because the queue hands each task to a single competing consumer, not to all 5.
Trade-offs and pitfalls
The common mistake is using a work queue where pub/sub was needed: if a "TenantUpgraded" task were placed on a single point-to-point queue instead of published to a topic, only one of billing, feature-flags, or customer-success would ever see it, and the other two would silently never fire, which is a subtle and easy-to-miss integration bug. The opposite mistake is using pub/sub where a work queue was needed for a task that must be done exactly once: if "resize this uploaded image" were published to a topic with multiple subscribed workers, every worker would independently resize the same image, wasting resources and, if the workers write to the same output path, potentially racing each other. A senior answer names this fan-out-versus-load-distribution distinction explicitly, rather than treating "pub/sub" and "queue" as interchangeable synonyms for "asynchronous messaging."
Unlock Full Question Bank
Get access to all 42 Event-Driven Architecture and Asynchronous Messaging interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.