TOPIC 08

Async Processing & Messaging

Not everything needs an answer right now. Queues let services hand off work, absorb traffic spikes like a shock absorber, and retry failures without anyone waiting on the line.

Intermediate 30 min read 10 concepts

At a Glance

Why Go Asynchronous?

When a user signs up, you might want to: create the account, send a welcome email, resize their avatar, update analytics, and notify the CRM. Doing all five synchronously means the user stares at a spinner while five systems (each with their own failure modes) hold their request hostage.

The async alternative: do the essential bit (create the account), drop messages onto a queue for everything else, and respond immediately. Three superpowers follow:

Producer fires & forgets Queue Consumer 1 Consumer 2

The queue is a buffer: producers burst, consumers drain steadily, nobody waits on anybody.

Why Async? The Core Insight

Strip away the tooling and the idea is simple. A synchronous call means the caller waits for the callee to finish — if the callee is slow, the caller is slow; if the callee is down, the caller fails. An asynchronous call means the caller puts work in a queue and moves on; the callee processes it at its own pace, whenever it can.

That one change buys you three distinct kinds of decoupling:

T

Temporal decoupling

Producer and consumer don't need to be up at the same moment. The email service can be mid-deploy while signups keep flowing — the messages simply wait.

R

Rate decoupling

The producer can spike to 10,000 msgs/sec for a minute while the consumer steadily drains 1,000/sec. The queue absorbs the difference, and the consumer catches up later.

F

Failure decoupling

If a consumer crashes mid-task, the unacked message stays on the queue and is retried when it restarts. The producer never even knows there was a problem.

Synchronous User API server blocks the request Email provider takes 3 seconds user stares at a spinner for 3s — or sees an error Asynchronous User API server responds in ~20 ms Queue Worker sends email user gets an instant response; email sends in the background

Same work, different experience: async returns immediately and lets the slow part happen off the critical path.

Delivery Guarantees: Pick Your Poison

Networks drop packets and consumers crash mid-task — so "the message arrives exactly once" is harder than it sounds. Every messaging system offers one of three contracts:

1
At-most-once

Fire and forget — no retries. Messages can vanish, but never duplicate. Fine for metrics, logs, presence pings where the next data point supersedes the lost one anyway.

2
At-least-once

Redeliver until the consumer acknowledges. Nothing is lost — but if a consumer finishes the work and crashes before acking, the message is delivered again. Duplicates are a fact of life. The industry default.

3
Exactly-once

The unicorn. True exactly-once delivery is impossible over an unreliable network; what systems offer is exactly-once processing — at-least-once delivery plus deduplication or transactions. Kafka and SQS FIFO get close, with real overhead.

The practical answer: idempotency

Design consumers so processing the same message twice is harmless. Use an idempotency key: "have I already processed payment msg_8f3a? Then ack and skip." At-least-once delivery + idempotent consumers gives you exactly-once effects — the pattern behind Stripe's API and most payment systems.

Message Delivery Guarantees — Deep Dive

Knowing the three contracts is table stakes; knowing how to implement them is what interviews probe. Here's the practical playbook for each:

Idempotency in one sentence

Before doing the work, ask "did I already process payment_id=abc123?" A payment service checks its processed-IDs table (or SETNX in Redis) before charging — the second delivery of the same message becomes a no-op instead of a double charge.

The Outbox Pattern: Killing the Dual-Write Problem

A classic bug: your service writes an order to the database, then publishes an OrderCreated event to the queue. If it crashes between the two, the DB and the queue disagree forever. The transactional outbox pattern eliminates this:

1
Write atomically

In one DB transaction, write the order row and a row in an outbox table describing the event. Either both commit or neither does.

2
Poll and publish

A separate poller (or CDC tool like Debezium) reads unpublished outbox rows and publishes them to the queue.

3
Mark published

After the broker acks, mark the outbox row as published. A crash between publish and mark just means a duplicate publish — which your idempotent consumers already handle.

