Lessons
1 · System Design Foundations
2 · APIs and Communication
3 · Capacity Estimation
4 · Core Building Blocks
5 · Databases
- 12Relational Databases and Data Modeling for System Design
- 13Database Indexes and Query Performance: How to Make Reads Fast
- 14Transactions, Isolation Levels and Concurrency Control: Preventing Race Conditions
- 15Database Replication: Primary-Replica, Read Replicas, Lag and Failover
- 16Database Sharding and Partitioning: How It Works and When You Need It
- 17SQL vs NoSQL: Data Models and How to Choose the Right Database
6 · Distributed Systems
- 18Why Distributed Systems Are Hard: Partial Failure, Timeouts and Clocks
- 19CAP Theorem and Consistency Models Explained Without the Myths
- 20Idempotency, Retries and Message Delivery Guarantees
- 21Distributed Transactions: Two-Phase Commit, the Outbox Pattern and Sagas
- 22Distributed Locks, Leases, Leader Election and Consensus
7 · Reliability and Operations
8 · Architecture and Security
9 · Interview Method and Foundation Case Studies
10 · Real-World Case Studies
- 33Design a Notification System: Email, SMS and Push at Scale
- 34Design a Chat System Like a Messaging App
- 35Design a News Feed: Fan-Out, Ranking and the Celebrity Problem
- 36Design Search Autocomplete (Typeahead)
- 37Design a Distributed Job Scheduler
- 38Design a Ticket Booking System Without Overselling
- 39Design a Payment System: Idempotency, Ledgers and Reconciliation
11 · Large-Scale Case Studies
- 40Design a Video Streaming Platform: Upload, Transcoding and Adaptive Streaming
- 41Design a Ride-Hailing System: Location, Matching and Real-Time Updates
- 42Design a Food Delivery Platform: Orders, Restaurants and Delivery Partners
- 43Design an E-Commerce System: Catalog, Cart, Checkout, Inventory and Flash Sales
- 44Design a Web Search Engine: Crawling, Inverted Indexes and Ranking
12 · Interview Readiness
Lesson 11 · Core Building Blocks
Message Queues, Pub/Sub and Event Streams: How Asynchronous Systems Work
Why queues exist and how they work: producers, consumers and brokers, acknowledgements and redelivery, visibility timeouts, retries, dead-letter queues, ordering, and delivery guarantees. Compares work queues, pub/sub and event streams such as Kafka, and shows how to design background workers that are safe to run twice.
For
Beginners and SDE-1 candidates who need to add background processing to a design, and developers who use queues and want to understand their guarantees and failure modes.
Level
Before you start
Read Synchronous vs Asynchronous Communication (article 05) for when to defer work, and Load Balancers (article 08) for stateless workers. API idempotency from article 04 is useful background.
On this page
Key takeaways
- A queue decouples producers from consumers in time: it absorbs spikes, lets slow work happen later, and retries failed work without failing the original request.
- A message is removed only after the consumer acknowledges it; if the consumer crashes first, the message is delivered again, so most queues are at-least-once and consumers must tolerate duplicates.
- Use a dead-letter queue to park messages that keep failing, and monitor queue depth, age of the oldest message and consumer lag.
- Work queues give each message to one consumer, pub/sub gives each message to every subscriber, and event streams keep an ordered, replayable log that many consumer groups read at their own pace.
- Ordering is guaranteed only within a partition or message group, so choose the key that must stay in order and accept less parallelism for it.
In article 05, Pixly's upload request saved the photo and then "put a message on a queue" so that thumbnails, moderation, feed updates and notifications could happen later. That one sentence hides a lot of machinery. Where does the message live while it waits? What happens if a worker crashes halfway through making a thumbnail? What if the message is delivered twice, or a malformed message fails forever? What if uploads arrive faster than workers can keep up?
This article answers those questions. Queues are one of the most useful tools in system design, and also one of the most frequently misused ("add Kafka") so the goal is to understand what they guarantee, what they do not, and how to choose between a work queue, pub/sub and an event stream.
What you will learn
#- The problems queues solve, and the vocabulary: producer, consumer, broker, acknowledgement, consumer group, offset.
- Three models: work queues, publish/subscribe, and event streams.
- The life of a message: delivery, acknowledgement, visibility timeout and redelivery.
- Retries, poison messages and dead-letter queues.
- Delivery guarantees: at-most-once, at-least-once and "effectively once".
- Ordering, partitions and the trade-off with parallelism.
- Backlogs and backpressure, and how to design good workers and messages.
- How to choose, and how each option fails.
Why queues exist
#A message queue is a durable buffer between parts of a system. A producer writes messages to it; a consumer reads and processes them. The software that stores and delivers messages is the broker.
Queues solve five problems:
- Decoupling in time. The producer does not wait for the consumer, and the consumer does not have to be running when the message is sent.
- Load leveling. A burst of 10,000 messages is absorbed by the queue; consumers process them at a steady rate (article 05).
- Reliable retries. If processing fails, the message is still there to try again, without failing the user's original request.
- Fan-out. One event ("photo uploaded") can trigger several independent pieces of work.
- Independent scaling. You scale consumers by adding workers, without touching producers.
Three models
#1. Work queue (point-to-point)
#Each message is processed by one consumer. Many workers read from the same queue, and the broker hands each message to just one of them. Microsoft calls this the Competing Consumers pattern: multiple concurrent consumers process messages from the same channel, to increase throughput and availability (Competing Consumers pattern).
Producer --> [ m5 m4 m3 m2 m1 ] --> Worker A gets m1 --> Worker B gets m2 --> Worker C gets m3Use for: background jobs where each job should run once, such as "make thumbnails for photo 9001" or "send this email". Examples: Amazon SQS, RabbitMQ queues, Azure Service Bus queues, Google Cloud Tasks.
2. Publish/subscribe (pub/sub)
#A producer publishes a message to a topic. Every subscription to that topic gets its own copy. Each subscription may itself be read by several competing workers. Microsoft's Publisher-Subscriber pattern describes this: an application announces events to multiple interested consumers asynchronously, without coupling senders to receivers (Publisher-Subscriber pattern).
+--> subscription "thumbnails" --> thumbnail workersProducer --> topic ------+--> subscription "moderation" --> moderation workers "photo.uploaded" +--> subscription "notifications" --> notification workersUse for: one event that several independent services care about. Adding a new consumer means adding a subscription; the producer does not change. Examples: Amazon SNS with SQS queues behind it, Google Cloud Pub/Sub, Azure Service Bus topics. Google describes Pub/Sub as an asynchronous, scalable messaging service that decouples services producing messages from services processing them (Pub/Sub overview).
3. Event stream (log)
#An event stream is an append-only, ordered log that the broker keeps for a configured time (hours, days, or longer), whether or not anyone has read it. Consumers do not delete messages; each consumer tracks its own position (an offset) in the log, and can rewind to replay history.
Apache Kafka is the best-known example. Its documentation describes events stored durably in topics, topics split into partitions spread across brokers, events with the same key written to the same partition, and a guarantee that any consumer of a given partition reads its events in exactly the order they were written; events are retained for a configurable period rather than deleted after consumption (Introduction to Kafka).
Topic "orders", partition 0: [o1][o4][o7][o9] ... <-- group "billing" at offset 3 <-- group "analytics" at offset 1Topic "orders", partition 1: [o2][o3][o5][o6][o8]...Several consumer groups read the same stream independently, each at its own pace; within a group, each partition is read by one consumer at a time, which is how a stream scales out while keeping per-partition order.
Use for: high-throughput event pipelines, multiple consumers that each need the full history, replay (rebuild a search index from events), stream processing, and event-driven architectures (article 27). Examples: Apache Kafka (and managed versions such as Amazon MSK), Amazon Kinesis Data Streams, Azure Event Hubs, Redis Streams.
Comparison
#Work queue | Pub/sub | Event stream | |
|---|---|---|---|
Who gets a message | One consumer | Every subscription (one consumer within each) | Every consumer group, which reads the log |
After processing | Message is deleted | Deleted from that subscription | Stays in the log until retention expires |
Replay | No | Usually limited | Yes, rewind the offset |
Ordering | Usually none; FIFO variants by group | Usually none; some offer ordering keys | Per partition |
Typical scale | Moderate to high | Moderate to high | Very high throughput |
Operational weight | Low (especially managed) | Low to moderate | Higher: partitions, retention, consumer groups |
Classic use | Background jobs | Fan-out of events to services | Event pipelines, analytics, replayable history |
The life of a message
#Here is what happens to one message in a typical work queue:
1. Producer sends message --> broker stores it durably, replies "stored"2. Consumer receives it --> broker hides it from other consumers (it is "in flight")3. Consumer processes it4a. Success: consumer ACKNOWLEDGES (deletes) it --> gone for good4b. Crash or error: no acknowledgement arrives --> after a timeout, the message becomes visible again and is redeliveredThe key idea is the acknowledgement. The broker only forgets a message when the consumer says it is done. Amazon SQS implements step 2 with a visibility timeout: a received message stays in the queue but is hidden from other consumers; if it is not deleted before the timeout expires (30 seconds by default, adjustable), it becomes visible again and can be received by another consumer. The SQS documentation also states that, because of its at-least-once delivery model, there is no absolute guarantee a message will not be delivered more than once (Amazon SQS visibility timeout).
In a stream like Kafka, the equivalent of acknowledging is committing the offset: the consumer records "I have processed up to here". If it crashes before committing, it resumes from the last committed offset and processes some messages again.
Choosing the timeout
#- Too short: a slow but healthy worker is still processing when the message reappears; a second worker processes it too. Duplicates rise.
- Too long: when a worker really crashes, its message waits a long time before anyone retries it.
For long tasks, a worker can extend the timeout periodically (a heartbeat) while it works, so the message stays hidden only as long as the worker is alive.
Retries, poison messages and dead-letter queues
#Some failures are transient: the email provider timed out, the database was briefly busy. Retrying later works. Others are permanent: the message is malformed, or refers to a photo that was deleted. Retrying forever wastes capacity and can block other messages. Such a message is called a poison message.
The standard answer is a dead-letter queue (DLQ): after a message has been received a set number of times without success, the broker moves it to a separate queue for investigation. SQS, for example, moves a message to the configured DLQ when its receive count exceeds a maximum you set (Using dead-letter queues in Amazon SQS).
Good practice:
- Retry with backoff. Do not retry instantly in a tight loop; wait longer after each failure (article 20 adds random jitter).
- Classify errors. Retry transient ones; send clearly permanent ones to the DLQ immediately.
- Alert on DLQ growth. A DLQ nobody watches is a place where data quietly disappears.
- Make redrive easy. After fixing a bug, move messages from the DLQ back to the main queue.
Delivery guarantees
#Guarantee | How it happens | Risk |
|---|---|---|
At-most-once | Acknowledge before processing | A crash during processing loses the message |
At-least-once | Acknowledge after processing | A crash after processing but before acknowledging causes a redelivery: duplicates |
Exactly-once | Not achievable end to end in general across independent systems | What is achievable is "effectively once": at-least-once delivery plus a consumer whose effects are idempotent |
Most systems choose at-least-once because losing work is usually worse than doing it twice, and then make the consumer idempotent: processing the same message twice has the same effect as processing it once. Some brokers offer "exactly-once" features within their own boundaries (for example, transactions inside Kafka), but as soon as your consumer touches an outside system (sends an email, charges a card, writes to another database) you need idempotency in your own code. Article 20 covers the techniques; the simulation below shows the basic idea.
Simulation: redelivery, deduplication and a dead-letter queue
#This Java program models a work queue in memory. Message m2 fails once with a transient error and succeeds on retry. Message m3 is malformed and fails every time, so it ends in the DLQ. Message m1 is delivered twice (as at-least-once delivery allows), and the consumer skips the duplicate because it remembers which ids it has processed.
// File: QueueDemo.javaimport java.util.ArrayDeque;import java.util.ArrayList;import java.util.Deque;import java.util.HashMap;import java.util.HashSet;import java.util.List;import java.util.Map;import java.util.Set;public class QueueDemo { record Message(String id, String body) {} static final Deque<Message> queue = new ArrayDeque<>(); static final List<Message> deadLetters = new ArrayList<>(); static final Map<String, Integer> attempts = new HashMap<>(); static final Set<String> processed = new HashSet<>(); // in real systems: durable storage static final int MAX_ATTEMPTS = 3; static void handle(Message m) { if (m.body().equals("corrupt")) { throw new IllegalArgumentException("cannot parse"); // permanent } if (m.id().equals("m2") && attempts.get("m2") == 1) { throw new IllegalStateException("timeout calling email provider"); // transient } if (!processed.add(m.id())) { System.out.println(" " + m.id() + " already processed, skipping side effect"); return; } System.out.println(" " + m.id() + " processed: " + m.body()); } public static void main(String[] args) { queue.add(new Message("m1", "send welcome email to u1")); queue.add(new Message("m2", "send welcome email to u2")); queue.add(new Message("m3", "corrupt")); queue.add(new Message("m1", "send welcome email to u1")); // duplicate delivery while (!queue.isEmpty()) { Message m = queue.poll(); int attempt = attempts.merge(m.id(), 1, Integer::sum); System.out.println("deliver " + m.id() + " (attempt " + attempt + ")"); try { handle(m); // success: acknowledge, message is gone } catch (RuntimeException e) { System.out.println(" failed: " + e.getMessage()); if (attempt >= MAX_ATTEMPTS) { deadLetters.add(m); System.out.println(" moved to dead-letter queue"); } else { queue.add(m); // no acknowledgement: redelivered later } } } System.out.println("dead letters: " + deadLetters.stream().map(Message::id).toList()); }}deliver m1 (attempt 1) m1 processed: send welcome email to u1deliver m2 (attempt 1) failed: timeout calling email providerdeliver m3 (attempt 1) failed: cannot parsedeliver m1 (attempt 2) m1 already processed, skipping side effectdeliver m2 (attempt 2) m2 processed: send welcome email to u2deliver m3 (attempt 2) failed: cannot parsedeliver m3 (attempt 3) failed: cannot parse moved to dead-letter queuedead letters: [m3]One simplification matters: the program keeps processed ids in memory. A real consumer must store them durably, and ideally in the same transaction as the side effect, or a crash between "send email" and "record id" still produces a duplicate. Article 20 shows how.
Ordering
#Many queues do not preserve order: with competing consumers, message 2 may finish before message 1. Often that is fine (thumbnails for different photos are independent). Sometimes it is not: "account opened" must be processed before "account closed".
Brokers offer ordering within a group or partition, never across a whole high-throughput system:
- Kafka: order is guaranteed within a partition. Events with the same key (for example
account_id) go to the same partition, so all events for one account are read in order (Introduction to Kafka). - SQS FIFO queues: messages with the same message group id are processed in strict sequence; the next message in a group is not available until the in-flight one is deleted or its visibility timeout expires (Amazon SQS visibility timeout).
The trade-off is direct: order within a key means only one consumer works on that key at a time. Choose the narrowest key that needs ordering (per account, per conversation, per order), not a global order, or you lose parallelism. Also note that retries can reorder messages: if message 1 fails and is retried while message 2 succeeds, 2 is processed first unless the broker blocks the group.
When strict ordering is too expensive, design consumers to tolerate reordering: include a version or timestamp in each message and ignore updates older than what you have already applied.
Backlogs and backpressure
#A queue hides a mismatch between arrival rate and processing rate, but only for a while. If producers send 2,000 messages per second and consumers process 1,500, the backlog grows by 500 per second: 1.8 million messages after an hour. Messages wait longer and longer, and the work may no longer be useful when it finally happens (a "your order is out for delivery" notification three hours late).
Watch two metrics above all:
- Queue depth (or consumer lag in streams): how many messages are waiting.
- Age of the oldest message: how long the oldest one has waited. This one tells you the user impact.
Responses:
- Scale consumers on lag or age, as far as downstream systems can take it.
- Protect downstream systems: more workers can overload the database they write to. Limit concurrency.
- Prioritise: separate queues for urgent and bulk work (for example, password-reset emails versus newsletters).
- Drop or expire stale work when it is no longer useful, instead of processing it late.
- Push back on producers (backpressure): if the queue is full or too old, reject or slow new work at the edge rather than accept work you cannot do.
The Amazon Builders' Library has an article on exactly this problem, describing how backlogs can grow until they are effectively insurmountable and discussing techniques such as separating workloads and dropping messages that are too old to matter (Avoiding insurmountable queue backlogs).
Designing workers and messages
#Workers
#- Idempotent. Every handler must be safe to run twice.
- Stateless. Any worker can process any message (article 08); state lives in shared stores.
- Bounded. Each message is a small, bounded unit of work. Split "process all 2 million users" into one message per user or per batch.
- Timeouts on every call, and heartbeats or timeout extension for long tasks.
- Graceful shutdown: stop receiving, finish or release in-flight messages, then exit, so deployments do not cause mass redelivery.
- Concurrency limits that protect the database, provider APIs and rate limits.
Messages
#{ "message_id": "evt_7b2f9c", "type": "photo.uploaded", "version": 2, "occurred_at": "2026-10-04T18:22:07Z", "photo_id": 9001, "owner_id": 42, "object_key": "photos/u42/p9001/original.jpg"}- A unique id for deduplication.
- A type and version, so the format can evolve without breaking old consumers.
- References, not large payloads. Put the photo in object storage and send its key. (Passing a reference to a large stored payload is sometimes called the claim-check pattern.) Brokers have message size limits, and large messages slow everything.
- No secrets, and as little personal data as necessary: messages are copied, retained and logged.
Worked example: Pixly's "photo uploaded" event
#Pixly needs four things to happen after an upload: thumbnails, moderation, feed updates and notifications. Two reasonable designs:
Option A: pub/sub topic with a queue per consumer.
API server --> topic "photo.uploaded" --+--> queue "thumbnails" --> thumbnail workers +--> queue "moderation" --> moderation workers +--> queue "feed-fanout" --> feed workers +--> queue "notifications" --> notification workers (each queue has its own DLQ)Each consumer scales and fails independently. A broken notification worker fills its own DLQ without affecting thumbnails. Simple to operate with managed services.
Option B: an event stream, topic photo-events partitioned by photo_id, with one consumer group per service.
Adds replay: if Pixly later builds a search service, it can read the topic from the beginning (within retention) to index existing photos. Ordering per photo is preserved (useful if "uploaded" and "deleted" events for the same photo must be applied in order). Costs more operational effort.
Choice: start with Option A, unless the requirements include replay or many future consumers of the full history. That is a defensible, requirement-driven answer, and you can say exactly what would make you switch.
Failure modes
#Failure | Effect | Mitigation |
|---|---|---|
Consumer crashes mid-processing | Message redelivered after timeout; possible duplicate side effect | Idempotent consumers; acknowledge after success |
Visibility timeout shorter than processing time | The same message processed in parallel by two workers | Longer timeout or heartbeats; idempotency |
Poison message | Retries waste capacity; can block ordered groups | Max attempts, DLQ, alerting |
Broker unavailable | Producers cannot enqueue; requests may fail | Managed, replicated brokers; producer retries; transactional outbox (article 21) |
Producer crashes between database write and enqueue | Data saved, event never sent | Transactional outbox (article 21) |
Backlog grows | Work delayed, possibly useless | Autoscale on lag and age, priorities, expiry, backpressure |
Out-of-order processing | Older update overwrites newer state | Partition or group by key; version checks in consumers |
Message loss | Work silently never done | At-least-once with acknowledgements; replicated brokers; never acknowledge before processing important work |
Security and observability
#- Access control per queue or topic: which services may publish and which may consume.
- Encryption in transit and at rest; minimise personal data in messages, because retention keeps it around.
- Metrics: queue depth or lag, age of the oldest message, processing rate, error rate, DLQ size, end-to-end latency from event to effect.
- Tracing: pass a trace id in message metadata so a user action can be followed from the API through each consumer (article 25).
Trade-offs
#Decision | You gain | You pay |
|---|---|---|
Queue instead of a direct call | Fast responses, spike absorption, retries, isolation | Delay, duplicates, more components, harder debugging |
Event stream instead of a queue | Replay, many consumer groups, ordered high throughput | Operational complexity, partition planning |
Strict ordering | Correct sequence per key | Less parallelism; head-of-line blocking on failures |
At-least-once | No lost work | Duplicates; idempotency required |
Long retention | Replay and audit | Storage cost, privacy obligations |
Common mistakes
#- Using a queue where the user needs the answer now.
- Acknowledging before processing, then losing work on crashes.
- Assuming "exactly-once" and writing non-idempotent consumers.
- No dead-letter queue, or one that nobody monitors.
- Asking for global ordering when per-key ordering is enough.
- Putting large payloads in messages instead of references.
- Scaling consumers without considering the database they all write to.
- Choosing Kafka for a handful of background jobs.
In an interview
#- Introduce a queue with its reason: "thumbnails are slow and can fail; a queue makes uploads fast and retries failures".
- Say "at-least-once, so consumers are idempotent", and how (unique constraint, processed-ids table, idempotent writes).
- Mention a DLQ and monitoring of lag or oldest-message age.
- If ordering matters, name the partition or group key.
- Justify pub/sub or a stream only when there are several consumers or a need for replay.
Exercises
#Exercise 1
Which model?
#Choose a work queue, pub/sub or an event stream, and justify it:
- Resize uploaded profile pictures.
- When an order is placed, update inventory, send a confirmation email and record analytics.
- Keep a replayable history of every price change so new services can rebuild their view of prices.
- Send password-reset emails.
Exercise 2: The double charge
#A worker charges a customer's card when it receives a "charge order" message. Charging takes up to 40 seconds when the payment provider is slow. The queue's visibility timeout is 30 seconds. Some customers are charged twice. Explain the sequence of events, and give two independent fixes.
Exercise 3: Ordering for bank events
#A system emits account events: opened, deposit, withdrawal, closed. Consumers update balances. Should the stream be partitioned by event type, by account id or by a random key? Explain what goes wrong with the other two choices.
Exercise 4: Backlog arithmetic
#During a sale, messages arrive at 2,000 per second for one hour. Your workers process 1,500 per second.
- How large is the backlog at the end of the hour?
- After the sale, arrivals drop to 500 per second and you scale workers to 3,000 per second. How long until the backlog is cleared?
- Each message is a "your order is confirmed" email. What would you do differently next time?
Show solutionHide solution
- Work queue. Each resize should happen once; one consumer type.
- Pub/sub (or a stream). Three independent consumers need the same event; each should fail and scale independently.
- Event stream with long retention (or compaction by key, keeping the latest price per product). Replay for new services is the defining requirement.
- Work queue, ideally a separate high-priority queue so resets are never stuck behind bulk email.
Exercise 2
Show solutionHide solution
Sequence: worker A receives the message and calls the payment provider, which is slow. At 30 seconds the visibility timeout expires and the message becomes visible again. Worker B receives it and also calls the provider. Worker A's call succeeds at 40 seconds and B's also succeeds: two charges. A's delete may even fail because B now holds the message.
Fixes (independent; use both):
- Idempotency at the provider: send an idempotency key derived from the order id (for example
charge-order-77) with the charge request. The provider returns the original result for the second call instead of charging again (article 04). - Idempotency in your system: record "order 77 charged" with a unique constraint before or atomically with acting, so a second worker sees it and stops; and keep the message invisible long enough, by setting a visibility timeout comfortably above the worst-case processing time or extending it with a heartbeat while the call is in progress.
Raising the timeout alone is not enough: crashes and network failures can still cause redelivery, so the idempotency key is the real protection.
Exercise 3
Show solutionHide solution
Partition by account id. All events for one account go to one partition and are processed in order, so deposit is never applied after closed, and balances are computed in the order events happened. Different accounts are spread across partitions, so processing still scales.
- By event type: all deposits go to one partition and all withdrawals to another. Consumers read them at different speeds, so a withdrawal can be processed before the deposit that funded it. Ordering per account is lost, and one partition holds a huge share of the traffic.
- Random key: events for one account scatter across partitions with no ordering between them. Every consumer must handle reordering itself, which is complex and error-prone for balances.
Even with per-account partitioning, consumers should be idempotent (each event has an id) because retries and redelivery can still repeat events.
Exercise 4
Show solutionHide solution
- The backlog grows by 2,000 - 1,500 = 500 messages per second for 3,600 seconds: 1.8 million messages.
- The net drain rate is 3,000 - 500 = 2,500 per second. 1,800,000 / 2,500 = 720 seconds: 12 minutes.
- Confirmation emails were up to an hour late at the worst point, which hurts users. Next time: scale workers before the sale (it was a known event), on the age of the oldest message rather than CPU; check that the email provider's rate limits allow the higher throughput; put confirmations in a high-priority queue separate from bulk or marketing email; and, if the provider is the bottleneck, consider whether the in-app confirmation screen (which is immediate) makes a late email acceptable.
Where to go next
#- Next: Relational Databases and Data Modeling for System Design starts Module 5, the data layer every one of these consumers writes to.
- Going deeper: idempotency and delivery guarantees in article 20, the outbox pattern in article 21, and event-driven architecture in article 27. The notification system (article 33) is a full design built on these ideas.
Sources & further reading
- Technology
- Software Engineering
- #asynchronous processing
- #backend
- #distributed systems
- #Kafka
- #message queues
- #roadmap
- #scalability
- #system design
- #system design interview
Connected lessons
Linked from 3 guides
Other pages in the library that point here.
Keep reading
More in Technology →Guide · Technology
Start Here: Your Roadmap from Zero to Job-Ready DSA
Guide · Technology
Web Development Roadmap: How to Become a Web Developer From Scratch
Guide · Career
A LinkedIn Profile That Gets Recruiter Messages

