57 questions

System design interview questions and answers

Middle questions

  1. 1. You are given 45 minutes and the task "design a notification service". How do you split the time and where do you start? Which mistakes most often sink candidates in system design?

    Answer

    The first 5 minutes go to requirements. Functional: which channels (push, email, in-app), whether history is needed, whether there is scheduling. Non-functional: how many users, acceptable delivery delay, whether a notification can be lost, whether order matters. I also say explicitly what is out of scope: for example, we do not design email templates. Then 5 minutes for estimation: DAU, notifications per user per day, average and peak QPS, storage per year. The numbers decide the architecture: 50 notifications per second fit in one Postgres with BullMQ, while 50 thousand already need a partitioned broker. Then 10 minutes for the API and a high-level design: who generates the event, the queue, a worker per channel, the status store, the APNs and FCM providers.

    Follow-up question: The interviewer says "traffic grew 10 times". What breaks first in your design and how did you determine that?

  2. 2. Estimate on a napkin: 10 million DAU, each makes 50 requests per day, 10% of requests are writes of 1 KB, the average response is 5 KB. Compute average and peak QPS, data volume per year and outgoing traffic. How many Node processes and servers do you need?

    Answer

    Requests: 10 million × 50 = 500 million per day, so on average about 5,800 rps, and with a threefold peak about 17 thousand. Writes at 10%: 580 on average and 1,700 at peak. Volume: 50 million records of 1 KB is 50 GB per day, about 18 TB per year, and with indexes and two replicas over 50 TB. One database without partitioning and archiving of old data will not last a year, and backups of that size must be planned separately. Outgoing traffic: 17 thousand rps × 5 KB is about 85 MB/s, so hundreds of Mbit/s at peak. For process count I take the measured rps of one Node process (say 1,000 rps) at 60% utilization: about 30 processes, which is 2 or 3 servers of 16 cores with N+1 headroom.

    Follow-up question: How does the estimate change if 80% of reads can be served from a cache with a 90% hit rate?

  3. 3. An endpoint assembles a user profile: 40 sequential GETs to Redis and 15 sequential queries to Postgres. Locally the p50 is 8 ms. In production Redis and Postgres moved to a neighboring data center, and the p50 became 400 ms, even though CPU is low. Explain it using the numbers every engineer should know, and fix it.

    Answer

    The classic table of orders of magnitude: a memory access is about 100 ns, a round trip within one data center is about 0.5 ms, between data centers in one region a few milliseconds, between continents about 150 ms. Reading 1 MB from memory takes hundreds of microseconds, from disk tens of milliseconds. Here there are 55 sequential calls. Locally the RTT is tens of microseconds, hence 8 ms. Between data centers the RTT is, say, 7 ms: 55 × 7 ≈ 385 ms, plus the work itself. CPU is low because the process spends almost all its time waiting on the network. Vertical scaling will do nothing here.

    Follow-up question: What changes if you also add TLS between services without connection reuse?

  4. 4. A WebSocket chat behind Nginx. Complaints: connections drop exactly every minute when the chat is quiet. And after every deploy one of three Node processes holds 80% of all connections. Explain both effects and fix the config.

    Answer

    The first effect. proxy_read_timeout defaults to 60 seconds: if nothing arrives from the backend in that time, Nginx closes the connection. A quiet chat looks exactly like that. I fix it from both sides: the server sends a ping every 25 or 30 seconds (in ws via ws.ping, Socket.IO does this itself through pingInterval), and I set proxy_read_timeout and proxy_send_timeout to, for example, 1h. The ping is also needed on its own: it finds dead connections when a client vanished without a FIN. The config also lacks proxy_http_version 1.1; without it Nginx talks to the backend over HTTP/1.0 and the upgrade does not work. During a rolling deploy the processes restart one at a time. The clients of the first process reconnect to the two live ones, then the clients of the second to the live ones, and so on.

    Follow-up question: What changes if Cloudflare or another CDN sits in front of Nginx? What are its WebSocket limits?

  5. 5. The dashboard shows an average response time of 45 ms, yet users complain about slowness. Why does the average lie here? What do p50, p95 and p99 mean? Why is a page that makes 50 calls to backends slow more often than the p99 of each backend suggests?

    Answer

    Latency is not normally distributed. Most requests are fast, but there is a long tail: a GC pause, a cold cache, waiting for a connection from the pool, a database lock. An average of 45 ms can mean that 98% of requests take 20 ms and 2% take 1.3 seconds. A user who clicks 50 times per session will almost certainly see those 1.3 seconds. pN means that N% of requests are faster than this value. p50 is the median, the typical experience. p95 and p99 describe the tail: p99 is every hundredth request. For an active user every hundredth request happens several times per session, so SLOs are written on p95 or p99, not on the average. If a page makes 50 independent calls and waits for all of them, it is slow when at least one of them hit the tail.

    Follow-up question: How do you reduce tail latency if the cause is a single slow backend out of 50?

  6. 6. An API handles 3000 rps with an average latency of 40 ms, and each request makes two Postgres queries of 3 ms. How many requests are in flight at once? What total connection pool do you need? What happens when database queries slow down to 100 ms each?

    Answer

    Little's law: the average number of requests in the system equals the arrival rate times the average time spent, L = λ × W. Here 3000 × 0.04 = 120 requests in flight at once. The law holds for any stable system, regardless of distribution. The same law for the database: each request holds a connection for 2 × 3 = 6 ms, so on average 3000 × 0.006 = 18 connections are busy. I size the pool with headroom for unevenness, for example 40 in total. With 12 processes that is 3 or 4 per process, through PgBouncer in transaction mode. More connections will not make it faster: in Postgres each one is a process, and beyond a few dozen active ones on a 16-core server they compete with each other.

    Follow-up question: How would you use the same law to estimate how many VUs k6 needs for 3000 rps?

  7. 7. A sale: traffic is three times the API capacity. A per client rate limiter is in place and no client exceeds its quota, yet latency is 20 seconds for everyone and there are fewer successful responses per second than on a normal day. Why, and what do you add?

    Answer

    Rate limiting protects against one client exceeding its quota, but here the overload is aggregate and the limiter cannot see it. The service accepts everything, queues grow, each request waits longer than the client timeout, so the server spends resources on responses nobody is waiting for, and clients retry on top: goodput falls. You need load shedding: cap concurrent requests or queue length and reject the excess immediately with a cheap 503 and Retry-After, preferably by priority (payments matter more than recommendations) and taking the request deadline into account. 429 means 'you, this client, exceeded your quota', 503 means 'the service is overloaded', and clients should react to both with backoff, not an instant retry.

    Follow-up question: Which signal should the service use to decide it is time to shed: CPU, latency, or something else?

  8. 8. Case: a URL shortener. 100 million new links per month, reads are 100 times more than writes, links are stored for 5 years. Which requirements do you clarify first? Compute write QPS and redirect QPS, average and peak, storage volume, and how many base62 characters the code needs.

    Answer

    Functional: create a short link from a long one, redirect, optionally a custom alias and an expiry, click statistics, deletion by the owner. Non-functional: redirect is fast (p99 under 20 ms in the data center) and almost always available, codes must not be predictable if links are private. Redirect matters more than analytics: if statistics lag by a minute, nobody notices, but if the redirect goes down, all emails and QR codes already sent break. Writes: 100,000,000 / 2,592,000 s ≈ 40 per second, peak ×5 ≈ 200. Reads are 100 times more: ≈ 4000 rps on average, 15,000 or 20,000 at peak. For Node that is several processes, not a cluster, but 20,000 point SELECTs per second without a cache will already be felt by the database. Over 5 years 100 million × 60 = 6 billion records.

    Follow-up question: How do you generate a unique 7-character code without collisions when 10 API instances write at the same time?

  9. 9. Case: a social network home feed. 20 million DAU, each opens the feed 10 times per day and publishes 1 post per 5 days, with 200 follows on average. Clarify the requirements and compute read and write QPS, how many inserts into feeds fan-out on write produces, and how much memory the feeds will take in Redis.

    Answer

    Requirements: a chronological or ranked feed, acceptable delay before a post appears (a few seconds is fine), deletion, infinite scroll, and reads matter more than instant appearance. Reads: 200 million per day is about 2300 rps, 8000 or 9000 at peak. Posts: 4 million per day, about 46 per second. Fan-out on write spreads each post into 200 feeds: 800 million inserts per day, about 9300 per second on average and 30,000 at peak, and an account with 10 million followers produces 10 million inserts with a single post. A feed holds only post_id and score, trimmed to 500 items, which is about 30 KB per user. For all 20 million that is 600 GB, so feeds are kept only for active users, about 250 GB across several Redis shards. The main problem here is not reads but the multiplication of writes and the tail of large accounts.

    Follow-up question: How does the design change if the feed is ranked by a model rather than by time?

  10. 10. After creating a listing, the user is redirected to the My listings page, and the new listing is not there: the Elasticsearch projection lags by 1 to 3 seconds. Users press again and create duplicates. What are the fix options, and which would you choose?

    Answer

    The problem is that the UI pretends to be a synchronous system on top of an asynchronous one. There are several options, usually combined. The command returns the created object, and the client adds it to the list locally. It is the cheapest and sufficient in most cases. The command returns a position: an outbox record id or a version. The projector stores the last applied position. The list request passes min_position, and the API waits up to 2 seconds for the projection to reach it, or returns the response with a stale flag.

    Follow-up question: The projector crashed on one bad event and has been stuck for an hour. How do you keep it from blocking all the others?

  11. 11. The tech lead wants to move the whole new orders service to event sourcing because there will be an audit and you can rewind time. What do you say? What real problems will the team get in the first six months?

    Answer

    First I ask which problem we are solving. Audit and change history can be had much cheaper: an audit_log table, a change trigger, or domain events in an outbox next to ordinary state. Rewinding time is needed rarely, and when it is, the change history of a single order is enough. What the team gets in six months. Every list and search needs a projection, because you cannot run SELECT * FROM orders WHERE status = 'paid' on a log. That means projectors, their lag, read-your-writes in the UI, lag monitoring. The event schema changes with every feature, so upcasters and versioning rules are needed from the first month.

    Follow-up question: What structure of audit_log would you build so that an order's state can be reconstructed for a given date?

  12. 12. A Node api service calls recommendations over gRPC through a regular Kubernetes Service. Recommendations was scaled from 2 to 8 pods, but the load stayed on the two old ones, and the new ones are almost idle. Why? How do you fix it?

    Answer

    kube-proxy balances connections, not requests: the pod is chosen once when the TCP connection is established. gRPC runs over HTTP/2 and multiplexes all calls into one long connection. The api client connected when there were 2 pods, and since then all its calls go there. The new 6 pods stay empty until the connection breaks. With HTTP/1.1 and keep-alive the same happens, but there are more connections, so it is much less noticeable. Option one: client-side balancing. A headless Service with clusterIP: None, DNS returns the IPs of all pods. The client keeps a connection to each pod and distributes calls.

    Follow-up question: In Nest the gRPC client is created through ClientsModule. Where do you pass the service config with round_robin?

  13. 13. Services already have e2e tests on staging that run all services together for 40 minutes and are often flaky. What does consumer-driven contract testing give and how does Pact work? What does it not verify?

    Answer

    E2E tests on a shared staging verify everything at once, so they are slow, flaky and localize errors poorly: the checkout test fails, and anyone of 8 services could have broken it. Contract testing verifies each consumer and provider pair separately. The consumer, for example orders, describes the interaction in its test: the state user 42 exists, the request GET /users/42, the expected response with id and email fields, where values are set with a like matcher, meaning the type is checked, not the concrete value. Pact starts a mock server, the real HTTP client of orders talks to it, and if the client makes exactly such requests, a pact file in JSON is generated. It is published to a Pact Broker with a version, usually the git sha. The provider users, in its own CI, starts itself, takes from the broker the contracts where it is the provider, prepares data through provider states (for example the handler for user 42 exists inserts a row into the test database), and replays the requests, comparing responses with expectations.

    Follow-up question: How do you test an event contract in Kafka, where there is no request and response?

  14. 14. During a migration the team did this: when a product changes, the monolith writes to its own database and immediately makes an HTTP POST to the new catalog service. A month later 0.3% of products in the new service have different prices. Why does this happen when there are almost no errors? How do you fix both the discrepancy and the approach itself?

    Answer

    This is a dual write: two writes to two systems without a shared transaction, so any failure between them causes a discrepancy. The monolith committed the price, then the POST failed on a timeout: the catalog has the old price. Or the POST went through and the monolith transaction rolled back: the catalog has a price that never existed. There are races too: two admins changed the price almost at once, in the monolith database B committed last, but the requests arrived in the order B, A. A timeout also does not mean a failure, and a retry without a version can overwrite newer data with older. The approach itself must be fixed: sync from a log, that is CDC on the WAL or an outbox in the same transaction, with a product_id key and a conditional write by version. Existing discrepancies are healed by a reconciliation job that compares both databases by product_id and rewrites the diverging ones from the source of truth.

    Follow-up question: How do you do the initial fill of the new database so as not to lose changes that happen during copying?

  15. 15. A consumer sends a welcome email and then commits the offset. After a pod crash some users got two emails. Another team commits the offset before processing and loses events now and then. Which delivery guarantee does each option give, and what do you choose for emails and what for crediting bonuses?

    Answer

    Committing after processing gives at least once: if the process dies between sending the email and committing, the message comes again after the restart, hence duplicates. Committing before processing gives at most once: the offset has moved but the work is not done, so the event is lost. The guarantee is set not by the broker but by the order of 'did the work' and 'acknowledged', plus whether the side effect is idempotent. For emails at least once with deduplication by event id is enough, since a duplicate is annoying but not critical. For bonuses you need at least once plus an idempotent write: a unique key on the event id in the same transaction as the credit, so a retry changes nothing and the result is effectively exactly once. At most once fits where a loss is cheaper than a duplicate: metrics, telemetry, an 'online' status.

    Follow-up question: How do you deduplicate when the side effect is a call to an external API without an idempotency key?

  16. 16. Redis restarted for 20 seconds. 1000 workers retry the connection every second, and when Redis comes back it falls over again under the wave of connections, several times in a row. What do you change in the retry logic? Write down the delay formula.

    Answer

    A fixed interval synchronises the clients: they all lost the connection at once and retry at once, so the server gets waves of 1000 connections exactly when it is weakest. Exponential backoff spreads attempts over time, and jitter desynchronises clients from each other; without it the waves remain, just rarer. The full jitter formula: sleep = random(0, min(cap, base * 2 ** attempt)), where cap limits the longest pause, say 30 seconds, so a client does not wait an hour after a long outage. On top of that, limit the number of attempts or set an overall deadline, and retry only transient errors.

    Follow-up question: How do you avoid the same wave when hundreds of pods start at once after a deploy?

  17. 17. An integration with a payment partner retries any error 5 times with a 1 second pause: 400 validation, 401, 429, a timeout and 503. The call is POST /charges without an idempotency key and with a 30 second timeout. When the partner slowed down, double charges appeared and our service went down. What is wrong and how do you rewrite it?

    Answer

    Only transient errors should be retried: timeouts, reset connections, 502, 503, 504, and 429 honouring Retry-After; 400, 401, 403, 404 and 422 are permanent, and a retry only adds load. A timeout on a non idempotent POST is ambiguous: the request may have gone through, so a retry without an idempotency key causes a double charge, and you need an Idempotency-Key that is the same for every attempt. A fixed pause without jitter synchronises retries, and 5 attempts of 30 seconds each hold a request and a connection for up to 2.5 minutes, so the pool and memory run out. You need a short timeout, an overall deadline, backoff with jitter and a circuit breaker or retry budget, so a degraded partner does not get multiplied traffic.

    Follow-up question: How do you implement a retry budget in a Node service and which metric do you monitor?

  18. 18. An audit found 12 thousand messages from the last quarter in the refunds service DLQ that nobody knew about, and the customers never got their money. How should the process around a DLQ work so it does not become a graveyard?

    Answer

    A DLQ exists so that a message that failed after N attempts (maxReceiveCount in an SQS redrive policy, a dead letter exchange in RabbitMQ, the failed list after attempts in BullMQ) does not block the queue. But a DLQ without a process just hides failures. You need: an alert on a non zero DLQ depth and on the age of the oldest message, not only on errors in the logs; an owner who triages it within an SLA; the reason, stack, attempt count, time and code version stored with the message; permanent errors (validation, a missing record) separated from transient ones, because permanent ones should not go round in circles. And a tested, rate limited redrive with an idempotent consumer, so reprocessing is safe.

    Follow-up question: How does consumer code tell a permanent error from a transient one, and what does it do with each?

  19. 19. A RabbitMQ consumer keeps restarting, CPU is 100%, the queue is not moving, though most messages are fine. In the code, on any error, channel.nack(msg, false, true) is called. What is this and how do you treat it? And how does a similar situation look in Kafka?

    Answer

    This is a poison message: one message, for example with broken JSON, fails every time. requeue=true puts it back at the head of the queue, so the consumer instantly receives it again. The RabbitMQ documentation warns about such a requeue/redelivery loop: it eats CPU and network. If the whole process crashes, the connection drops and the message is returned too. The handler must not crash: schema validation before processing, try/catch around it. For permanent errors nack(msg, false, false), so it goes to the DLX. A quorum queue has a delivery-limit: since RabbitMQ 4.0 it defaults to 20, after which the message is dropped or goes to the DLX, so the DLX should be configured. Classic queues do not have this protection.

    Follow-up question: How does a Kafka consumer learn that this is already the N-th attempt if the broker does not count it?

  20. 20. Order events created, paid, shipped go through SQS standard. With one consumer everything worked; after scaling to four, some orders get paid processed before created and the handler crashes. Why, and how do you get ordering within one order without global ordering?

    Answer

    SQS standard does not guarantee ordering at all, and several consumers process neighbouring messages in parallel, so with one consumer the order was a coincidence. Global ordering is not needed and is expensive; you need ordering per key: SQS FIFO with MessageGroupId = order id (strict order inside a group, groups processed in parallel), Kafka with the message key = order id (one partition, one consumer in the group), in RabbitMQ a consistent hash exchange or a queue per shard. Even then the handler should tolerate reordering: store a version or sequence number, drop stale events and defer those whose previous state is not there yet, because retries and the DLQ break ordering anyway.

    Follow-up question: What happens to ordering within a group when one message goes to the DLQ?

  21. 21. An SQS consumer generates PDF reports and emails them. Some customers get the same report two or three times. Generation takes up to 90 seconds, and the visibility timeout is left at the default. Explain what is happening and fix it.

    Answer

    A received message is not deleted, it only becomes invisible for the visibility timeout, 30 seconds by default. If processing takes longer and DeleteMessage is not called in time, the message reappears in the queue, another consumer takes it, and the report is generated again. The fix: a visibility timeout with headroom over the real processing time (for Lambda AWS recommends at least six times the function timeout), and for long tasks extend it with ChangeMessageVisibility as a heartbeat. And still make processing idempotent, for example by recording the id of the sent report, because SQS standard is at least once and duplicates can happen even without this bug.

    Follow-up question: What changes if the consumer is a Lambda with batch size 10 and one message in the batch fails?

  22. 22. A topic has 6 partitions and lag is growing. The team scaled the consumer deployment from 6 to 12 pods, but lag did not drop and half the pods do nothing. Why, what will actually help, and what happens when another service starts reading the same topic?

    Answer

    Within one consumer group each partition is assigned to exactly one consumer, so parallelism is capped by the partition count: six partitions mean at most six active consumers, and the rest sit idle waiting for a rebalance. What can help: more partitions (but that changes the key to partition mapping and the ordering for new messages), faster processing per message (batches, fewer synchronous calls), or parallel processing inside the consumer by key, keeping per key order and committing offsets only for processed messages. Another service reads the topic with its own consumer group and its own offsets and gets every message regardless of the first group, because Kafka does not delete what was read.

    Follow-up question: How do you parallelise processing inside one consumer without committing the offset of a message that is not processed yet?

  23. 23. After a consumer restart some events were not processed even though lag showed 0. Another time events were processed twice. The settings are default, and in the code a separate pool processes messages asynchronously. Explain both cases. How do you fix it?

    Answer

    enable.auto.commit defaults to true: the offset is committed periodically in the background. It does not know whether your code has finished processing. Loss: pool.submit returns immediately, the handler is considered done, and the offset is committed. The process crashed with jobs in the pool, and after the restart reading continues past them. Lag showed 0 because it is about the committed offset, not finished work. Duplicates: processing completed but the commit had not happened yet. The process crashed, and after the restart the consumer read the same records again.

    Follow-up question: If auto.offset.reset=latest, what happens to a new consumer group on first start?

  24. 24. A consumer processes a batch of records for 10 minutes. Rebalances happen constantly, the same batch is processed again and again, and lag grows. The heartbeat is fine meanwhile. What happened and how do you fix it?

    Answer

    There are two separate mechanisms. The heartbeat goes in a background thread and proves the process is alive (session.timeout.ms, 45 s by default). max.poll.interval.ms, 300000 ms by default, is the maximum pause between poll() calls. It proves that the consumer is making progress. Processing of 10 minutes exceeds 5 minutes, so the consumer is considered stuck and leaves the group. The partitions go to another one, which starts from the last committed offset, that is, from the same batch. The first one finishes and tries to commit, but the partitions are no longer its own. This is an endless loop, and lag grows.

    Follow-up question: What does the new group.protocol=consumer change compared with classic?

  25. 25. The order.created event in RabbitMQ must reach both billing and the email service. A developer publishes to the default exchange with routing key billing, and nobody gets the emails. And when the billing queue was deleted for maintenance, messages silently disappeared. Explain both problems and fix the topology.

    Answer

    The default exchange is a direct exchange where the routing key equals the queue name, so the message lands in exactly the billing queue: the producer effectively knows its consumer, and a second queue cannot be added this way. The right way: the producer publishes to a named exchange, for example topic orders with key order.created, and each service has its own queue bound to the key it needs, so a new consumer is added without touching the producer (fanout if no filtering is needed). The second problem: if no queue matches the route, a message without the mandatory flag is simply dropped. Protection: durable queues that are not deleted for maintenance, an alternate exchange for unroutable messages or mandatory with return handling, and publisher confirms.

    Follow-up question: The email service was down for a week. What must the topology have so it gets the missed events once it is back?

  26. 26. A RabbitMQ consumer with automatic ack. Messages are lost during a deploy. They switched to manual ack, and now one consumer takes all 200 thousand messages and crashes with OOM while the others sit idle. One more incident: messages disappeared when the broker restarted. Explain all three problems.

    Answer

    The broker considers a message processed right after sending it. Everything the consumer holds in memory is lost on a crash. The solution: a manual ack after processing, which is at-least-once. An unacked message is returned to the queue when the connection drops. The value 0 means no limit, so the broker pushes everything it has into the first consumer's socket. It buffers everything in memory. The solution: basic.qos, prefetch 10 to 100 depending on task duration.

    Follow-up question: How do you choose the prefetch value if tasks take 10 ms at times and 10 s at others?

  27. 27. You need retries with delays of 1, 5 and 30 minutes in RabbitMQ, and a parking lot after that. The broker has no simple "retry in 5 minutes" option in classic queues. How do you build this? What are the pitfalls?

    Answer

    The scheme is built on TTL and DLX. The main queue orders has a dead-letter-exchange to a retry exchange. There are three queues without consumers: orders.retry.1m, 5m, 30m, each with its own x-message-ttl and a dead-letter-exchange back to the main exchange. When processing fails, the consumer does a nack without requeue or publishes to the needed retry queue. The message sits for the TTL and then returns to the main queue. The attempt counter: the x-death header, filled in by the broker on dead-lettering, or a custom x-attempt header. After the third attempt the message goes to orders.parking, with no TTL, an alert and manual triage.

    Follow-up question: What is more reliable for publishing to the retry queue: nack, or publish with a confirm and then ack?

  28. 28. Kafka broker disks are 90 % full. You cut retention.ms on a 2 TB topic from 7 days to one hour, but disk usage barely moves for hours. Why, and what do you do?

    Answer

    Retention deletes data in whole segments, not individual messages, and only closed segments: the active segment being written is not deleted until it rolls by segment.bytes (1 GB by default) or segment.ms (7 days by default). A segment is deleted when its newest record is older than the retention, and the check runs periodically (log.retention.check.interval.ms, 5 minutes). On partitions with slow writes the active segment can hold almost a week of data. What to do: temporarily lower segment.ms or segment.bytes so segments roll, wait for the check, and in future also cap the topic by size (retention.bytes) and have an alert on disk usage.

    Follow-up question: How do you delete one user's data from a compacted topic for a GDPR request, and when do consumers stop seeing it?

  29. 29. You need a broker for two flows: thumbnail generation jobs, and order events read by five services, where a new analytics service wants to reread the last 30 days. For each flow, RabbitMQ or SQS, or Kafka, and why?

    Answer

    Thumbnails are a classic job queue: each job is done by one worker, you need per message acknowledgements, retrying a single job, a DLQ and scaling by just adding workers, so RabbitMQ, SQS or BullMQ. After an ack the message is gone from the queue and cannot be read again. Order events fit Kafka better: it is a log, messages stay after being read for the retention period, each service reads with its own consumer group and offset, and a new service can start from an offset a month back and replay. The cost of a log: parallelism is capped by partitions, retrying one message without blocking the partition is something you build yourself, and Kafka is harder to operate.

    Follow-up question: What do you do when an order event in Kafka fails because of a bug and you cannot block the partition?

  30. 30. After 20 minutes of consumer downtime there are 500 thousand jobs in the queue. New ones arrive at 800 per second, one worker handles 50 per second, and there are 20 workers now. How long will it take to drain the queue? How many workers do you need to drain it in 10 minutes? What else should you check before adding workers?

    Answer

    Arithmetic. 20 workers at 50 each is 1000 per second. New ones arrive at 800, so the queue shrinks by only 200 per second. 500000 / 200 = 2500 s, about 42 minutes. During all that time new jobs also wait, because they stand at the end of the queue. To drain in 10 minutes (600 s), the net drain must be 500000 / 600, about 833 per second. Together with the inflow of 800 that is 1633 per second, which is 33 workers. Little's law: L = λ W. The number in the system equals the arrival rate times the average time spent.

    Follow-up question: How do you set up autoscaling of workers by queue depth so that it does not oscillate?

  31. 31. A Redis Streams consumer crashed mid processing and restarted. It reads with XREADGROUP GROUP g c1 STREAMS orders >, and the 300 messages it was processing at the moment of the crash stay unprocessed. Why, and how do you pick them up?

    Answer

    XREADGROUP with ID > returns only new messages that were never delivered to anyone in the group. Delivered but not acknowledged with XACK ones stay in the group's pending entries list (PEL), assigned to a specific consumer, and do not come back on their own. To pick them up: after a restart the same consumer first reads from ID 0 and gets its own pending messages, and messages of dead consumers are taken over with XPENDING and XCLAIM or XAUTOCLAIM with a minimum idle time (Redis 6.2+). The delivery counter in the PEL lets you move a message to a separate stream as a DLQ after N attempts. Processing must be idempotent, because some messages will be processed twice.

    Follow-up question: How do you stop the stream from growing forever without losing unacknowledged messages?