The guarantee: the message is published if and only if the transaction committed. No more dual-write inconsistency.

Event Streaming: The Kafka Model

A queue deletes a message once consumed. Kafka flips the model: it's a durable, append-only log that consumers read at their own pace.

This makes Kafka the backbone for event sourcing (the log is the source of truth), audit trails, change-data-capture, and stream processing (Flink, Kafka Streams). LinkedIn built it to move trillions of events per day; Uber, Netflix and Airbnb run their nervous systems on it.

Kafka Architecture Deep Dive

The previous section gave you the model; here's the machinery underneath, piece by piece:

Producer partitions by key Topic: orders P0 P1 P2 Consumer group A analytics offset: 1,204,557 Consumer group B fraud detection offset: 1,198,002

One topic, three partitions, two consumer groups — each group reads the full stream independently at its own offset.

Task Queues: Async for Application Code

A task queue wraps a message broker in developer-friendly clothing: you enqueue function calls, and worker processes execute them with retries, scheduling, and result tracking built in.

# Celery: this runs in a worker, not in the web request
@app.task(max_retries=3, retry_backoff=True)
def send_welcome_email(user_id):
 user = db.get_user(user_id)
 mailer.send("welcome", to=user.email)

# In the request handler — returns instantly:
send_welcome_email.delay(user.id)

Ecosystem picks: Celery (Python), Sidekiq (Ruby), BullMQ (Node). Classic jobs: sending email, resizing images and video transcoding, generating PDFs and reports, syncing with third-party APIs — anything slower than ~100 ms that the user doesn't need to wait for.

Back Pressure: When the Queue Fights Back

Queues hide overload — until they don't. If producers enqueue at 1,000/s and consumers drain at 600/s, the queue grows by 400/s forever. Latency balloons (messages wait hours), memory or disk fills, and the broker — your decoupling hero — becomes the outage.

Back pressure means propagating "slow down!" signals upstream instead of buffering infinitely:

Monitor lag, not just errors

A queue-based system can be "100% up" while every message waits 45 minutes. Queue depth and consumer lag are the heartbeat metrics of async systems — alert on them like you'd alert on 500s.

Choosing a Tool

RabbitMQKafkaAWS SQSGCP Pub/Sub
ModelSmart broker, routing exchangesDistributed append-only logManaged simple queueManaged pub/sub stream
Replay old messagesNo (consumed = gone)Yes — rewind offsetsNoLimited (seek/retain)
OrderingPer queuePer partitionFIFO queues onlyPer ordering key
ThroughputHighExtreme (millions/s)High (auto-scales)High (auto-scales)
Ops burdenSelf-managed (or cloud)Heaviest (or use managed)ZeroZero
Sweet spotTask routing, RPC-ish work queuesEvent streaming, event sourcing, analytics pipelinesDecoupling on AWS with no opsStreaming on GCP with no ops
Archie says

Rule of thumb for interviews: need work done (emails, thumbnails)? Say task queue / SQS / RabbitMQ. Need facts recorded and shared (events many systems consume, possibly re-read later)? Say Kafka. Saying "Kafka" for a simple email job is like buying a freight train to deliver a pizza.

Event Sourcing and CQRS

Traditional systems store current state: a users row holds the latest email, the latest address. The question "what was this account's state last Tuesday?" requires a separate audit log — if anyone remembered to build one.

Event Sourcing flips it: store the events (facts), not the state. UserCreated, EmailChanged, OrderPlaced — an append-only sequence of things that happened. Current state is derived by replaying all events.

+

Benefits

A full audit trail for free, time travel ("state at any T"), and replayability — rebuild any projection by re-running the event log.

Costs

Eventual consistency between events and derived views, more complex queries, and snapshots needed for performance (replaying 10M events on every read is not a plan).

@

Who uses it

