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 Dead Letter Queue (DLQ) processing workflow. Requirements: safe reprocessing of failed messages, visibility into failure reasons, quarantine for poison messages, and automation to replay or archive. Explain checks to run before re-enqueueing (idempotency, schema compatibility), and how to monitor DLQ health.
Sample Answer
Direct answer
Treat the dead-letter queue (DLQ) as a state machine, not just a holding queue: every quarantined message moves through explicit states, and nothing gets replayed until it passes two specific automated checks, has this exact message already been applied downstream (idempotency), and does its payload still match what the current consumer code expects (schema compatibility). Skipping either check on replay is the single most common way a "fixed" DLQ incident turns into a second incident.
Structured elaboration
Idempotency check before replay. A message can land in the DLQ after a later step in its processing failed, not the first one, for example a payment charge that succeeded but a subsequent confirmation-email step that did not. Replaying that message from the top without first checking whether it already partially or fully succeeded would double-apply the parts that already worked. Before replay, re-run the exact same dedup or idempotency lookup the normal delivery path would use, so a message that actually did succeed is recognized and skipped rather than blindly reprocessed.
Schema compatibility check before replay. A message can sit in the DLQ for days or weeks while the consumer code keeps evolving. Replaying it against the current code without checking whether its payload still matches the current expected schema risks a deserialization failure (landing right back in the DLQ) or, worse, being silently misinterpreted by code that has since changed its assumptions about a field's meaning. Validate the payload against the current schema before replay; a compatible message replays normally, an incompatible one gets transformed if a safe mapping exists, or archived with a note for manual handling rather than replayed blind.
Visibility into failure reasons. Every quarantined message should carry its classification (why it failed, and which state it is in) so a reviewer, or the automation itself, can act without re-diagnosing from scratch.
Automated replay or archive. Auto-classify at arrival (the same failure-reason tagging as the DLQ architecture question), auto-archive messages that pass a retention cutoff without being addressed, and auto-replay only for a narrowly pre-approved class of situations (for example, "downstream service X was down for known maintenance window Y, safe to replay everything quarantined during that window") with the idempotency and schema checks still applied even to auto-replays, not just manual ones. Anything outside that narrow, pre-approved class gates on human approval.
Monitoring DLQ health. Depth alone is not enough. Track the age of the oldest quarantined message (a slow leak looks fine on depth if replay keeps pace, age catches it), the replay success rate (a replay that lands right back in the DLQ signals the underlying fix did not actually work), and the arrival rate of new quarantines relative to a normal baseline.
stateDiagram-v2
[*] --> Quarantined: retries exhausted
Quarantined --> UnderReview: on-call or automation inspects
UnderReview --> SchemaCheck: candidate for replay
SchemaCheck --> IdempotencyCheck: schema compatible
SchemaCheck --> Archived: schema incompatible
IdempotencyCheck --> Replayed: safe to reprocess
IdempotencyCheck --> Archived: unsafe or already applied
Replayed --> [*]
Archived --> [*]
Worked example
An illustrative incident, numbers chosen for the walkthrough, not measured: a downstream service outage sends 200 messages to the DLQ. On recovery, the on-call engineer runs the automated checks before batch-replaying. Of the 200: 12 had actually already succeeded on a delayed retry that landed just before the DLQ escalation was processed, the idempotency check filters these out and marks them replayed-as-already-done rather than reprocessing them; 3 have a payload shape that predates a schema migration made two weeks earlier, these route to archive with a note for manual follow-up; the remaining 185 pass both checks and replay cleanly. As a sanity check on the walkthrough's own numbers: 12 + 3 + 185 = 200, accounting for the full batch.
Trade-offs and pitfalls
- Skipping the idempotency re-check on replay is the most common way a DLQ "fix" causes a new incident, double-processing something that had already partially succeeded.
- An auto-replay policy that is too broad (for example, "replay everything older than a fixed age" with no per-message check) reintroduces exactly the failure mode this workflow exists to prevent.
- Monitoring only depth misses a slow, steady quarantine growth that never actually gets worked down, age and replay-success-rate catch what depth alone cannot.
- Not capturing why a message was archived (versus replayed) leaves nothing for someone auditing the incident later to reconstruct the decision.
Implement an exponential backoff retry strategy as a middleware for a Node.js message consumer. The middleware should support max retries, jitter, and configurable base/backoff multipliers. Provide code or clear pseudocode showing retry logic, how failures are bubbled to DLQ after max retries, and where to insert idempotency checks.
Sample Answer
Direct answer
Structure the middleware as a loop around the handler: an idempotency check runs first (before any retry bookkeeping, so a redelivery of an already-processed message never consumes a retry slot), then on each failure compute a capped exponential delay and sleep a random amount between zero and that cap (full jitter), and once the configured maximum retries is exceeded, push the message to a dead-letter queue (DLQ) and stop.
Structured elaboration
Where the idempotency check belongs. It runs before the retry loop, not inside it. A message redelivered after it already succeeded should short-circuit immediately, it should not be treated as attempt 1 of a fresh retry sequence, and it should not re-run the handler's side effects.
The retry loop. On each failure: if the attempt count exceeds maxRetries, escalate to the DLQ and stop; otherwise compute delay = min(maxDelayMs, baseMs * multiplier ** (attempt - 1)) and sleep a uniformly random value between 0 and that delay (full jitter) before the next attempt.
Configurable parameters. baseMs and multiplier control how fast the delay grows per attempt; maxDelayMs caps it so a message near its retry limit does not wait an unreasonably long time; maxRetries bounds total attempts before DLQ escalation.
function makeBackoffMiddleware({ baseMs, multiplier, maxRetries, maxDelayMs, rng, sendToDLQ, idempotencyCheck }) {
return async function process(message, handler) {
// Idempotency check runs BEFORE any retry bookkeeping: a redelivered
// message already committed downstream short-circuits here instead of
// consuming a retry slot or re-running side effects.
if (await idempotencyCheck(message)) {
return { status: 'skipped-duplicate', attempts: 0, delaysMs: [] };
}
let attempt = 0;
const delaysMs = [];
for (;;) {
attempt += 1;
try {
await handler(message);
return { status: 'processed', attempts: attempt, delaysMs };
} catch (err) {
if (attempt > maxRetries) {
await sendToDLQ(message, err, attempt - 1);
return { status: 'dlq', attempts: attempt - 1, delaysMs, error: err.message };
}
// Full jitter: sleep a UNIFORM random value between 0 and the capped
// exponential delay, not the exponential value itself, this is what
// prevents every instance that failed at the same moment from
// retrying in lockstep (the thundering-herd failure mode).
const capped = Math.min(maxDelayMs, baseMs * multiplier ** (attempt - 1));
const jitterMs = Math.round(capped * rng());
delaysMs.push(jitterMs);
// production code awaits a real timer here: await sleep(jitterMs)
}
}
};
}
Worked example
Executed with Node.js, baseMs = 100, multiplier = 2, maxRetries = 4, maxDelayMs = 5000, and a seeded pseudo-random number generator (mulberry32, seeded with 42) standing in for Math.random() purely so the printed jitter values are reproducible for this demo; production code would use Math.random() or crypto.randomInt() instead.
case1 (recovers within budget): {"status":"processed","attempts":4,"delaysMs":[60,90,341]}
case2 (exhausts retries -> DLQ): {"status":"dlq","attempts":4,"delaysMs":[67,35,211,219],"error":"downstream-500"}
dlq contents: [{"id":"evt-2","attempts":4,"error":"downstream-500"}]
case3 (duplicate redelivery skipped): {"status":"skipped-duplicate","attempts":0,"delaysMs":[]}
Case 1: a handler that fails 3 times then succeeds recovers on attempt 4, within the 4-retry budget, with 3 recorded jitter delays (one per failure before the successful attempt). Case 2: a handler that always fails exhausts all 4 retries and escalates to the DLQ, with the DLQ entry recording the event id, attempt count, and error. Case 3: redelivering the same event id from case 1 (already committed) is caught by the idempotency check and returns immediately with zero attempts, confirming the check runs ahead of the retry loop rather than inside it.
Trade-offs and pitfalls
Complexity. O(1) work per attempt (one comparison, one exponentiation, one random draw); total attempts bounded by maxRetries + 1, so worst-case work per message is O(maxRetries).
Edge cases.
-
A message redelivered mid-retry (a second copy of the same event arrives while the first copy's retry loop is still running) needs the idempotency check to be safe under concurrency too, a check-then-act pattern here has the same race risk as any other idempotency check, it needs an atomic claim, not just a lookup.
-
If
handleritself is not safe to partially re-run (it has non-idempotent side effects), retrying at all is unsafe regardless of how well the backoff is tuned, the retry middleware assumes the handler's work is safe to repeat. -
A cap (
maxDelayMs) that is too low relative to how long the downstream outage actually lasts causes retries to keep hammering a still-down dependency at the capped rate instead of backing off further. -
Sleeping the exponential value itself instead of a random value up to it (no jitter at all) reintroduces the thundering-herd risk this design exists to avoid.
-
Forgetting to insert the idempotency check ahead of the retry loop (instead of, say, only checking once right before the DLQ push) lets an already-succeeded message consume retry attempts and, worse, potentially re-run a non-idempotent handler on every redelivery.
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.
Design a coordination mechanism to schedule distributed batch tasks via events so that exactly N out of M workers pick up a task and no task is processed more than once. Discuss leader election, distributed locks, use of Kafka partitioning vs ZooKeeper/etcd, and failure handling for workers that die mid-task.
Sample Answer
Direct answer
There are two standard mechanisms for this, and the choice depends on whether the work already has a natural partition structure. If it does, let a partitioned topic's consumer-group rebalance protocol hand out ownership 1:1, no external coordinator needed. If it does not, use an external coordination service (ZooKeeper or etcd) to hand out a leased, fenced lock per task, so a worker that stops renewing its lease loses ownership automatically, and a monotonically increasing fencing token stops a worker that only appears dead (a long garbage-collection pause, not a real crash) from corrupting the result after ownership has already moved on.
Structured elaboration
Reading "exactly N out of M workers." The dominant real case is N = 1: exactly one of M available workers should own a given task at a time, with the other M - 1 idle or working other tasks. The same mechanism generalizes to N > 1 by treating a task as N independent lease slots (task/slot-0 ... task/slot-N-1) instead of one, each slot claimed and fenced exactly like the N = 1 case below.
Kafka partitioning approach. Model the tasks as a fixed number of partitions on a topic (or hash a task id onto one of a fixed set of partitions), and let Kafka's own consumer-group protocol assign partitions to consumers, exactly one consumer owns a given partition at a time, enforced by session heartbeats to the group coordinator. Extra workers beyond the partition count sit idle as hot standby. This gives "at most one active owner per partition" for free, with no separate lock service to run. Failure handling is a rebalance: a missed heartbeat triggers reassignment of that consumer's partitions to a live one; because offset commits and processing are not perfectly atomic, the new owner may reprocess the last uncommitted batch, so tasks still need to be safe to reprocess (idempotent), the transport guarantee alone is not enough.
ZooKeeper or etcd lock approach. For tasks with no natural partition structure, each task gets its own lock. ZooKeeper's standard recipe: a worker creates a sequential ephemeral node under the task's lock path; the worker holding the lowest sequence number holds the lock (this doubles as the leader-election primitive if a single coordinator process needs to be elected too). Etcd's equivalent: a worker acquires a lease with a time-to-live (TTL) and performs a compare-and-swap on a key tied to that lease. In both, if the worker's session stops renewing (crash, or the process is killed), the ephemeral node or lease expires and another worker can acquire it.
The subtlety failure handling has to cover. A worker whose process is still alive but paused (a long garbage-collection pause, a network partition that cuts it off from the coordinator but not from the downstream system it writes to) still believes it holds the lock after the coordinator has already reassigned it to someone else. This is the split-brain case: two workers, each convinced it alone owns the task. The fix is a fencing token, a number that increases every time the lock changes hands, which the downstream resource being written to checks and rejects any write carrying a token lower than the highest one it has already accepted. This makes "no task processed more than once" hold even when a worker never technically crashed, it just woke up too late.
Leader election specifically. It is only needed if a single coordinator process is the one deciding which task goes to which worker (rather than workers self-organizing via the lock primitive directly). ZooKeeper and etcd's leader-election recipes elect that coordinator the same way: an ephemeral node or leased key, re-elected automatically on failure, so the assignment logic itself does not become a single point of failure.
sequenceDiagram
participant WA as Worker A
participant Coord as Coordinator etcd/ZooKeeper
participant WB as Worker B
participant Res as Downstream resource
WA->>Coord: acquire lease(task)
Coord-->>WA: token=1
WA->>Coord: renew(token=1)
Note over WA: Worker A stalls (GC pause or crash)
WB->>Coord: acquire lease(task) after TTL expiry
Coord-->>WB: token=2
WB->>Res: write(result, token=2)
Res-->>WB: accepted
WA->>Res: write(result, token=1)
Res-->>WA: rejected (stale token)
Worked example
A deterministic simulation of the diagram above, using a manual step counter instead of real time so the sequence is fully reproducible.
class Coordinator:
def __init__(self, lease_ttl_steps: int):
self.lease_ttl_steps = lease_ttl_steps
self.next_token = 0
self.leases = {} # task_id -> {owner, token, last_renew_step}
def try_acquire(self, task_id, worker_id, step):
lease = self.leases.get(task_id)
if lease is not None and (step - lease["last_renew_step"]) < self.lease_ttl_steps:
return None # still held by someone else, not expired
self.next_token += 1
self.leases[task_id] = {"owner": worker_id, "token": self.next_token, "last_renew_step": step}
return self.next_token
def renew(self, task_id, worker_id, token, step):
lease = self.leases.get(task_id)
if lease and lease["owner"] == worker_id and lease["token"] == token:
lease["last_renew_step"] = step
return True
return False
class DownstreamResource:
def __init__(self):
self.max_token_seen = 0
self.committed_writes = []
def write(self, task_id, worker_id, token, value):
if token < self.max_token_seen:
return {"accepted": False, "reason": f"stale token {token} < {self.max_token_seen}"}
self.max_token_seen = token
self.committed_writes.append({"task_id": task_id, "worker_id": worker_id, "token": token, "value": value})
return {"accepted": True}
Run against the scenario in the diagram: worker A acquires the lease at step 0, renews at steps 1 and 2, then stalls (never renews again). Worker B acquires the same task at step 5, after the 3-step lease TTL has elapsed since worker A's last renewal.
step 0: worker-A acquires task, token=1
step 1: worker-A renew ok=True
step 2: worker-A renew ok=True
step 5: worker-B acquires expired task, token=2
step 6: worker-B write -> {'accepted': True}
step 7: worker-A (stale) write -> {'accepted': False, 'reason': 'stale token 1 < 2'}
committed writes: [{'task_id': 'batch-task-42', 'worker_id': 'worker-B', 'token': 2, 'value': 'result-from-B'}]
invariant holds: exactly one committed write, from the worker holding the live lease
Worker A's late write at step 7 (it never crashed, it just paused past the lease TTL) is rejected because its token (1) is lower than the highest token the resource has already accepted (2). Exactly one write commits.
Trade-offs and pitfalls
Complexity and edge cases. Lock acquire and renew are O(1) coordinator operations per task per worker. The main edge case is lease TTL sizing: too short relative to realistic garbage-collection pause or network-blip durations causes thrashing (a live worker keeps losing and reacquiring its own lease); too long delays failover after a genuine crash, letting a task sit unclaimed for that whole window. Clock skew between workers and the coordinator also matters: a lease TTL is only meaningful relative to the coordinator's own clock, workers renewing based on their own clock without accounting for skew can renew "late" from the coordinator's point of view.
- The partition-based approach couples task granularity to a fixed partition count and rebalances at whole-partition granularity, coarser than per-task locks.
- The lock-based approach gives fine, per-task granularity but adds an external coordination service as a new dependency with its own availability story.
- Relying on "the worker believes it is still alive" without fencing is the pitfall the worked example exists to demonstrate: it looks correct until a real garbage-collection pause or network blip happens in production.
- Fencing only works if the downstream resource itself enforces the token check; if the resource cannot support that (a legacy system with no such field), the fencing discipline has nowhere to be enforced and the split-brain risk returns.
What is a Dead Letter Queue (DLQ)? Describe a DLQ-based architecture for handling poison messages in a task processing system, including how you would instrument metrics and alerts, automate quarantining, and implement a manual review and replay workflow. Mention any retention and security considerations.
Sample Answer
Direct answer
A Dead Letter Queue (DLQ) is a separate queue or topic a consumer (or the broker on its behalf) diverts a message to once it has failed processing beyond an agreed limit, so one message that will never succeed, a poison message, cannot block or endlessly retry-loop every other message queued behind it. A DLQ architecture is not just that holding area, it is the metrics and alerting that make its arrivals visible, the automated classification that quarantines messages sensibly, and the manual review and replay workflow that actually resolves them.
Structured elaboration
Why a message ends up in the DLQ. Broadly three reasons: a permanent business-logic or validation failure (the payload is malformed or violates a business rule, retrying changes nothing), a transient failure that simply exhausted its retry budget (the downstream dependency was down long enough that every retry also failed), or a poison message that crashes or hangs the consumer process itself (for example, a payload that triggers a parsing exception on every attempt). Distinguishing these at arrival is what makes the rest of the workflow tractable, they need different handling.
Metrics and alerts. Track DLQ depth (how many messages are currently quarantined), the age of the oldest DLQ message (a slow leak looks fine on depth alone if replay keeps pace, age catches it), arrival rate into the DLQ, and a breakdown by failure reason. Alert on any new arrivals to a topic that is normally near-zero, and on depth or age crossing a threshold tied to how quickly the team can realistically review them (a service-level objective, or SLO, for DLQ review turnaround).
Automated quarantining. Classify each message at the moment it arrives in the DLQ, not later: capture the failure reason, the error type or exception, the attempt count, timestamps, and the originating topic or partition as metadata alongside the payload. Tag messages that look transient (timeouts, connection resets, a known downstream outage window) as candidates for automatic replay after a cool-down; tag messages that look permanent (schema validation failures, business-rule violations) for manual review instead of blind retry.
Manual review and replay workflow. A reviewer inspects the payload, the failure reason, and the attempt history, then decides to replay (re-inject to the main queue), archive (keep for record but do not reprocess), or fix-forward (patch the producer or consumer, then replay). Replayed messages must go back through the exact same idempotency and deduplication path any normal delivery would use, not a special-cased bypass, otherwise a message that partially succeeded before its original failure can be double-processed on replay.
Retention. DLQ messages typically need a longer retention window than the main topic, since a human needs time to notice and act, but not indefinite retention; tie it to compliance and data-retention requirements rather than leaving it as an afterthought default.
Security. A DLQ commonly holds the exact same sensitive payloads as the main data path (a payment event that failed validation is still a payment event), so it needs the same encryption-at-rest and access control as the primary pipeline. Treating the DLQ as a lower-security scratch space is a common and risky oversight, precisely the hardest-to-process messages are the ones most likely to sit there.
flowchart LR
P[Producer] --> Q[Main queue or topic]
Q --> C[Consumer]
C -->|success| Ack[Ack / commit offset]
C -->|failure, retries exhausted| DLQ[Dead-letter queue]
DLQ --> M[Metrics: DLQ depth, age]
M --> Alert[Alert on-call]
DLQ --> Review[Manual review / classification]
Review -->|fixable| Replay[Replay to main queue]
Review -->|not fixable| Archive[Archive / discard]
Worked example
A payment consumer starts failing on three messages because an upstream producer shipped a breaking field rename. Each message exhausts its retry budget and is diverted to the DLQ. The unusual arrival rate (normally zero, now three in a minute) trips an alert. Automated classification tags all three as schema-related, since the failure is a deserialization error rather than a downstream timeout, routing them to manual review instead of an automatic retry loop that would just fail the same way again. On-call fixes the producer (or adds a compatibility shim on the consumer side), confirms the fix against one message manually, then replays the remaining three, and access logs record who replayed them, satisfying the audit angle of the security requirement.
Trade-offs and pitfalls
- A DLQ with no automated classification or alerting quietly becomes a graveyard nobody looks at, the single most common real-world failure of this pattern, not a hypothetical.
- Replaying a message without re-running it through idempotency checks is the most common way a "fixed" DLQ incident turns into a second incident, double-processing.
- Indefinite DLQ retention creates both a storage cost problem and, for sensitive payloads, a compliance liability; set retention deliberately.
- Treating every DLQ arrival identically (always retry, or always require manual review) wastes either engineering attention or retry budget; the transient-versus-permanent classification is what makes the workflow scale.
Unlock Full Question Bank
Get access to all 47 Event-Driven Architecture and Asynchronous Messaging interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.