When you miss a “Check yourself” question, it returns here a day later. Answer it right and it comes back after 3 days, then 7, and then it is done. Missing it again starts it over. The spacing is the point: remembering something after a gap is what makes it stick.
A pull request changes how a loyalty discount is calculated. It edits cart.ts, checkout.ts, invoice.ts and the admin sales report, a few lines in each, and none of those files imports another. What does the diff tell you?
Four files each hold their own copy of one responsibility, which is low cohesion, and its tell is a diff that touches many files without doing many things. Give the rule one module and the next change is one file.
None of them imports another, so there is no dependency between them to break. Each holds a separate copy of the same rule, which is a cohesion problem: one responsibility spread across four places.
Smaller pull requests are easier to review, but the code underneath is unchanged. The rule still lives in four places, and the next change still has to find all four, including the one nobody remembers.
How often a rule changes is up to the business. The cost here comes from where the rule lives: a frequent change to one module is cheap, while even a rare change to four files is how the fourth one gets missed.
Show the answer
The answer is A. The reasoning for each option is now shown under it.
Shipping exposes quote(order), which returns a Quote. A warehouse dashboard skips it and reads the shipping_rates table directly. Shipping now splits that table in two and changes nothing about quote or Quote. What has to change outside shipping?
Callers of quote depend on its name, its arguments and the Quote it returns, and none of those moved. The table sits behind the function, which is the reason for having the function.
It does still work, for the modules that use it. The dashboard never used it: it memorized the table's shape, and that shape just changed underneath it.
Depending on shipping is not one thing. The callers are on the top rung, a published function, and are untouched; the change lands only on the bottom rung, where the dashboard reached into storage.
The dashboard depends on shipping's storage, the bottom rung, so a storage change reaches it. Callers of quote depend on a function and its return shape, which did not change, so the blast radius is exactly one module.
Show the answer
The answer is D. The reasoning for each option is now shown under it.
Inventory decides whether to ship by checking statusOf(order) === 'PAID', where statusOf belongs to billing and returns a string. Which billing change reaches production without anyone noticing?
Inventory's call no longer resolves, so the build fails before anything ships. It is a nuisance, but it is the cheap kind of breakage: loud and immediate.
Inventory still compiles and still calls statusOf, but SETTLED never equals PAID, so no order counts as paid and nothing ships. There is no compiler error and no exception, because the dependency is on a value, which the compiler cannot check when the type is string.
Every call that does not pass the new argument stops compiling. It is work for inventory, but it cannot reach production by accident.
It looks like the sneaky one, but TypeScript rejects comparing a Promise<string> with PAID because the types have no overlap. The build fails, which makes this breakage loud.
Show the answer
The answer is B. The reasoning for each option is now shown under it.
Orders and billing live in one codebase and read each other's tables. The team splits them into two services, and each now fetches the other's rows as JSON over HTTP. What happened to the coupling between them?
Separate deploys change when the two ship, not what they know about each other. Orders still depends on the shape of billing's rows; it just finds out later when that shape changes.
A boundary helps when what crosses it is small and stable. What crosses this one is still the raw rows, so every fact orders memorized about billing's storage is still memorized.
Each service still depends on the other's storage, field names and all. What changed is the feedback: a rename that used to fail the build now fails a request in production. Same coupling, worse feedback loop.
The number of facts each side knows about the other did not change, so neither did the coupling. What got worse is when you find out that something broke.
Show the answer
The answer is C. The reasoning for each option is now shown under it.
A report exporter writes CSV or PDF, chosen by one if in one function. Nothing else in the codebase branches on the format, and nobody has asked for a third. A reviewer suggests an ExportStrategy interface with two classes. What is the better call?
Extracting later is a mechanical refactor once a real third format shows what the interface needs. Extracting now means guessing that interface from the two cases you have, and paying for a file, an interface and a lookup in the meantime.
Two branches that will stay two are cheaper as a conditional, and the condition lives in one place, so there is no duplicated knowledge to remove. Let the next real requirement design the seam.
Open/closed is about the risk of editing working code again and again. A conditional nobody needs to extend carries no such risk; the principle is a reason to extract when branches keep arriving, not a ban on if.
Show the answer
The answer is B. The reasoning for each option is now shown under it.
Customers on the enterprise plan pay by invoice, and everyone else pays by card. You already have InvoiceStrategy and CardStrategy. Where should the choice between them be made?
Selection belongs where the request comes in, as data: a table from plan to a prepared strategy. Checkout receives one and calls pay, so a new plan is a new table entry and checkout never learns that plans exist.
That puts a branch back into the context, the one place the pattern exists to keep free of them. Checkout would change for every new plan and would import both providers again.
A strategy that knows its siblings and passes work to them is no longer interchangeable. Choosing what happens next from inside is the shape of the state pattern; a strategy should know its own job and nothing else.
A subclass fixes the choice in a type instead of holding it in a reference, so every new variation needs another class, and plans times providers multiply fast. The choice is a value you pass in, not a class you write.
Show the answer
The answer is A. The reasoning for each option is now shown under it.
The crypto strategy needs an API key and the merchant's wallet address. The card strategy needs neither. Where should those two values go?
Then every order carries crypto settings and every strategy receives fields it ignores. The type stops describing what an order is, which is the signature that lies to every caller.
Changing pay changes the interface every strategy implements, so the card and PayPal classes would have to accept values they never use. The interface should hold only what all strategies share.
That teaches checkout one provider's needs, which is exactly the knowledge the pattern moved out of it. The next provider with its own settings would mean opening checkout again.
Configuration is fixed when the strategy is built, so it belongs to the instance: new CryptoStrategy(apiKey, wallet). The interface stays pay(order), and only the class that needs the values ever sees them.
Show the answer
The answer is D. The reasoning for each option is now shown under it.
An order moves from placed to paid to shipped, and what cancel() does depends on the stage: refund when paid, refuse when shipped. Each stage is an object, and each one decides which stage comes next. Which pattern is this?
Same skeleton, opposite flow of control. A strategy is chosen from outside and knows nothing about its siblings; here each object picks its own successor, which a strategy never does.
A plain status field with a switch in every method is the simpler alternative, and a fine one for a few stages. What is described here is more than a field: objects per stage that choose their successors.
In the state pattern the object drives its own transitions: paid knows that shipping leads to shipped. With strategy, something outside makes every choice and the strategies stay unaware of one another.
Show the answer
The answer is C. The reasoning for each option is now shown under it.
You load the 50,000 users from the opening into the sample HashTable, keyed by email. It keeps its 5 buckets, and the hash spreads the emails evenly. A login arrives with an email that matches nobody. About how many key comparisons does get make?
One comparison is what a table that resizes gives you. This one never grows, so each bucket holds about 10,000 users, and the hash only gets you to the right bucket, not to the right entry.
Half a chain is the average for an email that is in the table, because the search stops when it finds it. An email that matches nobody has nowhere to stop, so it walks the whole chain.
50,000 users ÷ 5 buckets is about 10,000 per chain, and a miss compares against every one of them. That is a fifth of the plain array scan, and still O(n). A table that resized at 0.75 would keep the average chain under one entry.
That is the plain array from the opening. Even a five-bucket table narrows the search to one bucket, about a fifth of the users.
Show the answer
The answer is C. The reasoning for each option is now shown under it.
A table has 16 buckets, holds 12 entries, and doubles when an insert would push the load factor past 0.75. What happens when a 13th key is inserted?
12 ÷ 16 is exactly 0.75, which is not past the threshold, but 13 ÷ 16 is 0.8125, which is. This is the occasional expensive insert that amortized O(1) allows for.
An entry's bucket is its hash modulo the bucket count, and the count just changed. Copied to the old index, every key whose hash modulo 32 differs from its hash modulo 16, about half of them, would sit where no lookup goes.
One more bucket barely moves the load factor: 13 entries in 17 buckets is about 0.76, still past 0.75, and every insert after it would cross the threshold and rehash everything again, O(n) each time. Doubling is what spreads one expensive step over many cheap ones.
13 ÷ 16 is 0.8125, past 0.75, so the table allocates 32 buckets and rehashes all 12 existing entries before placing the 13th. That one insert costs O(n); the next eleven, up to 24 entries, are cheap again.
Show the answer
The answer is D. The reasoning for each option is now shown under it.
In a language whose hash map hashes a key object from its fields, you store a value under point, with x 3 and y 4, then set point.x = 5 on that same object. map.get(point) now finds nothing, though nothing was removed. Why?
The entry was filed in the bucket that (3, 4) hashes to. The lookup hashes (5, 4), opens a different bucket, and never sees it. The entry is still in the table, unreachable, which is why hash keys are expected never to change.
Nothing tells the map that a field changed, so it leaves the entry exactly where it was filed. That is the trouble: the entry still takes up memory, and no lookup will ever reach it.
It is the same object, so its identity has not changed. The lookup fails before any comparison happens: it is looking in the wrong bucket.
A collision never replaces an entry. Chaining keeps both and compares full keys on the way out; only set with an equal key overwrites a value.
Show the answer
The answer is A. The reasoning for each option is now shown under it.
A public endpoint parses each JSON body into a hash map whose hash function is fixed and publicly known. One request arrives with 50,000 keys, chosen so that all of them have the same hash. Why is that one request so slow to process?
Resizing follows the number of entries, not collisions, so it happens as often as usual. And keys with the same full hash share a bucket at every table size, so each resize piles them back into one chain.
All 50,000 land in one bucket, so the k-th insert compares against the k − 1 keys already there: about 1.25 billion comparisons in total, O(n²) for a single request. A per-process random seed makes those colliding keys impossible to choose in advance.
Fifty thousand short keys take a few megabytes, the same whether they collide or spread out. Memory is fine; it is the time per insert that grew.
Show the answer
The answer is B. The reasoning for each option is now shown under it.
A duplicate check that compares every pair of items takes 2 ms on a cart of 100 items. A bulk import sends a cart of 1,000. About how long does the check take now?
The constant is already inside your measurement: 2 ms at 100 items includes it. What Big-O adds is how that number scales, and that is enough for an estimate.
Ten times the items, ten times the time is the linear intuition. Every-pair work grows with the square of the input: (1,000 ÷ 100)² = 100.
Ten times the items is a hundred times the pairs, (1,000 ÷ 100)² = 100, so 2 ms becomes about 200 ms. It is the opening story in miniature: the input grew tenfold and the check a hundredfold.
That is a thousandfold increase, which ten times the input produces only for O(n³) work, three nested loops. Two nested loops give a hundredfold.
Show the answer
The answer is C. The reasoning for each option is now shown under it.
A function links each order to its parent by running orders.find((o) => o.id === order.parentId) once for every order in orders. There is only one for loop in the function. How does its work grow with the number of orders?
find reads the array from the front until something matches, up to n comparisons, and it runs once per order: n × n. At 4,000 orders that is up to 16 million comparisons. Build a Map from id to order first and each lookup becomes O(1), the whole function O(n).
Counting the for keywords misses the loop inside find. A method call that scans an array is a loop, whatever it is called.
Being built in makes each comparison quick, not fewer of them. find has no index to jump through, so it scans; O(n log n) is what a good sort costs, not what a search costs.
Show the answer
The answer is A. The reasoning for each option is now shown under it.
A binary search over 1,000,000 sorted order ids takes about 20 steps. The list grows to 4,000,000 ids. About how many steps does a search take now?
Logarithmic growth is slow, not zero. Each doubling of the list adds one step, and this list doubled twice.
4,000,000 is 1,000,000 doubled twice, and each doubling costs one more halving: about 22 steps. Four times the data for 10% more work is what makes O(log n) so valuable.
Twice the steps for four times the data would be square-root growth. Halving the range on every step means one extra step per doubling, so two extra here.
Four times the steps for four times the data is linear growth, which is a scan. Binary search discards half of what is left on every step.
Show the answer
The answer is B. The reasoning for each option is now shown under it.
Every request checks a flag against a fixed array of 6 enabled feature flags with includes. A reviewer asks you to switch to a Set, because O(1) beats O(n). What is the right call?
Big-O promises that past some size the O(1) lookup wins and keeps winning. It says nothing about where that size is, and six fixed items may well sit below it.
The reverse. A Set is a hash table, with buckets and spare room to keep chains short; it spends memory to buy fast lookups, the trade this page describes.
Big-O describes growth, and this list cannot grow: it is 6 by design. At that size constants decide, and includes over six neighboring slots can beat hashing the string first. Measure, and revisit if the list ever stops being fixed.
Show the answer
The answer is C. The reasoning for each option is now shown under it.
An index on orders.id holds 400 entries to a page at every level. The table grows from 1,000,000 orders to 50,000,000. How many more pages does a lookup by id read?
That is how a full scan grows, one page read per page of table. A lookup reads one page per level, and levels grow far more slowly than rows.
At 1,000,000 keys: 2,500 leaves, 7 branch pages, one root. At 50,000,000: 125,000 leaves, 313 branch pages, still one root, because 313 fits in a page of 400. Three levels hold up to 400³ = 64,000,000 keys, so the table grew fiftyfold and the lookup did not change.
One extra level per doubling is how a binary tree grows, and 50 times is between 2⁵ and 2⁶. A B-tree level multiplies what the tree can hold by 400, not by 2.
A level is added only when the keys outgrow what the levels above can point to. Three levels at 400 a page reach 64,000,000 keys, and 50,000,000 is under that.
Show the answer
The answer is B. The reasoning for each option is now shown under it.
The order-history page runs WHERE customer_id = ? ORDER BY created_at DESC LIMIT 20. Which index lets the database read just the 20 newest entries for that customer, with no sort?
Sorted by date first, so one customer's orders are scattered through the whole index. Walking it newest first means passing every other customer's recent orders until twenty of this one's turn up.
It finds all of the customer's orders, but not in date order, so every one of them has to be fetched and sorted before the newest 20 are known. For a customer with thousands of orders that is thousands of rows for a page of twenty.
Two indexes are two separate trees. A planner can use one of them, and some engines can combine their matches, but neither keeps one customer's orders in date order, so the newest 20 still need a sort.
Equality column first, sort column second. The customer's entries form one run already in date order, so the database descends to the end of the run and reads twenty entries backwards.
Show the answer
The answer is D. The reasoning for each option is now shown under it.
orders has 2,000,000 rows at 100 to a page and an index on status. 90% of orders are 'delivered'. EXPLAIN shows WHERE status = 'delivered' running as a full table scan. Why?
The query keeps 1,800,000 rows. Fetched through the index, each can need its own page read, up to 1,800,000 of them, and even an engine that groups those fetches by page ends up reading nearly every page and the index on top. The scan reads the 20,000 pages once, in order. The planner estimates this from its statistics and picks the scan.
A corrupt index shows up as errors or wrong answers, not as a plan choice. Run the same query for a status held by a small share of rows and the planner would most likely use the index.
Uniqueness is not the requirement; the share of rows a query keeps is. An index on status serves a status that only a few rows have.
A B-tree sorts text as readily as numbers. Indexes on email and other text columns are among the most used in any schema.
Show the answer
The answer is A. The reasoning for each option is now shown under it.
An events table takes 5,000 inserts a second and has 6 indexes. Four of them have not been used by any query in months, and writes are falling behind. What does dropping those four change?
Unused by reads is not unused by writes. Every insert adds an entry to every index, whether or not any query ever reads it.
Only a query that used an index can lose it, and these four have not been used in months. Check the database's index-usage statistics over a window long enough to include rare jobs, such as a monthly report, before dropping one.
Today each insert writes the row plus 6 index entries, 7 structures, or 35,000 a second. With two indexes left it is the row plus 2, which is 15,000 a second.
Show the answer
The answer is C. The reasoning for each option is now shown under it.
Two requests withdraw 100 each from an account holding 500, at the same moment, at read committed. Each reads the balance, subtracts 100 in application code, and writes the result back with UPDATE ... SET balance = 400. What happens?
Both read 500 before either wrote, both computed 400, and the second write landed on top of the first. The account paid out 200 and recorded 100, with no error anywhere. Write balance = balance - 100 so the database does the arithmetic, or lock the row with SELECT ... FOR UPDATE before reading.
Each transaction is atomic, but atomicity is about one transaction's own writes, not about two transactions seeing each other. Neither saw the other's withdrawal, so neither subtracted it.
At read committed nothing checks that the value you read is still current. The second UPDATE waits for the first to commit and then overwrites it. At serializable the database would abort one of them with an error like that, for you to retry.
Show the answer
The answer is A. The reasoning for each option is now shown under it.
A transfer's COMMIT returns to your code. A moment later the server loses power, before the changed data pages have been written to disk. What does the database hold after it restarts?
Durability does not wait for the data pages. It waits for the log, which is small and written in order, and the log is enough to rebuild the pages.
Before COMMIT returned, a record of both writes was appended to the write-ahead log and flushed to disk. Recovery replays it into the data pages. That is why the pages can be written lazily: the log already holds the truth.
Recovery never leaves half a transaction. Committed work is replayed whole from the log, and anything uncommitted is discarded whole.
Show the answer
The answer is B. The reasoning for each option is now shown under it.
A bug makes transfer credit 210 where it debits 200. The accounts table has no constraints beyond its primary key. Starting from A at 500 and B at 120, the buggy transfer runs in a transaction. What happens?
The database has never heard of 620. Consistency enforces the rules you declared, and none of them mentions a total, so 300 and 330 are two perfectly valid numbers.
A transaction never commits part of itself. Both writes succeeded, so both commit together.
Every declared rule holds, so the transaction commits: A 300, B 330. It kept your code's result whole and had no way to know the result was wrong. "Money is conserved" lives in your code unless you declare it to the database.
Show the answer
The answer is C. The reasoning for each option is now shown under it.
A checkout does everything inside one transaction: insert the order, charge the card through a payment provider's API, insert the invoice. The invoice insert breaks a constraint, and the transaction rolls back. Where does that leave the customer?
The rollback removes both inserts, but the charge happened in the payment provider's system the moment its API answered, and a rollback reaches only this database. Undoing it takes a compensating action, a refund, which is the saga idea. The call also held the transaction open, locks and all, while the provider responded.
ROLLBACK undoes this database's writes and nothing else. A transaction covers one database, and the card was charged in another company's system.
The order insert was inside the same transaction, so the rollback removed it with everything else in this database. Half-done states inside the database are the one thing a rollback does prevent.
Show the answer
The answer is A. The reasoning for each option is now shown under it.
Your DELETE /orders/42 answers 204 the first time and 404 when a client retries it. A reviewer says the second answer breaks idempotency. Are they right?
Idempotency is defined by the state the server is left in, not by identical responses. A 404 on the retry is an honest answer about a resource that is already gone.
GET, PUT and DELETE are all defined as idempotent. A client that retries a DELETE after a timeout is relying on exactly that.
After one DELETE or two, order 42 is gone, and that is all idempotency promises. The responses are allowed to differ: some APIs answer the retry with 404 and others with 204, and both are idempotent.
Show the answer
The answer is C. The reasoning for each option is now shown under it.
A mobile app sends POST /payments, and the connection drops before any response arrives. The payment may or may not have gone through. What should the app do?
If the first attempt landed, the server recognizes the key and returns the original result; if it did not, the retry does the work. Either way the customer is charged once, which is otherwise impossible to guarantee after a lost response.
Nothing on the server compares two POSTs unless you build it. Each one is a new submission, so the retry is a second payment unless a key ties the two together.
The first attempt may well have succeeded, since a lost response looks exactly like a lost request. A user told it failed will press Pay again, which is the double charge, done by hand.
PUT is idempotent because it names the resource it replaces, and /payments is the collection, not a payment. It works only if the client picks the id itself, PUT /payments/<id>, which is an idempotency key by another name.
Show the answer
The answer is A. The reasoning for each option is now shown under it.
A client gets one of these replies to a request. Which one is worth sending again, unchanged, a moment later?
The resource does not exist, and sending the same request again will not make it exist. A retry just repeats the question.
The server understood the request and rejected what it said. The same body will be rejected the same way; the client has to change the request, not resend it.
The request clashes with the resource's current state, such as editing an order that was already cancelled. Resending it meets the same state; the client has to re-read the resource and decide again.
Anything in the 500s puts the failure on the server, and 503 says it is temporary: overloaded or restarting. The request itself was fine, so the same request a moment later may succeed. Retry after a delay, and only requests that are safe to repeat.
Show the answer
The answer is D. The reasoning for each option is now shown under it.
Checkout keeps each user's cart in the memory of whichever server handled their first request. You add a second server behind a round-robin load balancer. What do users notice?
Round-robin hands each request to the next server in turn, with no memory of who sent it. Keeping a user on one server is a separate feature, sticky sessions, and even then the cart vanishes when that server restarts.
With two servers taking turns, roughly half of a user's requests land on the server that never saw their cart. The fix is statelessness: keep the cart in a resource, such as /carts/7, that any server can read.
Each server keeps a separate cart per user in its own memory. The problem is not that carts are shared but that only one server holds each one.
Nothing syncs two processes' memory unless you build it. Both servers answer happily, one of them with an empty cart, which is worse than an error because nobody is told.
Show the answer
The answer is B. The reasoning for each option is now shown under it.
A mobile app wraps POST /payments in a retry loop and calls crypto.randomUUID() for the Idempotency-Key at the top of each attempt. The first reply is lost and the loop retries once. What does the server make of the two requests?
That needs the retry to carry the key the first attempt stored. A fresh UUID per attempt is a key the server has never seen, so there is no stored reply to find.
It is the other way round: the bodies are identical and the keys differ. A 422 is for a known key that arrives with a different request.
A 409 is for a key that is still in progress. The first attempt had finished, since its reply was lost after the work, and in any case the retry carries a different key.
The key has to name the payment, not the attempt. Generated inside the loop, it changes every time, which is the same as sending none. Create it once, when the customer presses Pay, and reuse it on every retry.
Show the answer
The answer is D. The reasoning for each option is now shown under it.
Your server looks the key up with a SELECT, and only if it is absent charges the card and then INSERTs the key with the reply. A charge takes 3 seconds. A client whose timeout is 2 seconds sends its retry, with the same key, while the first attempt is still charging. How many charges are made?
The first request writes its key only when its 3-second charge is done, so the retry, at 2 seconds, looks and finds nothing. Both charge. Claim the key as in progress before charging, with an insert that fails on a duplicate, and the retry gets 409 instead.
The key is written after the charge finishes, at 3 seconds, and the retry looked at 2. A key recorded after the work protects only the retries that arrive after the work.
Each statement is atomic, but the SELECT and the INSERT are separate statements with a 3-second charge between them, and the other request runs its SELECT in that gap. Atomic statements do not make a check-then-act sequence atomic.
A 409 needs a record saying the first attempt is in progress, and this design writes nothing until the work is done. Inserting the key as in progress before charging is exactly what would produce it.
Show the answer
The answer is A. The reasoning for each option is now shown under it.
Keys expire 24 hours after they are stored. A nightly job sends 500 refunds, each keyed by its refund id, and crashes after 300 have succeeded. Someone reruns it the next evening, 30 hours after the first run, with the same keys. How many customers are refunded twice?
Only the 300 that succeeded the first night can be paid twice. The other 200 were never paid, so the rerun pays them for the first time, which is what it is for.
The 300 successful refunds stored their keys 30 hours ago, and those records expired at the 24-hour mark, 6 hours before the rerun. A key the server has forgotten is a new request, so each of the 300 is paid again. The window has to outlast the slowest path a retry can take, and a rerun the next day is one.
The 200 that never ran are new work, and the rerun pays each of them once, which is correct. The duplicates are among the 300 that had already been paid.
Identical keys only help while the server still remembers them. After 24 hours the records are gone, and the rerun's keys look brand new to it.
Show the answer
The answer is B. The reasoning for each option is now shown under it.
A consumer handles deposit messages. In one database transaction it credits the account and inserts the message id into a processed table with a unique key. It commits, then crashes before acknowledging, and the broker redelivers the message. What happens to the account?
The missing ack makes the broker deliver again, but a delivery is not a credit. The consumer finds the message id in processed and skips the work.
The crash came after the commit, and a committed transaction survives a crash; that is the durability in ACID. Only uncommitted work is rolled back.
The credit and the id committed together, so the redelivered message finds its id and is acknowledged without doing anything. Had the crash come before the commit, neither would exist, and the redelivery would do the work for the first time.
A message is dead-lettered after repeated failures, and this one has been redelivered once. Its second delivery succeeds, as a no-op.
Show the answer
The answer is C. The reasoning for each option is now shown under it.
An admin changes a product's price. Reads go through cache-aside with a 10-minute TTL and no explicit invalidation. What do customers see?
On a hit, cache-aside never asks the database. The database being right does not help a reader whose request stops at the cache.
The cached copy is served until its TTL runs out, and the next read after that misses and fetches the new price. This is what the TTL buys you: a guaranteed ceiling on how long an answer can stay wrong.
Nothing compares the two. That is what makes stale data dangerous: it is served with the same confidence as fresh data, and no one is warned.
That would be true of a cache with no TTL. The expiry exists precisely so that a missed invalidation heals itself.
Show the answer
The answer is B. The reasoning for each option is now shown under it.
Your database is comfortable at 50 reads a second. Traffic is 1,000 reads a second, all through the cache. What is the lowest hit rate that keeps the database comfortable?
Half of 1,000 still misses, so the database gets 500 reads a second, ten times what it can take.
Sounds high, but 10% of 1,000 is 100 misses a second, twice the limit. This is the trap of thinking in hit rate rather than miss rate.
A 5% miss rate on 1,000 reads is exactly 50 reads a second. Work from the miss rate: allowed misses ÷ total reads = 50 ÷ 1,000 = 5%, so the hit rate must be at least 95%.
This works, with only 10 misses a second, but it is not the lowest. Going from 95% to 99% cuts database load fivefold, which shows how much those last few points are worth.
Show the answer
The answer is C. The reasoning for each option is now shown under it.
A homepage key is read 2,000 times a second and has a 60-second TTL. Once a minute, the database load spikes hard. What is happening?
A longer TTL only makes the spike rarer; a shorter one makes it more frequent. The spike comes from what happens at the moment of expiry, not from how long the entry lives.
A key read 2,000 times a second is the most recently used entry in the cache, so LRU would evict it last. The timing, once a minute, points at the TTL.
Averaged over the minute, the hit rate is above 99.8%. The average hides the problem, because all the misses arrive in the same instant.
This is a stampede. At 2,000 reads a second, a 100 ms database query lets about 200 requests miss together, and every one of them goes to the database. The fix is a single in-flight load per key that the others wait on, which is the exercise above.
Show the answer
The answer is D. The reasoning for each option is now shown under it.
A user renames themselves. Your code deletes user:42 from the cache straight away. A day later, the comment thread still shows their old name. Why?
Invalidation has to cover every cached value built from the data, not only the one stored under its obvious key. The thread was cached with the name copied into it, and nothing deleted that entry. This is the hard part of invalidation the section describes.
It did: the profile page shows the new name. The stale name is coming from a different cache entry.
If it had not, the profile would be stale too. The rename committed, and one stale corner was left behind.
That can happen, but it would not survive a day of other visitors loading fresh pages. The staleness is on the server, in a derived cache entry.
Show the answer
The answer is A. The reasoning for each option is now shown under it.
A reader is 150 ms round trip from your origin and 10 ms from an edge that already holds the file. On a fresh connection, the first byte takes three round trips: the TCP handshake, the TLS handshake and the request. When does it arrive from each?
Three round trips at each distance: 3 × 150 and 3 × 10. The edge shortens all three, because the browser makes its TCP connection and its TLS session with the edge itself, and the origin takes no part in the conversation.
That sends the handshakes to the origin and only the request to the edge. The connection is made with the edge, handshakes included, so all three round trips are the short ones.
That counts only the request. A new connection pays for its handshakes first, which is why distance hurts more than one round trip suggests: it is multiplied by every round trip before the first byte.
An edge holding a fresh copy answers without asking anyone. It contacts the origin only on a miss, or once its copy has expired.
Show the answer
The answer is A. The reasoning for each option is now shown under it.
A bug ships in /app.js, served with max-age=31536000 and no hash in the URL. You deploy the fix and purge the whole CDN. A week later, some readers are still running the buggy file. Why?
A purge finishes long before a week is out. These readers are not getting the old file from an edge at all.
max-age is addressed to every cache on the way, and the browser is one. Readers who loaded the file before the fix keep it for up to a year, and a purge only empties the CDN. A new URL is the one thing that reaches them, because their page then asks for a name they have never cached.
Then every reader would have the bug, new ones included. After the purge, the edges refilled from the origin with the fixed file; the stale copies are held further out than the CDN.
Show the answer
The answer is B. The reasoning for each option is now shown under it.
Your /account page shows the signed-in reader's name and recent orders. To speed it up, a teammate changes its header to Cache-Control: public, s-maxage=60. What is the risk?
That would be the risk if the edge kept a copy per reader. It keeps one per URL, and every reader asks for the same /account.
A shared cache keys on the URL unless it is configured otherwise. public told the edge this response is the same for everyone, and the edge believes the header.
Nothing in that header makes the edge check anything. For 60 seconds it answers /account from its copy, without asking the origin who is asking.
The edge stores the first response for /account and serves it to everyone who asks in the next 60 seconds, whoever they are. A personalized response needs private or no-store, which keep it out of shared caches entirely.
Show the answer
The answer is D. The reasoning for each option is now shown under it.
Your fingerprinted assets have sat at edges across Europe and Asia for months. You launch in Brazil, and that morning the origin's traffic for those same files jumps. Why?
They are routed to their nearest edge, like every other reader. It is that edge that goes to the origin, once per file.
Fingerprinted files are cached for a year, and an expiry in Europe would show up whatever was happening in Brazil. The timing points at the launch.
Each edge is its own cache, so months of traffic in Europe warmed nothing in South America. Each edge there misses once per file and then serves it locally, so the jump fades on its own.
Absorbing floods is half of what a CDN is for. The origin sees misses, not overflow, and these misses come from edges seeing the files for the first time.
Show the answer
The answer is C. The reasoning for each option is now shown under it.
Your balancer probes each server every 5 seconds and removes one after 3 failed probes in a row. Traffic is 300 requests a second, round-robin over three servers. One server crashes just after passing a probe. About how many requests fail before it is removed?
This balancer learns only from its probes, not from real traffic, so it keeps routing to the dead server until the third probe fails. Some balancers also watch real responses (passive health checks) precisely to close this gap.
That would be true if one failed probe removed the server. Requiring three in a row protects against a single unlucky timeout, and the price is three intervals of failures, not one.
The dead server keeps its third of the traffic, 100 requests a second, until the probes at 5, 10 and 15 seconds have all failed. 100 × 15 is 1,500.
Only the dead server's share fails. The other two keep answering their turns, so two requests in three still succeed.
Show the answer
The answer is C. The reasoning for each option is now shown under it.
A pool uses least-connections. Server B holds 900 idle WebSocket connections. Servers A and C hold 40 connections each, all busy requests, and their CPUs are running hot. Where do the next requests go?
Least-connections compares counts, and 40 is less than 900. The connections on B cost it almost nothing, but the balancer cannot see that, which is why pools like this count outstanding requests instead of open connections.
B does have the most spare capacity, but least-connections never measures capacity. It counts open connections, and by that count B looks like the busiest server in the pool.
That is round-robin. Least-connections always picks the lowest count, and B will not have it until A and C each pass 900.
Show the answer
The answer is A. The reasoning for each option is now shown under it.
Each server's health endpoint runs a query against the shared database. One afternoon the database slows to a crawl for a few minutes. What does the balancer do?
The servers share the database, so they slow down together. There is no single slowest server to remove; every probe fails at once.
That is what a check of the process alone would do. This endpoint checks the database, so it fails whenever the database is slow, however healthy the process is.
A load balancer chooses between servers, not between their dependencies. All it knows is that every server answered its probe badly.
Every server reports unhealthy at once and the balancer empties the pool, so even requests that never touch the database fail. The endpoint tested a shared dependency rather than whether this one server can serve; some balancers fail open when every server fails, to survive exactly this.
Show the answer
The answer is D. The reasoning for each option is now shown under it.
Carts live in server memory, so the balancer pins each client to one server with a cookie. Traffic grows and you add a fourth server. An hour later it is nearly idle. Why?
A server failing its checks gets nothing at all. Nearly idle means it is in rotation and receiving a small share of the traffic.
Only clients who arrive without a cookie can be sent to the new server; everyone already shopping stays where their cart is. Stickiness turns adding a server into a slow drift rather than a rebalance, which is one more reason to move carts into a shared store.
A balancer adds a server to the pool once it passes its health checks, not an hour later. The new server is in the pool; few clients are free to be sent to it.
Show the answer
The answer is B. The reasoning for each option is now shown under it.
With the sample's numbers, one instance handles 400 rps and the largest for rent handles 800. Your peak is 300 rps today and should double within a year, and the business can live with a rare short outage. What is the sensible plan?
That pays for 800 rps while you use 300. A resize is one command when the peak actually needs it, so there is nothing to gain from buying the headroom a year early.
Doubled, the peak is 600 rps, and verticalHeadroom(600) is still 200: one machine covers it. Scaling out now buys a year of coordination costs to solve a problem you do not have yet.
The vertical road still has 200 rps of room at the doubled peak, and since the business accepts a rare outage, availability does not force your hand. Keeping sessions and caches out of process memory is the cheap insurance that lets you scale out later without a rewrite.
Show the answer
The answer is C. The reasoning for each option is now shown under it.
You scale out to four instances, each keeping its own in-process cache of product prices with a 5-minute TTL. An admin changes a price. For a few minutes, refreshing the page shows the old price and the new one by turns. Why?
One database holds one current price. The old one is coming from copies kept outside it.
Four caches filled at four different moments, so they expire at four different moments, and each refresh shows whichever copy the balancer happens to reach. A single instance had one cache, and it could not disagree with itself.
A browser cache would show the old price on every refresh until it expired, not alternate. Alternating means the answer depends on which server you reach.
Show the answer
The answer is B. The reasoning for each option is now shown under it.
Two fleets each have 2,400 rps of capacity: three instances at 800 rps, or six at 400. Peak traffic is 1,800 rps. One instance fails at peak. Which of the two still keeps up?
Their totals match, but a failure removes one instance, and the instances are not the same size. Losing 800 leaves 1,600; losing 400 leaves 2,000.
Each has more room, and each takes more capacity down with it when it fails. Three at 800 drop to 1,600, which is 200 short of the peak.
Only if the fleet was sized with nothing spare. Six at 400 still have 2,000 rps after losing one, which is 200 more than the peak.
Three at 800 drop to 1,600, 200 short of the peak; six at 400 drop to 2,000, 200 over it. Smaller instances make each failure a smaller share of the fleet, which is the availability argument for scaling out in many small units.
Show the answer
The answer is D. The reasoning for each option is now shown under it.
A nightly job bills every customer, and an in-process lock stops it from running twice. After you scale out to three instances, every customer is billed three times in one night. Why?
Nothing sent a request. A nightly job is started by a scheduler inside each instance, and the balancer only routes incoming traffic, which this job never was.
The database did exactly what it was asked. Three separate processes each asked it once per customer.
A lock only excludes threads inside one process. Every instance has its own scheduler and its own lock, so each found the job unlocked and ran it. A job that must run once needs a lock they all share, or one place chosen on purpose to run it.
An expired lock could let a second run start, but it would not make the number of runs match the number of instances. Three instances, three bills: the count gives it away.
Show the answer
The answer is C. The reasoning for each option is now shown under it.
Orders arrive at a steady 90 a minute, all day, with no spikes. Each worker finishes 40 a minute, and you run two. What does the queue's depth do?
A queue absorbs bursts, not a shortfall in the average. With 90 in and 80 out every minute, there is never a quiet spell for the workers to catch up in.
Nothing makes it level off: arrivals do not slow down because the queue is long. Depth settles only when the workers finish at least as much as arrives.
Two workers finish 80 a minute against 90 arriving, so 10 more wait every minute, which is 14,400 by the end of the day. A third worker, at 120 a minute, is the fix; more buffer only delays the day you notice.
That is 90 minus one worker. Both workers pull from the same queue, so together they finish 80 a minute, and the shortfall is 10.
Show the answer
The answer is C. The reasoning for each option is now shown under it.
Stock reservations go through the queue. During business hours the warehouse API goes down for 20 minutes. What do customers checking out notice?
Checkout only publishes, so it never learns the warehouse is down. Reservations wait in the queue and drain once the warehouse is back, provided retries back off rather than burning through their limit in the first few seconds.
That was true when checkout called the warehouse directly. Now checkout only writes a message, and writing a message does not involve the warehouse at all.
Publishing costs about the same whether the queue holds ten messages or ten thousand. That is the decoupling in time: a slow consumer does not make the producer wait.
A message that is not acked goes back on the queue, not away. The reservations are late, not lost, and even one that exhausts its retries lands in a dead-letter queue rather than the bin.
Show the answer
The answer is A. The reasoning for each option is now shown under it.
A consumer sends the receipt email, then records the job id in done. It crashes between the two, and the broker redelivers the message. What does the customer get?
That is at-most-once, which is not what the broker gives you. The message was never acknowledged, so it comes back.
The crash came before the job id was recorded, so done has no idea the email went out. The check can only catch what was written down.
The broker knows only about acks, and this message was never acked. It cannot see anything the consumer did before it crashed.
The first delivery sent the email but died before recording it, so the redelivery looks brand new and sends it again. A done record written after the side effect always leaves this gap; closing it means the email service itself must recognize the order and refuse a second send.
Show the answer
The answer is D. The reasoning for each option is now shown under it.
Two workers consume email-changed messages. A user changes their email to b@example.com, and a second later to c@example.com. Nothing crashes and nothing is redelivered, yet the account ends up with b@example.com. Why?
The queue handed them out in arrival order, b first and then c. The order was lost later, once the two workers had them.
One worker took b and the other took c a moment later, and c finished first, so b landed last and overwrote it. Handed out in order is not finished in order; updates to one record need one consumer per key, or a version check that refuses the older write.
The two messages carry different changes and different ids, so nothing treats them as duplicates. Both were applied, in the wrong order.
Show the answer
The answer is B. The reasoning for each option is now shown under it.
Checkout wraps every outgoing call, to payments and to the email service alike, in one shared breaker. The email service starts timing out. What happens to card payments?
The breaker counts failures through it, not per destination. Email timeouts trip it, and an open breaker refuses everything it wraps, payments included.
One breaker means one count and one state for both services, so an email outage becomes a payments outage. Give each dependency its own breaker, and a dependency whose endpoints fail independently one per endpoint.
A breaker never retries; it either lets a call through or refuses it. The slowdown here is the email timeouts themselves, until the shared breaker opens and refuses payments as well.
Nothing here queues. An open breaker answers at once with an error, so charges fail immediately rather than wait, and that is the problem, since payments was never down.
Show the answer
The answer is B. The reasoning for each option is now shown under it.
Checkout makes 40 calls a second to a recommendations service, through a breaker that opens after 10 failures in a row. The timeout is 5 s and checkout has 150 worker threads. Recommendations starts to hang. When does checkout run out of threads?
The breaker counts a failure only when a timeout fires, and the first one fires 5 s after the hang began. By then 40 × 5 = 200 calls have started, more than the 150 threads.
That counts a call as failed the moment it is sent. A hung call is not a failure until its timeout fires, so nothing has failed at 0.25 s; the threads are simply all still waiting.
Timeouts free threads, they do not take them. The threads were taken by calls that are still waiting, and they run out before the first timeout fires.
150 threads ÷ 40 calls a second = 3.75 s, and the first timeout fires at 5 s. The breaker is blind until a timeout fires, so the timeout has to be short enough that rate × timeout fits in the pool: under 3.75 s here, and in practice well under.
Show the answer
The answer is D. The reasoning for each option is now shown under it.
Checkout runs as 20 instances, and each one creates a CircuitBreaker for payments when it starts, with a 30 s cool-down. Payments goes down for ten minutes, and every breaker trips within a second of the others. About how many charges reach payments in each 30 s while it is down?
Each process has its own breaker object, with its own count and its own clock, and none of them knows what the others have seen. Twenty breakers on the same schedule send twenty trials together. That is usually harmless; across hundreds of instances, random jitter on the cool-down spreads them out.
One trial per breaker, but there are twenty breakers. Nothing shares their state between processes unless you build that, and most deployments do not.
A breaker does not listen for health reports. It learns only from real calls, which is why half-open sends a real charge as its trial.
Half-open lets exactly one call through and refuses the rest until that call settles. Traffic comes back only after a trial succeeds and the breaker closes.
Show the answer
The answer is A. The reasoning for each option is now shown under it.
A flash sale brings a wave of declined cards. Payments answers each one in 50 ms with 402 card declined, and the breaker trips open, refusing every checkout, including the cards that would have gone through. What should change?
That only delays the same mistake. A big enough wave of declines still trips it, and a real outage now takes longer to detect.
A decline is payments working correctly and answering quickly. Count timeouts, refused connections and server errors, and let a 4xx business answer pass through as a result. The breaker exists to detect a sick dependency, not unlucky customers.
The next trial may well succeed, but the declines keep coming, so the breaker trips again moments later. A shorter cool-down turns one wrong decision into a breaker that flaps.
A declined card declines again. Retrying doubles the calls and the failures, and the breaker trips twice as fast.
Show the answer
The answer is B. The reasoning for each option is now shown under it.
A Node.js checkout handler runs const stock = await db.getStock(id); if (stock > 0) await db.setStock(id, stock - 1); in one process on a one-core server. One unit is left. Two orders arrive together, both succeed, and stock ends at 0. How?
Node runs all of your JavaScript on one thread, and it does not need a second one to race. Each await hands that thread to the other request, which is enough to interleave them.
A race needs interleaving, not parallelism. One core switches between the two requests at every await, so both can read before either writes.
Order does not matter here: both writes store 0, so either order ends the same. The damage was done earlier, when both handlers read 1.
Each await lets the other handler run. Both read 1, both pass the stock > 0 check, and both write 0: two sales, one decrement. It is check-then-act, and the same lost update as the scene.
Show the answer
The answer is D. The reasoning for each option is now shown under it.
A page counter runs SELECT views FROM pages WHERE id = $1, adds one in TypeScript, then runs UPDATE pages SET views = $2 WHERE id = $1. At peak traffic it undercounts. Which change fixes it?
At the default isolation level of most databases, two transactions can still both read 5 and both write 6, and each is valid on its own. A transaction makes one request all-or-nothing; it does not stop two of them reading the same value.
A delay moves the collisions around rather than removing them. Two requests can still both read before either writes, and at peak traffic they will; the bug just becomes rarer and harder to reproduce.
The database now reads and writes the row as one step, under its own row lock, so no second request can read the old value in between. It removes the window instead of guarding it, which is why it is the first fix to reach for.
Nothing reports an error. Both UPDATEs succeed and each changes one row; a lost update throws nothing and leaves no trace for a retry to react to.
Show the answer
The answer is C. The reasoning for each option is now shown under it.
A unit test awaits increment(counter) once on a counter at 5 and asserts 6. It has passed for months while production loses views. Which test would have caught the bug?
await Promise.all([increment(counter), increment(counter)]) overlaps the two read-modify-writes on purpose. Against the in-memory counter on this page it fails every time, with 6. A race only shows up when operations overlap, so the test has to make them overlap.
Each repetition finishes before the next one starts, so no two increments ever overlap, and it passes a thousand times. Running a sequential test more often does not make it concurrent.
Stepping through one call shows one call; the bug needs a second one landing inside its window. A debugger also changes the timing it is meant to observe, which is how a race becomes a Heisenbug.
Cores are not the missing ingredient. This code races on a single thread; what the test lacks is a second increment running at the same time, and no machine will start one for it.
Show the answer
The answer is A. The reasoning for each option is now shown under it.
Sign-up runs SELECT 1 FROM users WHERE username = $1 and inserts the user if no row comes back. Two people choose ada at the same moment, and their requests land on two different app servers. What prevents a duplicate account?
A mutex lives in one process. The other server has its own, so the two requests never wait for each other. A lock only serializes work that shares it.
The database, which owns the state, checks and inserts as one step, so the second insert fails however the requests interleave. Catch that error and tell the person the name is taken.
The shared state is the users table, not memory. Both requests can check, both find no ada, and both insert. Two machines race over a database row as easily as two threads over a variable.
Show the answer
The answer is B. The reasoning for each option is now shown under it.
A job runs orderIds.forEach(async (id) => { await sendReceipt(id); }) and then logs done. There are three ids, and each send takes 100 ms. When does done appear?
That reads forEach as a loop that waits for each callback. It does call them one after another, but each callback returns a promise at its first await, and forEach drops that promise and moves straight on.
The three sends do run together and finish at about 100 ms, but nothing waits for them. Waiting for every one takes await Promise.all(orderIds.map(sendReceipt)), which is probably what this code meant.
Each callback starts its send and returns a promise at the await, and forEach ignores the promises. Run it and done is logged about 1 ms in, with all three receipts sent about 100 ms later.
Nothing rejects them. forEach accepts any function, TypeScript compiles this without a word, and the sends really happen. That is what makes it dangerous: it looks as if it works.
Show the answer
The answer is C. The reasoning for each option is now shown under it.
loadDashboard awaits Promise.all over its three calls. Today getRecommendations rejects at 300 ms, while getUser and getOrders succeed at 200 and 400 ms. When does the caller's catch run, and what happens to getOrders?
Promise.all rejects the moment any input does, with that error. It cannot stop the other calls, because a promise only reports on work already under way. Run it and the catch fires at 300 ms, while the orders still arrive at 400, to be thrown away.
Rejecting the combined promise does not reach back into the calls. A JavaScript promise has no cancel; stopping the request itself takes an AbortSignal passed into getOrders.
That is Promise.allSettled, which waits for all three and never rejects. Promise.all fails fast: one rejection is enough to settle it, 100 ms before the orders are back.
There is no partial result. Promise.all either fulfils with every value or rejects with the first failure; it has no slot for an undefined in place of one.
Show the answer
The answer is A. The reasoning for each option is now shown under it.
placeOrder calls saveOrder(order) without await and then returns 'saved'. Its caller wraps await placeOrder(order) in try/catch. The database is down, so the save rejects after 50 ms. What happens in Node?
The try covers what the caller awaits, and the caller awaits placeOrder, which fulfilled with saved before the save failed. The rejection belongs to a promise nobody awaited, so no catch anywhere is waiting for it.
Leaving out await is not an error. The call starts the save and returns a promise, which the line simply drops. TypeScript compiles it too; catching it takes a lint rule such as no-floating-promises.
An async function does not wait for promises it never awaits. placeOrder returns saved at once, before the save has finished, let alone failed.
The caller is told the order was saved at 0 ms. At 50 ms the save rejects with no handler attached, which Node has treated as fatal by default since version 15: it prints the error and exits with code 1. A browser would log it and carry on, with the order still lost.
Show the answer
The answer is D. The reasoning for each option is now shown under it.
A sign-up service runs await Promise.all(passwords.map(hashPassword)) over three passwords. hashPassword is an async function whose body computes for 100 ms without awaiting anything. The server has four cores. How long does that line take?
Promise.all runs nothing; it only waits for promises that already exist. All three calls run on the one JavaScript thread while the other cores sit idle. Using them takes worker threads.
Each call runs its 100 ms loop to the end before it returns its promise, so map takes 300 ms just to build the array, and Promise.all has nothing left to wait for. A native call such as crypto.scrypt in Node is different: it runs on a thread pool, and there Promise.all does overlap the work.
async changes what a function returns, not where it runs. The body runs synchronously on the thread that called it, up to its first await, and this body has none.
Calling a function runs it; await starts nothing. The three hashes are computed during the map, before Promise.all is even called.
Show the answer
The answer is B. The reasoning for each option is now shown under it.
Given async function f() { console.log(1); await null; console.log(2); }, what does setTimeout(() => console.log(3), 0); f(); console.log(4); print?
That assumes await null does not pause, since there is nothing to wait for. await always suspends, even on a value that is not a promise, so f hands the stack back and 4 prints before 2.
f runs synchronously up to its await, printing 1, then suspends and returns to the script, which prints 4. The rest of f resumes as a microtask, so 2 prints as soon as the stack empties, and the timer task prints 3 last.
That treats the code after await as a fresh task queued behind the timer. It resumes as a microtask, and the microtask queue drains before the loop takes any task.
Calling an async function does not postpone its body. It starts at once, on the same thread, and runs until its first await, so 1 prints before the script reaches 4.
Show the answer
The answer is B. The reasoning for each option is now shown under it.
What does this print? setTimeout(() => console.log(5), 0); Promise.resolve().then(() => console.log(1)).then(() => console.log(2)); Promise.resolve().then(() => console.log(3)).then(() => console.log(4));
That treats a chain as one unit that runs to the end. Each .then is its own microtask, queued only when the step before it finishes, so it joins the back of the line behind the other chain.
The timer was registered first, but it waits in the task queue, and every microtask runs before the loop takes a task. Registration order only counts within the same queue.
That stops the drain at the microtasks that were waiting when the stack emptied. The loop keeps draining until the queue is empty, including microtasks added along the way, so 2 and 4 beat the timer too.
The microtask queue starts as 1, 3. Running 1 queues 2 behind 3; running 3 queues 4 behind 2. The two chains take turns, and the timer prints 5 once the queue is empty.
Show the answer
The answer is D. The reasoning for each option is now shown under it.
A click handler calls buildReport(), a loop over two million rows that takes 300 ms, and the page freezes on every click. A teammate changes the handler to call setTimeout(buildReport, 0) instead. What happens now?
The timer only moves the work into a later task on the same thread. When that task runs it holds the stack for the full 300 ms, so no frame is painted and no click is handled until it returns. The freeze has moved, not gone.
That reads a zero delay as now. A timer never runs its callback inside the code that set it; it queues a task, and that task cannot start until the handler has returned.
The host runs the timer, but the callback is JavaScript, and all JavaScript on the page runs on one main thread. Moving work off that thread takes a Web Worker, not a timer.
A callback runs to completion once it starts; the loop cannot pause it halfway to paint a frame. Slices only exist if you write them yourself, as in the exercise above.
Show the answer
The answer is A. The reasoning for each option is now shown under it.
A clock should redraw as often as possible, so it is written as function tick() { drawClock(); queueMicrotask(tick); }. After the first tick(), the tab freezes: the clock never visibly changes and clicks do nothing. Why?
Each tick returns before the next one starts, because the next call waits in the queue, not on the stack. The stack never holds more than one tick, so nothing overflows, and no error ever appears.
Each call can be fast and the tab still freezes, because there is never a gap between calls for the loop to paint in. The trouble is not speed; it is that the loop never gets a turn.
The loop drains the microtask queue completely, including microtasks added during the drain, before it paints or takes a task. Every tick adds the next, so the queue never empties. Scheduling with requestAnimationFrame(tick) gives the loop a turn between frames.
Microtasks run on the same single thread as everything else on the page. That is exactly why they can starve it: while one is running, nothing else can.
Show the answer
The answer is C. The reasoning for each option is now shown under it.
A breach report says 3,000 rows in your stolen users table share one identical hash. Each was stored as SHA-256 of the password plus one site-wide salt kept in the app config. What went wrong?
Leaked or not, those 3,000 rows would still match each other, because they share one salt and one password. The report describes identical digests, and no amount of secrecy about the salt prevents those.
A salt only separates users when each has their own. With one salt for everyone, equal passwords still give equal digests, and an attacker builds one table for that salt. A fresh random salt per user would have made 3,000 different digests.
The rows match because the inputs match, not because the output ran out of room. A longer digest of the same input is still the same digest every time it is computed.
They did, and per-user salts exist for exactly that case. Nothing stops users choosing alike; the storage can still be stopped from showing it.
Show the answer
The answer is B. The reasoning for each option is now shown under it.
A pull request stores sha256(salt + password), with a fresh 16-byte random salt per user. You are reviewing it. Which change matters most?
A salt works in plain view: it makes every digest different even when the attacker can read it. Encrypting it adds a key to protect and leaves the real weakness where it was.
SHA-512 is still a fast hash, built to be quick. Guessing depends on how cheap one hash is to compute, not on how long its output is.
That doubles a cost measured in billionths of a second. Repetition is a real technique, and PBKDF2 repeats a hash hundreds of thousands of times, but at that point you are choosing a tunable cost, which is the actual fix.
The salt already defeats precomputed tables and identical rows. It cannot make one guess expensive, and with a fast hash the attacker still tests billions a second against each user. A slow, tunable password hash is the missing piece.
Show the answer
The answer is D. The reasoning for each option is now shown under it.
A teammate proposes encrypting passwords with AES, the key in an environment variable on the app servers, so support can read a password out to a user who forgot it. What is the problem?
The table and its key usually sit on, and are backed up from, the same systems, so the breach that leaks one tends to leak the other, and then every password comes back at once. Support wanting to read passwords is the warning sign: a login never needs one back.
AES with a random 128-bit or 256-bit key is far beyond brute force. The cipher is fine; the trouble is where its key has to live.
They can: decrypt and compare. That is exactly the problem, because anything the server can decrypt, an attacker who has the key can decrypt too.
Only in a deterministic mode. With a fresh random nonce, the usual way to run AES-GCM, equal passwords encrypt differently, and either way the one key still unlocks every row.
Show the answer
The answer is A. The reasoning for each option is now shown under it.
Logins use bcrypt at cost 10, and each step up doubles the work. You raise it to 12. An attacker could test 4,000 guesses a second against one stolen cost-10 hash. How many a second against a hash made at cost 12?
That counts two steps as one doubling. Each step doubles the work, so two steps quadruple it.
That scales the work with the number itself, 12 against 10. The cost counts doublings: going from 10 to 12 is 2 × 2 = 4 times the work.
Two doublings make each guess four times the work, so 4,000 ÷ 4 = 1,000. That holds only for hashes made at cost 12: until a user logs in and is rehashed, their row still holds a cost-10 hash, which falls at the old 4,000 a second.
That divides by ten, as if each step multiplied the work tenfold. The cost counts doublings, so two steps divide the rate by 4.
Show the answer
The answer is C. The reasoning for each option is now shown under it.
A pull request adds a status filter to findOrder. The id is still bound as $1, and the status from the URL is placed in the query text inside quotes: AND status = '<status>'. What should the review say?
Parameterization is a property of each value, not of the query. One value spliced into the text is the whole hole, and a $1 elsewhere in the same query does nothing to close it.
Escaping by hand is the blocklist the page warns about: it has to anticipate every encoding and dialect quirk, and someone has to remember it on every path. Binding the value removes the question.
The comment is one way to dispose of the leftover quote, not the only one. A status of ' OR '1'='1 balances its own quotes and needs no comment at all.
The status is user input placed in query text, which is the bug whatever else the query does right. Bind it as a second parameter and the text is a constant again.
Show the answer
The answer is D. The reasoning for each option is now shown under it.
A support ticket: a customer named O'Brien gets a 500 error when searching for their orders, and nobody else does. The log shows a syntax error near Brien. What does this tell you?
An apostrophe that breaks the query is proof the input is being parsed as SQL: it ended a string literal early. The same path will run ' OR 1=1 -- without an error. Bind the value, and the ticket and the hole close together.
That turns away real customers to protect one query, and it is a blocklist: the next field, or the next dialect quirk, is not covered. The fix is to stop the input being parsed at all.
An error is an attack that went wrong. The input that broke the query by accident can be rewritten on purpose, ending the literal and adding OR 1=1 --, and that version returns rows instead of an error.
Show the answer
The answer is A. The reasoning for each option is now shown under it.
An injectable endpoint connects to the database as a role that may only SELECT from orders and products. An attacker finds the hole. What can they do with it?
The role cannot tell which part of a query was injected. The injected text runs as part of a query the role is allowed to run, so whatever the role can read, the attacker can read.
Injected SQL runs with exactly the connection's permissions. This role can read both tables, so all of both is exposed, other customers' orders included. Least privilege did not stop the attack; it decided how big the attack could get.
Injected SQL runs as whoever the application connected as, not as the database owner. This role cannot drop anything, which is the point of keeping it narrow.
UNION is a real technique for reaching other tables, but it is still a SELECT run by this role, and this role cannot read users. The database refuses it.
Show the answer
The answer is B. The reasoning for each option is now shown under it.
Signup stores each username with a parameterized INSERT. A nightly report later builds ... WHERE username = '<name>' by pasting in names it has read from the database. One user signed up as x' OR 1=1 --. What happens when the report runs?
The parameter kept the name from running as SQL at signup. It stored the exact characters, quote included, and they are just as dangerous the next time someone pastes them into query text.
Where a string was read from does not matter; who wrote it does. A value a user typed is user input on every path, including the ones that read it back out of storage.
The report pastes the name into its text, so the quote ends the literal and OR 1=1 matches every row, a day after the input arrived. This is second-order injection, and the reason the rule says no input becomes query text on any code path.
Show the answer
The answer is C. The reasoning for each option is now shown under it.
A test for isExpired(token) passes all day on every laptop, and fails in CI only on runs that start a few minutes after midnight UTC. What is the most likely cause?
Slowness makes a test time out, not change its answer, and it would not line up this neatly with one time of day. The failures following the clock is the clue.
A test that depends on the current time gets a different input on every run, and some of those inputs cross a boundary like midnight. Pass the time in, isExpired(token, now), and the test can fix the moment and get the same answer forever.
Leftover state makes a test fail depending on which tests ran before it, not on the time of day. Order-dependent failures follow the run order; these follow the clock.
Several assertions in one test make a failure harder to read, but they do not make it intermittent. Something is changing between runs, and the time of day is what changes.
Show the answer
The answer is B. The reasoning for each option is now shown under it.
orderTotal has 100% line coverage. A bug report shows it returning NaN for an order from an old client that left unitPriceCents off one line. How did the suite miss it?
Every line did run: the function is short, and the first test alone executes all of it. The coverage number was accurate; it just measures something other than the inputs you tried.
toBe compares with Object.is, which treats NaN as equal to itself, so expect(result).toBe(NaN) works. Nothing compared against NaN because no test produced it.
The tests on this page assert on the totals orderTotal returns, which is behavior. The gap is in which orders they try, not in what they look at.
Coverage counts the lines that ran, not the inputs they ran with. undefined * 2 is NaN, and NaN plus anything is NaN, so one missing price spoils the whole total, and only a test with that input would have shown it.
Show the answer
The answer is D. The reasoning for each option is now shown under it.
You rewrite the body of orderTotal from a for loop into map and reduce. Every total it returns is identical, and four tests turn red. What were those tests most likely asserting?
Only tests tied to the implementation, such as one that spied on a helper or counted loop iterations, can fail while every output stays the same. Tests on what orderTotal returns would have stayed green, and that is what makes a refactor safe.
Every total is identical, so a test that checks totals sees exactly what it saw before. Tests on returned values stay green through a rewrite, which is the property that makes them worth having.
A crowded test hides which behavior broke, but it still only fails when a behavior changes. Here none did, so the red came from something other than behavior.
If the old totals were wrong and the new ones are identical, the tests would have been red before the rewrite too. Red that appears only after a refactor that changed no output points at the tests, not the totals.
Show the answer
The answer is A. The reasoning for each option is now shown under it.
A rewrite of orderTotal fails the first test with expected 3000, received 600. The input is twoLines: 1200 × 2 on one line, 600 × 1 on the other. Which bug produces exactly 600?
Then it would add the bare prices, 1200 + 600, and receive 1800. That is the failure the page uses as its example of an informative message.
Stopping after the first line gives 1200 × 2 = 2400. The received number points somewhere else.
With total = where total += belongs, each line replaces the one before, and the last line is 600 × 1 = 600. Each wrong guess predicts a different number, which is why the received value in a failure message is half the diagnosis.
That multiplies both prices by 2: 2400 + 1200 = 3600. Close in spirit, but the number does not match, and the number is the evidence.
Show the answer
The answer is C. The reasoning for each option is now shown under it.
Nothing to review right now. Questions you miss in any “Check yourself” are scheduled here automatically; pick a concept from the table of contents and keep going.
The schedule lives only in this browser, like your reading progress. Clearing site data clears it.