Banks (every transaction is an event — your balance is literally a replay), the Axon framework, and Kafka-centric architectures where the log is the source of truth.

CQRS: Separate Writes from Reads

CQRS (Command Query Responsibility Segregation) splits the system into a write model (commands that change state) and a read model (queries that read state):

Discord runs a CQRS-like pattern: message writes go to Cassandra (the write model), while Elasticsearch powers search (a read model fed asynchronously from the write path).

Saga Pattern — Distributed Transactions

Microservices each own their database — so no single ACID transaction can span "create order, charge card, reserve stock." A saga replaces the impossible distributed transaction with a sequence of local transactions, each publishing events that trigger the next step — and each paired with a compensating transaction to undo it if a later step fails.

CH

Choreography saga

No central coordinator — each service listens for events and reacts. OrderService creates the order, PaymentService charges the card, InventoryService reserves stock, ShippingService schedules delivery. On failure, PaymentService publishes OrderPaymentFailed and OrderService cancels the order. Decoupled — but the flow lives nowhere, so it's hard to understand and debug.

OR

Orchestration saga

A central Saga Orchestrator tells each service what to do and triggers compensations on failure. Much easier to understand and debug — one place holds the whole flow — at the cost of a component that's a single point of failure if it goes down. Used by Uber (Cadence/Temporal) and Netflix (Conductor).

Dead Letter Queues and Poison Pills

A poison pill is a message that always crashes the consumer — malformed JSON, a null field that triggers an exception. Without protection, the loop is grim: consumer crashes, restarts, re-receives the same message, crashes again. Forever.

The fix is a Dead Letter Queue (DLQ): after N failed processing attempts (typically 3), the message is moved out of the main queue and into the DLQ, where it can't hurt anyone.

Tool Comparison: RabbitMQ vs Kafka vs SQS vs Pub/Sub

The earlier table gave the highlights; here's the fuller spec sheet you'd want before committing:

FeatureRabbitMQApache KafkaAWS SQSGoogle Pub/Sub
ModelMessage broker (push)Event log (pull)Queue (pull)Message broker (push/pull)
Message retentionUntil consumedConfigurable (default 7 days)4 days (max 14)7 days (default)
ThroughputModerate (~50K msgs/s)Very high (millions/s)HighHigh
ReplayNoYes (seek to offset)NoYes (seek)
OrderingPer-queue FIFOPer-partitionFIFO queues onlyPer-key ordering
DeliveryAt-least-onceAt-least-once (exactly-once with transactions)At-least-onceAt-least-once
Best forTask queues, routing, RPCEvent streaming, log aggregation, analyticsSimple decoupling on AWSGCP services, pub/sub patterns
ExamplesCelery, background jobsKafka Streams, FlinkMicroservices on AWSFirebase, GCP pipelines

Interview Q&A

Three messaging questions interviewers actually ask — click to reveal strong answers.

Message queue (RabbitMQ, SQS): task-based — one consumer processes each message, and the message is deleted after consumption. Use for: background jobs, email sending, image processing, webhook delivery. Event streaming (Kafka): an event log that multiple consumer groups read independently, with messages retained. Use for: audit logs, analytics pipelines, event sourcing, and feeding multiple downstream systems (payments → analytics + fraud detection + notifications simultaneously). The key question: do you need replay, multiple independent consumers, or long retention? Kafka. Just want reliable task execution? A queue.

Design consumers to be idempotent. Three techniques: (1) Unique constraint: try the INSERT, catch the duplicate-key error, treat it as success. (2) Check-then-process: SELECT first, only process if not seen before — but beware race conditions; use SELECT FOR UPDATE or a Redis SETNX lock. (3) Idempotency key: the client provides a unique ID with each request; the server stores processed IDs in Redis or the DB and deduplicates. The pattern: if redis.setnx(f"processed:{msg_id}", 1, ex=3600): process(msg) — only the first delivery is processed.