Senior questions

  1. 1. Right now the peak is 2000 rps on three servers with 16 cores each, CPU 45%. Marketing promises a peak four times larger in two months. The vendor ships new servers in three weeks, and there is no cloud. How do you plan capacity and what do you do before day X?

    Answer

    First I do not multiply 45% by four: CPU does not grow linearly, and the bottleneck is almost never where it seems. I run a test on one server with data similar to production and a real mix of endpoints, raising rps in steps until p99 goes beyond the SLO. Say the knee is 1400 rps, the working limit is 70% of it, so about 1000 rps per server. Separately I test the shared dependencies at the target 8000 rps: Postgres, Redis, PgBouncer, the network, an external API with a limit. The calculation: 8000 / 1000 = 8 servers, plus headroom for N+1 and deploys, about 10 in total, so 7 more are needed. I order right after the test so there is time for setup, or rent dedicated servers for a month. I verify plan B in advance: microcaching, disabling heavy features, load shedding. A week before the peak I do a dress rehearsal at full capacity.

    Follow-up question: How do you check that plan B will really work without waiting for the real peak?

  2. 2. Stack: a mobile client with a 10 s timeout, Nginx, Node, Postgres. When the database slows down, Node has thousands of requests in flight, memory grows, and Postgres has queries hanging that have been running for 5 minutes already. Here are the settings. What is wrong with the timeouts and how do you build a budget?

    Answer

    The rule: every internal timeout is shorter than the outer one. Otherwise the client has already left, yet the system keeps doing its work and holding resources. The client waits 10 s and retries. Nginx waits for upstream 60 s. Node: server.requestTimeout defaults to 300 seconds. The node-postgres pool with connectionTimeoutMillis 0 waits for a free connection forever. statement_timeout 0 in Postgres means no limit. When the database slows down, every client retry adds a new request, old ones are not cancelled, thousands of promises in Node wait for the pool, and the database has queries hanging whose results nobody needs.

    Follow-up question: How do you pass a deadline through a chain of three services so that the last one knows how much time it has left?

  3. 3. Implement a token bucket on Redis for 12 Node processes on three servers. Why not GET and SET from code? Whose clock do you use? What about Redis Cluster, key TTLs, and what does the API do when Redis is unavailable?

    Answer

    GET, computing in Node and SET is a race: two processes read 1 token at the same time, both allow the request, both write 0. Under load the limiter lets through more than configured. So all the logic goes in one Lua script, which Redis executes atomically. Inside: redis.call('TIME') gives Redis's current time, HMGET tokens and ts, refill for the elapsed time capped by capacity, if there is at least 1 token, subtract 1, HSET the new values, PEXPIRE for the full refill time (capacity / rate, with headroom). It returns allowed, the remainder and how many milliseconds until a token appears, which goes into Retry-After and the RateLimit headers. The clock is Redis's because on three servers time drifts by tens of milliseconds, and a process with a fast clock grants extra tokens. I load the script once with SCRIPT LOAD and call EVALSHA, and in Redis 7 FUNCTION LOAD and FCALL are more convenient.

    Follow-up question: A user with 50 thousand rps makes one key hot. How do you reduce the load on Redis without losing the limit?

  4. 4. A B2B API with 300 clients. One large client starts an import and takes 70% of the capacity, and the rest get timeouts even though each is within its own quota. How do you design limits so the system is fair? What do you do with a per-IP limit?

    Answer

    First: quotas per tenant, not per IP. The key is the API key or organization_id, the limits depend on the plan and are stored in the client config. I keep IP only for anonymous endpoints: behind NAT and CGNAT thousands of people share one IPv4, while in IPv6 one person has a whole /64 network, so I count the limit there by the /64 prefix. Second: I count not only the number of requests. The import makes requests each of which is 50 times more expensive than a GET. So each endpoint has a cost in tokens (GET 1, search 5, batch import 50), and the token bucket charges the cost. I add a concurrency limit per tenant: no more than 10 requests in flight at once, because it is concurrent heavy requests that take the database pool.

    Follow-up question: How do you achieve fair distribution in BullMQ if one queue holds jobs from 300 clients?

  5. 5. Design: a chat for 5 million DAU, 20% online at peak, each user sends 40 messages per day, an average conversation has 5 participants. Estimate the number of concurrent WebSocket connections, gateway nodes, messages and deliveries per second, and storage volume. Which of these is the most expensive?

    Answer

    Connections: 5 million × 20% = 1 million concurrent WebSockets. How many one Node process holds I measure, but the guideline is 40 or 50 thousand with tuned file descriptors and TLS terminated at the balancer. Memory per connection with buffers is on the order of 20 or 50 KB, so 1 or 2 GB per process. That gives 20 or 25 processes, with headroom for losing a server and a deploy 30, that is 3 or 4 servers of 16 cores. Messages: 5 million × 40 = 200 million per day, on average 2300 per second. I take the peak as four times: about 9 thousand writes per second. For a single database that is serious for writes, but realistic with partitioning or a Cassandra-like store.

    Follow-up question: How would you split the system into services so that deploying business logic does not drop a million connections?

  6. 6. After a gateway node deploy, 300 thousand clients reconnected within a few seconds. The authentication service went down, Postgres got a spike of history queries, some gateways were overloaded and some empty, and the system could not recover for 15 minutes. Analyze the causes and what you would change in the client, gateway and deploy.

    Answer

    The deploy closed connections at the same time, and every client reconnected immediately. Each reconnect is a TLS handshake, a token check, a write to the presence registry and a history query from last_seq. 300 thousand of those in a few seconds takes down authentication and the database. Clients get an error and immediately try again, so the system DDoSes itself and cannot leave that state. Exponential backoff with full jitter: a random delay from 0 to min(cap, base × 2^n), for example base 1 s, cap 60 s. The first attempt after a planned close also has a random delay of a few seconds. The server can pass a reconnect_after hint in the close frame. On SIGTERM the node is removed from the balancer and closes its 40 thousand connections gradually, over 2 to 5 minutes, not all at once.

    Follow-up question: How can you deploy gateway logic at all without dropping connections?

  7. 7. Design the API and data model of a URL shortener. How will you generate the code: a hash of the URL, a random string or a counter? What do you do about collisions and how do 10 API instances get unique ids without hitting a shared store on every write?

    Answer

    API: POST creates a link and returns the code, GET /:code serves the redirect, and DELETE is available to the owner. I make the redirect 302, because a browser caches 301 forever: analytics will not see repeat visits, and the link target can no longer be changed. The model: links(code primary key, long_url, owner_id, created_at, expires_at), where code uniqueness is held by the primary key, not by a SELECT before INSERT, because there is a race between them. A code from a URL hash or a random one produces collisions (with 6 billion records there will be thousands), so INSERT ... ON CONFLICT DO NOTHING and a retry with a salt. A counter with ranges is more reliable: each instance takes a block of 10,000 ids from a Postgres sequence and hands them out from memory, with no collisions by construction, and gaps in ids bother nobody. To keep codes unpredictable, the id is scrambled with a bijective function with a secret before base62.

    Follow-up question: How do you make the same long URL from the same user always produce the same code?

  8. 8. After upload a file must be checked with an antivirus, images need three thumbnails, and videos need a preview. ClamAV scans tens of MB/s per core, while ffmpeg on video works for minutes. Design this pipeline and the serving: where the queues are, how to keep video from blocking images, when a file becomes available and how to serve 20 Gbit/s of downloads.

    Answer

    Complete records status = uploaded and writes file.uploaded to the outbox, the relay publishes to the files exchange in RabbitMQ, and from it separate queues go out: files.scan for everything, files.image and files.video after the scan. Separate queues and worker pools are needed because in a shared queue 50 videos of 10 minutes each would take all the workers, and images that take 200 ms would wait an hour. A worker streams the object from MinIO to clamd with the INSTREAM command without writing to disk, and scan cores are sized from peak traffic with headroom for archives. A clean file gets status = ready, an infected one goes to the quarantine bucket with status = rejected. Results are written idempotently by fileId, with retry with backoff and a DLQ. Serving goes through a signed URL, a CDN or an Nginx cache in front of MinIO, with immutable keys, a long Cache-Control and Range.

    Follow-up question: How do you serve private files through a CDN so that the cache works but an outsider cannot download a file with a copied URL?

  9. 9. Compare three ways to store a suggestion index: the completion suggester in Elasticsearch, a field with an edge_ngram analyzer, and a prefix index in Redis (a sorted set with the top 10 per prefix). Describe the data model of each, the endpoint API, and what you would choose for 2 million products and 10 million popular queries.

    Answer

    The completion suggester in Elasticsearch builds an FST in memory, a prefix search takes 1 or 2 ms, and it has weight and fuzzy, but search works only from the start of the string and the weight is static, so updating popularity requires reindexing. A field with an edge_ngram analyzer writes prefixes of each word, so it works with filters and function_score by sales and in_stock, but the index is bigger and the query slower. A prefix index in Redis keeps a sorted set with the top 10 per prefix: reads under 1 ms, but memory grows with the number of prefixes, the rebuild is offline and there is no typo tolerance. The API is GET /suggest with q and limit and Cache-Control: public, max-age=300 for short prefixes so Nginx or a CDN can cache them. For 10 million popular queries I take Redis or completion, and for 2 million products with an availability filter edge_ngram.

    Follow-up question: How do you update suggestion popularity without reindexing everything every hour?

  10. 10. A sale by categories: "general admission, 20,000 tickets" with no seat selection. The first version keeps the remainder in one row: UPDATE inventory SET left_count = left_count, 1 WHERE category_id = $1 AND left_count > 0. At the start of the sale p99 grows to 8 seconds, the connection pool is exhausted, and database CPU is low. Why, and how do you remove this bottleneck?

    Answer

    All buyers of a category update one row, and Postgres serializes them through a row lock: if a transaction with its round trip takes 20 or 50 ms, the row passes only tens or hundreds of updates per second. The rest stand in a queue, holding pool connections, so the pool is exhausted while CPU is low because everyone is waiting. In pg_stat_activity this shows up as sessions with wait_event_type Lock. The first thing I do is shorten the transaction: no external calls between BEGIN and COMMIT. Second, I remove the single row, either by creating separate ticket rows and taking a free one through FOR UPDATE SKIP LOCKED, or by splitting the remainder into dozens of buckets. Third, I put a counter in Redis in front of the database so that "sold out" is answered without touching Postgres, and a controlled entry limits the number of concurrent transactions per category.

    Follow-up question: How do you verify under load that overselling truly does not happen, rather than just not showing up in this run?

  11. 11. Design a double-entry ledger model and a transfer API between users. Which tables, how do you guarantee that the sum of entries equals zero, how do you store the balance, and how does an Idempotency-Key protect against a double transfer when the client repeated the request after a timeout?

    Answer

    Tables. accounts(id, owner_id, currency, type, balance bigint). Types: a user account, a fee account, a technical provider account through which money enters and leaves. transactions(id, type, status, idempotency_key, created_at, metadata). entries(id, transaction_id, account_id, amount bigint, currency, created_at), where amount is signed: minus is a debit, plus is a credit. A transfer of 100 UAH with a 2 UAH fee: 10200 kopecks debited from A, 10000 credited to B, 200 to the fee account. The sum is 0. The invariant is checked in code before insertion and by a constraint trigger in the database, and a nightly job computes SUM(amount) over the entire ledger. UPDATE and DELETE on entries are forbidden by the application role's permissions. A mistaken operation is corrected by a new transaction with opposite entries and a reference to the original.

    Follow-up question: What do you do with an account that receives 500 credits per second, for example the fee account?

  12. 12. Design the data model and the job selection mechanism for a scheduler where 4 service instances run. Compare two approaches: a leader (via an advisory lock in Postgres or a lease in Redis) picks jobs and hands them out, versus all workers pick jobs themselves via FOR UPDATE SKIP LOCKED. How is a job not lost if a worker crashes in the middle of execution?

    Answer

    Model. jobs(id bigint, user_id, kind, payload jsonb, run_at timestamptz, tz text, rrule text, status, attempts, locked_by, locked_until, created_at). Statuses scheduled, running, done, failed. An index CREATE INDEX ON jobs (run_at) WHERE status = 'scheduled': partial, so small and without millions of completed ones. Each worker in a loop: UPDATE jobs SET status = 'running', locked_by = $1, locked_until = now() + interval '2 minutes', attempts = attempts + 1 WHERE id IN (SELECT id FROM jobs WHERE status = 'scheduled' AND run_at <= now() ORDER BY run_at LIMIT 200 FOR UPDATE SKIP LOCKED) RETURNING *. Competitors skip locked rows, so 4 workers take different batches with no coordination at all. The transaction is short, just the claim. The execution itself happens after COMMIT, not inside an open transaction.

    Follow-up question: What is wrong with node-cron or @nestjs/schedule in each instance for this task?

  13. 13. At peak 150,000 events per second arrive and ClickHouse can only accept 100,000, consumer lag in Kafka grows by millions, and meanwhile one ingest instance crashes with OOM because the producer buffers in memory. Describe backpressure from the storage to the client: what each layer does and what you deliberately drop.

    Answer

    The idea: every layer has a bounded buffer, and when it is full, a "slow down" signal goes upstream instead of memory growing to OOM. Kafka is the shock absorber here: if the 150,000 peak lasts an hour while ClickHouse takes 100,000, lag will grow by 180 million events, and a few hours after the peak the consumer will catch up. That is a normal mode while retention is 72 hours. We set the alert on lag in time (the age of the oldest unread message), not on the number of messages. With a large lag the consumer increases the batch to 200,000 rows, because ClickHouse writes large batches more efficiently, and adds instances to the group up to the number of partitions. You must not respond to lag with smaller and more frequent inserts: that causes Too many parts and is even slower. OOM means an unbounded buffer: send without await, promises pile up, and Kafka responds slowly at that moment, for example because of partition leader re-election.

    Follow-up question: How do you keep one noisy service that suddenly sends ten times more logs from eating the whole capacity of the pipeline?

  14. 14. A bare metal cluster of six nodes. At 9:00 traffic jumps, the HPA wants 30 API pods, but 10 of them sit in Pending, and latency is ten times higher for the first 4 minutes of the spike. Why did the HPA react so late and so little, and what do you change without a cloud and without automatic node provisioning?

    Answer

    The HPA runs a loop: every 15 seconds it reads metrics from metrics-server, which themselves lag by tens of seconds, computes the desired count as current pods times the ratio of the current metric to the target, and a new pod still has to pull its image, start and pass readiness. So the reaction to a spike takes minutes, not seconds. Pending means the scheduler cannot find a node with enough room for the requests: bare metal has no Cluster Autoscaler, so the ceiling is the total requests the six nodes can hold. What to do: keep headroom (a higher minimum replica count before a known peak or on a schedule, a lower utilisation target), speed up startup (a smaller image, a startupProbe instead of a long initial delay), set behavior for fast scale up and slow scale down, keep placeholder pods with a low PriorityClass that get evicted for real load, and turn on load shedding while capacity catches up.

    Follow-up question: Which metric would you scale a Node API on instead of CPU, and why?

  15. 15. The business needs a daily report: revenue by region broken down by product category and customer segment. The data lives in three services: orders, catalog, customers, each with its own database. How do you build the report? Why should you not just give BI read-only replicas and join there?

    Answer

    Read-only replicas with a join in BI work exactly until the first schema change in one of the services: the report breaks, and the service owner does not even know somebody reads their table. It is the same shared database, only the consumer is analytics. API composition: a separate service calls three APIs and joins the data in memory. For an order page with 10 rows that is fine. For a report over 5 million orders it is pagination across three services, load on production and hours of work. The right way: a separate analytical store, for example ClickHouse or a separate Postgres, populated by the services themselves. Option one: domain events OrderPaid, ProductCategorized, CustomerSegmentChanged, each service publishes through an outbox, and ingestion puts them into fact and dimension tables.

    Follow-up question: One service sent events with the wrong currency for a week. How do you fix the data in the store?

  16. 16. The orders service keeps a copy of a product's name and price, received from a ProductUpdated event from catalog via Kafka. After an incident some cards show an old price even though catalog has the new one. The consumer logs are clean. Name three causes of how this happens and how to build the copy so that it converges on its own.

    Answer

    The first cause: order. Without a key, events of one product land in different partitions, or the consumer processes a batch in parallel, and an old event overwrites a new one. Second: loss, because autoCommit commits the offset regardless of the database write, or catalog writes to the database and to Kafka without an outbox. Third: a replay of an old event after a new one, for example after an offset reset or a redrive from the DLQ. The logs are clean because every processing is formally successful. For the copy to converge: the message key is productId, the event has a monotonic version and a conditional UPDATE ... WHERE version is lower, and the event carries full state, not a delta. Publishing from catalog goes through an outbox, and the recovery path is a compacted topic or a full reload.

    Follow-up question: How in production would you notice that the copy diverged from the original before a customer complains?

  17. 17. The saga orchestrator for placing an order keeps state in process memory. During a deploy the pod restarted: the payment went through, the stock reservation is hanging, and the delivery is not created. How do you design the orchestrator to survive crashes? What do you write to the database and when?

    Answer

    Saga state is data, not variables in memory. A table saga_instances(id, type, state, payload jsonb, version, deadline_at, updated_at). Each transition is in one transaction: an UPDATE of the state with WHERE version = $old and an INSERT of the next step's command into the outbox. The relay publishes the command to the broker. If the process crashed after COMMIT, the command still goes out. If before COMMIT, nothing happened, and the transition will be repeated. Participants' replies come as events with a sagaId.

    Follow-up question: The compensation RefundPayment also fails: the provider returns 500 for two hours. What does the saga do?

  18. 18. A transfer saga: debit account A, credit B. Between the steps another request reads A's balance and allows one more debit, and then the saga compensates. Why does this happen? What countermeasures against missing isolation in a saga do you know, and what does a semantic lock look like in practice?

    Answer

    Each saga step is a separate local transaction that commits immediately. So a saga gives atomicity through compensations, consistency and durability, but not isolation. Between steps other requests see an intermediate state: that is a dirty read if the saga later rolls back, and a lost update if someone overwrites what the saga will later compensate. A semantic lock: a step does not just change data but marks that the record is in progress. A debit from A does not reduce balance directly but creates a hold: a row in holds with the status PENDING and an amount. The available balance is computed as balance minus the sum of active holds. Another request sees the reserved money and does not let it be spent twice.

    Follow-up question: A hold has hung for 3 days because the saga got lost. How do you notice it and who removes it?

  19. 19. The OrderPlaced v1 event had an amount field in hryvnias as a floating point number. Now currency and kopecks are needed: amount as an object with value and currency. The log has 40 million old events. How do you change the event schema? Why can you not just UPDATE the old events?

    Answer

    An event records a fact that has already happened, and projections, audit and possibly other services that received it through Kafka rely on it. An UPDATE of 40 million rows breaks the immutability of the log: the audit no longer proves what happened, and consumers who already processed v1 have a different history than the database. It is also a large migration with no way to roll back. A new optional field can be added without a version: old events do not have it, and the handler sets a default. Changing the type, units or meaning of a field is possible only through a new version: schema_version in metadata or a new event type. I do rounding explicitly, Math.round(amount * 100), because in a double 0.1 + 0.2 does not equal 0.3. Handlers and projections know only the latest version, and everything old lives in one place.

    Follow-up question: How do you test an upcaster so as not to break aggregate rehydration with old events?

  20. 20. Tracing 100% of requests at 3000 rps costs more than the whole cluster. How do you configure sampling? How does head-based differ from tail-based? How do you avoid losing the traces of exactly the requests that failed?

    Answer

    Head-based sampling: the decision is made by the first service at the moment the trace is created. In Node, OTEL_TRACES_SAMPLER=parentbased_traceidratio and OTEL_TRACES_SAMPLER_ARG=0.05, that is 5%. The decision is recorded in trace-flags, the last field of traceparent: 01 means sampled. The parent-based sampler in downstream services does not decide again but follows the parent's flag. If each service samples independently at 5%, there will be almost no complete traces across 4 services. The downside of head-based: the decision is made before it is known whether the request will fail, so 95% of errors are left without a trace. Tail-based: services send all spans to an OpenTelemetry Collector, and the tail_sampling processor waits for decision_wait, for example 10 seconds, and decides by policy: all traces with status ERROR, all longer than 1 second, and 1% of the rest.

    Follow-up question: A client complains about a specific request, and its trace may not have made it into the sample. How do you find it?

  21. 21. A vendor promises "exactly-once delivery". What do you ask? What do Kafka transactions really give? Why does that not solve the double-charge problem in Stripe?

    Answer

    First I ask: where exactly are the boundaries of this guarantee? A consumer cannot distinguish "did it but the ack was lost" from "did not do it". So only at-least-once is guaranteed, and exactly-once processing is achieved by deduplication: repeats are possible but the effect is one. This is called effectively once. In Kafka the idempotent producer removes duplicates on retries of writes to the log, and it is enabled by default. Transactions let you atomically write output messages together with the offset, and a consumer with isolation.level=read_committed does not see aborted ones. That is a real guarantee, but for a read-process-write flow inside Kafka.

    Follow-up question: If a consumer's output is a write to Postgres, how do you get effectively once without Kafka transactions?

  22. 22. Design an idempotent POST /payments. The client retries on a timeout. Describe the table schema, handling of two parallel requests with one key, a request with the same key but a different body, and the key retention period.

    Answer

    The table idempotency_keys(client_id, key, request_hash, status, response_code, response_body, created_at) with a unique index (client_id, key). The first insert with ON CONFLICT DO NOTHING decides who performs the operation. If the row was inserted, we are first and perform the payment. If the key already existed, we look at it: a different request_hash gives 422, a completed status returns the stored response including an error, and an execution still in progress gives 409 with Retry-After. This way a parallel request does not do the work a second time. Keys live a limited time, for example a day, and need cleanup. Separately you must think through a crash after calling the provider but before COMMIT: pass the same key to the provider.

    Follow-up question: What do you do if the process crashed after calling the payment provider but before COMMIT?

  23. 23. After an outage, 50 thousand messages accumulated in the DLQ over 6 hours. The bug was fixed and redrive was pressed. 5 minutes later the database went down, some messages are back in the DLQ, and some data was overwritten with old values. What went wrong and how should it have been done?

    Answer

    First: a redrive at full speed. That is 50 thousand jobs at once, although normally they are spread over 6 hours. The system got a peak it had never seen. Some failed again and went to the DLQ, which only increased the load. The events in the DLQ are old, and the main flow meanwhile processed new ones. When an old event was applied later, it overwrote a newer value. The fix in the consumer itself: apply an event only if it is newer.

    Follow-up question: How do you implement a bounded redrive in BullMQ if the jobs sit in the failed set?

  24. 24. Design a relay for an outbox on Postgres. Polling or CDC? How do you run several relay replicas without double publishing? How do you keep order per aggregate? What do you do about table growth?

    Answer

    Polling every 100 to 1000 ms is simple and works anywhere, but adds delay and load on the database. CDC through Debezium reads the WAL with minimal delay, but needs separate infrastructure and management of the replication slot, and an inactive slot holds back WAL cleanup. To start I take polling. Several replicas: either SELECT ... FOR UPDATE SKIP LOCKED in a transaction (parallel, but order between batches is not guaranteed), or a single active replica via an advisory lock, which is simpler and preserves order. Order per aggregate is ensured by the partition key in the broker. Double publishing is always possible, so I put a stable event_id in the event and the consumer deduplicates. The table is cleaned in batches or partitioned by time, the index is partial WHERE sent_at IS NULL, and the age of the oldest unsent row is the delay metric.

    Follow-up question: Why is a cursor "id > last read" dangerous in polling?

  25. 25. A topic with replication.factor=3, min.insync.replicas=1, and a producer with acks=1. After a broker failure, messages disappeared that the producer got a success for. How exactly did this happen? What configuration would you set and what does it cost when one and two brokers fail?

    Answer

    With acks=1 the partition leader answers the producer right after writing to its own log. Followers pull data asynchronously. If the leader fails before a follower has copied the record, the new leader becomes a follower from the ISR that does not have those records. The producer got success, and the data vanished. Kafka does not fsync on every write (flush.messages is off by default), so even restarting the same broker does not save you: durability is provided by replication. The second trap: min.insync.replicas=1. Even with acks=all, if both followers fall behind by more than replica.lag.time.max.ms (30 s) and drop out of the ISR, the leader is its own ISR, and acks=all effectively becomes acks=1.

    Follow-up question: Why is min.insync.replicas set on the topic and not on the producer, and what happens to a producer with acks=1 on such a topic?

  26. 26. A mobile client makes 3 attempts, the API gateway 3 attempts, the orders service 3 attempts to the payments service, and a background BullMQ job has attempts: 5. What happens when the payments service starts responding slowly? How do you distribute retries across layers correctly?

    Answer

    Retries multiply across layers: one user action gives 3 × 3 × 3 = 27 requests to payments, and a BullMQ job adds another ×5. Exactly when payments are slow, dozens of times more requests hit them, queues grow, and a metastable failure appears: even after the cause is gone, the retry storm keeps the system down. The right approach is to retry at one layer, the one closest to the failure or at the very edge, while intermediate layers return the error immediately. Add a retry budget, for example retries no more than 10% of requests, and deadline propagation so you do not retry when no time is left. Retry only transient errors and idempotent requests, and a circuit breaker quickly cuts off the failing dependency. Background jobs can retry longer, but with exponential backoff, jitter and a concurrency limit.

    Follow-up question: How do you verify in a load test that the system recovers on its own after a 2 minute dependency outage?

Train these questions

Study the cards, answer by text or voice and get an AI grade. Needs a signed in account.

Answer out loud and get graded

Here you get the questions with model answers. In the ShortLesson app you answer yourself, by text or voice, and AI grades the answer against the rubric and asks a follow-up, like a real interviewer.

The app is coming to the stores soon.

Other topics