(1) Priority queues: separate queues for CRITICAL (account security), HIGH (order updates), LOW (marketing). CRITICAL uses SQS FIFO or Kafka priority partitions and is processed first. (2) Fanout: one event ("order_placed") → fanout service → separate queues per channel (email, push, SMS), each with dedicated consumers calling SES, FCM, and Twilio. (3) Rate limiting: email providers have per-second limits — use a token bucket limiter per provider. (4) Retry: SQS visibility timeout plus a dead letter queue for failures. (5) Scale check: 100M/day ≈ 1,160/sec average, ~5,000/sec peak — Kafka handles this trivially with 10 partitions × 10 consumers per channel.

Anti-Patterns to Avoid

Using async for synchronous user flows

Don't put checkout behind a message queue if the user is waiting for order confirmation. Async makes sense for fire-and-forget (send the receipt email), not for blocking operations (charge the card).

Unbounded queue growth

If consumers are consistently slower than producers, the queue fills → RAM/disk exhaustion → system crash. Always monitor queue depth and lag, and auto-scale consumers.

Missing idempotency

"We're at-least-once, no big deal" leads to double-charging users, duplicate emails, and ghost inventory updates. Every consumer MUST be idempotent under at-least-once delivery.

Event-Driven Architecture — Core Concepts

Event-Driven Architecture (EDA) is a system design pattern where components communicate through events rather than direct synchronous calls. When something happens in one service, it publishes an event; other services listen and react asynchronously — without the producer knowing who consumes it.

Key Characteristics: Asynchronous processing (non-blocking workflows); Loose coupling (services only share event contracts, not direct dependencies); Scalability & flexibility (new consumers can be added without modifying producers).

Synchronous vs. Asynchronous: In synchronous (request-response) systems, the caller blocks until a response arrives — tight coupling, simple to reason about. In EDA, the producer fires an event and continues — the consumer processes it independently, enabling higher throughput and resilience.

Pub-Sub vs. Event Streaming: Pub-Sub (RabbitMQ, AWS SNS) broadcasts events to subscribers — events are transient, consumed once, and have no strict ordering. Event Streaming (Kafka, AWS Kinesis) persists events in an ordered log that consumers can replay — ideal for audit trails, event sourcing, and reprocessing historical data.

Key components: Event Producers generate events; Event Brokers (Kafka, RabbitMQ, AWS EventBridge) transmit and optionally store them; Event Consumers process events asynchronously; Event Store provides log-based persistence for replaying events.

Best Practices: Use idempotent event processing to handle duplicate deliveries safely; implement Dead-Letter Queues (DLQs) for failed messages; version events using a schema registry to handle schema evolution without breaking consumers.

Interview Prep

Event-Driven Architecture Interview Questions

Key interview questions covering EDA fundamentals, scalability, implementation technologies, and real-world use cases.

Fundamentals

Event-Driven Architecture (EDA) is a software architecture pattern where system components communicate through events rather than direct synchronous calls. Instead of services invoking each other directly, they publish events when something happens, and other services listen for and react to those events asynchronously.

Differences from Request-Response: Decoupling — in REST-based systems, services depend on each other directly; in EDA services are decoupled and interact only through events. Scalability — EDA scales better as new consumers can subscribe to events without modifying producers. Asynchronous Processing — traditional architectures require waiting for responses; EDA enables non-blocking workflows.

Pub-Sub (RabbitMQ, AWS SNS, Redis Pub/Sub): One-to-many event distribution. Events are transient — once consumed, they are gone. No strict ordering guarantee. Best for real-time notifications where consumers need only the latest event.

Event Streaming (Apache Kafka, AWS Kinesis): Events are stored and can be replayed. Maintains strict event order within partitions. Events persist for later processing. Best for processing historical data, event sourcing, and audit logs where consumers may need to replay past events.

1. Event Producers: Emit events when something happens (e.g., a user places an order, a sensor reports a reading).

2. Event Brokers: Middleware that routes events — examples: Kafka, RabbitMQ, AWS EventBridge. Acts as the communication backbone between producers and consumers.

3. Event Consumers: Services that process events asynchronously and react (update a database, send a notification, trigger a workflow).

4. Event Store (optional): A persistent log of all events used for auditing, debugging, or event replay to reconstruct past system state.

Scalability & Fault Tolerance

Challenges: Eventual Consistency — events propagate asynchronously so data may not be updated instantly across all services; Event Ordering — ensuring correct sequence in distributed systems; Debugging Complexity — events flow across many services making it hard to trace issues; Handling Failures — consumers may fail while processing events.

Handling Eventual Consistency: Use idempotent operations to avoid duplicate updates; implement Sagas (orchestration/choreography patterns) to ensure business logic correctness; leverage Event Sourcing to reconstruct system state from the ordered event log if inconsistency is detected.

Partitioning: Kafka uses partitions to ensure ordering within each partition — events for the same key (e.g., same user ID) always go to the same partition.

Event Versioning: Maintain a version number in events and process them sequentially, rejecting out-of-order versions.

Global Ordering Service: Use a dedicated ordering service that assigns sequence numbers to events before they reach consumers.

Deduplication: Implement unique event IDs so consumers can detect and discard already-processed or out-of-order duplicate events.

A Dead-Letter Queue (DLQ) is a separate queue where failed or unprocessable messages are sent after exhausting retry attempts.

Importance: Prevents infinite retries — avoids a faulty message being retried indefinitely and blocking the queue; Facilitates debugging — developers can inspect dead-lettered events to identify root causes; Ensures reliability — prevents faulty messages from blocking the main queue and causing backpressure across the system.

In Kafka, a dedicated topic stores dead-lettered messages for later analysis and reprocessing after the root cause is fixed.

Implementation & Technologies

Apache Kafka: Event Streaming type. Stores events for replay with guaranteed ordering within partitions. Highly scalable with partitions. Best for large-scale event processing and analytics pipelines.

RabbitMQ: Pub-Sub type. Transient messages (once consumed, gone). No strict ordering guarantee. Scales horizontally. Best for real-time messaging and task queues where replay is not needed.

AWS EventBridge: Managed Event Bus. No persistence. No strict ordering. Scales with AWS infrastructure. Best for cloud-native event integration between AWS services and external SaaS applications.

Idempotency ensures that processing the same event multiple times does not produce unintended side effects — critical in at-least-once delivery systems.

Use Unique Event IDs: Track processed event IDs in a database or cache to prevent re-processing the same event twice. Store Processed States: Maintain a "processed" flag in the database for each event. Ensure Business Logic is Idempotent: Avoid unconditional increments or timestamps — check current state first. Leverage Broker Deduplication: Kafka allows deduplication via log compaction; SQS has a deduplication ID feature.

Use Cases & Real-World Applications

E-commerce Order Processing System: When a customer places an order, multiple actions must happen: the Order Service records the order, the Payment Service processes payment, the Inventory Service updates stock, and the Notification Service sends a confirmation.

Using EDA, each service listens for order events asynchronously, with no direct dependencies on each other. Benefits: scalability (each service scales independently), fault isolation (a payment failure does not crash inventory), and flexibility (new services like fraud detection or analytics can be added by simply subscribing to order events without modifying existing code).

When events change over time (adding/removing fields), breaking changes can occur. Best practices:

Backward Compatibility: Ensure new consumers can still process older event formats. Versioning: Include a schema_version field in events and maintain handlers for different versions. Schema Registry: Use tools like Kafka Schema Registry (with Avro or Protobuf) to validate schema changes before they reach consumers. Field Deprecation: Instead of removing fields immediately, mark them as deprecated and stop producing them gradually over multiple releases. Event Transformation Layer: Introduce an intermediary service that translates older event schemas to the new format for legacy consumers.

Interview Prep

Messaging & Queues Interview Questions

Key interview questions on asynchronous messaging, queue design, and decoupling strategies.

Asynchronous messaging decouples the sender and receiver, allowing them to operate independently. This improves scalability because producers don't wait for consumers; resilience because messages are queued even if the consumer is slow or down; performance because users get a fast response while background tasks (e.g., sending emails, processing orders) are offloaded; and loose coupling because systems can evolve independently without tight runtime dependencies. A classic example: sending a confirmation email after a purchase — there is no need to delay the user response for email delivery.

RabbitMQ uses a queue-based push model with FIFO ordering per queue. Messages are deleted once consumed, making it ideal for real-time, transactional workloads that need per-message acknowledgment and complex routing (e.g., order processing, real-time updates). Kafka uses a log-based pull model with per-partition ordering and configurable retention — messages are kept even after consumption, enabling replay. Kafka is best for high-throughput data pipelines, event sourcing, and analytics (e.g., tracking user behavior at scale). RabbitMQ delivers at-most-once or at-least-once guarantees; Kafka can achieve exactly-once with the right configuration.

At-most-once: The message is sent once and not retried on failure. It is fast and simple but can lose data, making it appropriate for non-critical logging or telemetry. At-least-once: The message is retried until acknowledged, which is the default in most systems. It may result in duplicates so consumers must be idempotent. Suitable for order placement and email dispatching. Exactly-once: The message is delivered exactly once with no duplication or loss. It is complex and expensive to implement but required for critical financial transactions such as bank transfers and billing systems.

Queues act as a buffer between producers and consumers, so producers can send messages quickly without waiting for slow consumers. Consumers can be scaled horizontally and independently of producers to keep up with load. For fault tolerance: if a consumer crashes, messages remain safely in the queue until it recovers or another consumer instance takes over. Queues also smooth traffic spikes — during high-traffic events like Black Friday, the queue absorbs the surge and lets backend systems catch up at their own pace without being overwhelmed.

Common problems include queue backlog when consumers can't keep up, increased message delay leading to latency spikes, message duplication from retries, and resource exhaustion from CPU and memory strain caused by deep backlogs. Mitigations: scale consumers horizontally using auto-scaling groups; optimize message processing to reduce per-message work; use dead-letter queues (DLQ) to isolate and inspect poison messages that repeatedly fail; and implement rate limiting and backpressure mechanisms to signal producers to slow down when the system is under strain.

When a user places an order, the frontend service acts as a producer and publishes an order message to a queue. An Order Service (consumer) reads from the queue, validates stock, reserves inventory, and triggers downstream services for payment and confirmation emails — each of which may publish their own events in an event-driven chain. The queue provides value by decoupling payment and inventory logic from the frontend (users get an instant response), handling retries automatically on transient failures, and allowing each downstream service to scale independently based on its own load.

Assign each message a unique message ID (e.g., Order ID or broker-generated Message ID) and maintain a processed-message store (in Redis or a database table) that tracks messages already handled. Before processing a message, check whether its ID already exists in the store — if so, skip it. Make business operations inherently idempotent: for example, "mark order as paid" should be a no-op if the order is already marked paid. Leverage built-in deduplication features where available (e.g., AWS SQS FIFO queues have a deduplication window). This is essential in at-least-once delivery scenarios where retries are expected.

Further Reading

Quick Check

Your payment consumer might receive the same "charge customer" message twice (at-least-once delivery). What's the right defense?

Right! At-most-once would risk silently losing charges — worse. Idempotent processing (dedupe by idempotency key) turns at-least-once delivery into exactly-once effects, which is how real payment systems work.
Try It

Interactive: Message Queue Visualiser

Produce messages and watch the queue fill up. Drag the consumer speed slider to see backpressure build when production outpaces consumption. Flood the queue to trigger the high-load warning.